アセンブラでのHello worldを理解する
Syscallについて調べる。
あとからこの内容はブログにまとめるつもりです。
いちどメモをしておきたいのと、zennのスクラップを使ってみたかっただけ
モチベーション
- syscallについて昔調べたけど忘れてしまっているので復習したい。
- これを理解したい https://twitter.com/TaKOBKi/status/1388467581703520257
まず復習
今知っているsyscallについての知識をぶちまける。
何も見ずに書いていくので間違ってるかも。間違っても知らん!当てにしないで。
あってないかも知れないけど、何を知ってそうで、何を知らないかを一度整理しておきたい。
syscallはシステムコールのことを指している。アセンブラやc言語でシステムコールを発行するときはsyscallと書くので、一般的な略し方はsyscallだと思っている。
syscallはなぜ必要
OSが管理しているものをアプリケーションは触れないので、OSに処理をお願いする必要がある。(例えばディスクアクセス)なので、それを依頼しているのがsyscallだと理解している。
なぜアプリケーションから触れないのか?とか疑問に思った人はなぜOSというものがあるのかを調べると良いよ!
syscallの呼び方
ここらへんをすっごく忘れている。おぼろげながらに書く。
x86を調べた気がするので、他のアセンブラだとどうなるか全然知らない。
x86だとどっかのレジスタに呼び出したいsyscallの番号と引数を入れ込んでおく。
syscall番号を入れておくやつと、引数を入れておくやつは別のレジスタね。
その後にsyscall
ってやると呼ばれた気がする。
現状の理解としてはこんな感じ。
ふわふわしているところはちゃんとした理解にしたい。
syscall番号の調べ方
syscallには名前と番号がある。アセンブラで呼ぶときは番号が必要なのでそれを調べる必要がある。
man 2 $syscall_name
で見れるかと思ってたんだけど見れないらしい。使い方っぽいのは見ることが出来たが番号は取れなかった。(ちなみにman man
など叩いてみると面白いですよ)
printf '#include<sys/syscall.h>' | cpp -dM - | grep '#define __NR_'
で見られることを教えてもらった。
例えばwriteの番号を知りたくなったときは次のようにすればいい。
$ echo '#include<sys/syscall.h>' | cpp -dM - | grep '#define __NR_write'
#define __NR_write 1
#define __NR_writev 20
syscallの呼び方
x86_64のlinuxでsyscallを呼び出すには次のようにすれば良い
- raxレジスタにシステムコールの番号を格納
- 必要なら引数を各レジスタに格納する(第一引数はrdi, 続いて rsi, rdx, r10 r8, r9 という順に引数を入れておく)
- syscallを実行
example
exit systemcallを呼び出すコードを例として添えておきます。
mov rax, 60 # exitは60番なので60をセット
mov rdi, 0 # exit statusを第一引数で突っ込んでおく
syscall # 実際に呼ぶ
アセンブラの実行環境が手元にないので、Rustのインラインアセンブラを使用して動作確認を行いました。
#![feature(asm)]
fn main() {
println!("ここは出る");
unsafe {
asm!(
"mov rax, 60",
"mov rdi, 1",
"syscall",
);
}
println!("出ないぴょん");
}
本題
これを理解しましょうか。
最初は次の2文から見ていきます。
ここはRustのインラインアセンブラ特有の話になりそうですね。主題とは関係ないので雰囲気で理解しておきましょう。
in("rdx") buf.len(),
in("rsi") buf.as_ptr(),
このような理解で良いのでは?と思っています。
-
rdx
レジスタにbufの長さを突っ込んでいる。 -
rsi
にbufのアドレスを突っ込んでいる。
そして、この処理はアセンブラの最初に埋め込まれると予想できますね。(末尾に書いてあるが、これらはwrite system callの引数として使うので最初に実行しておかないとそもそも"hello world"が適切に動かないはず)
次。write system callを呼んでいるところですね。
これは先程の、system callの呼び方を理解していれば良さそうですね。
"mov rax, 1",
"mov rdi, 1",
"syscall",
man 2 writeとするとwriteの使い方が出てきます。cコードによるシグニチャを見ると、第一引数でファイルディスクリプタのid, 第2にバッファのポインタ,最後にバッファサイズを取ることがわかります。
ssize_t write(int fd, const void *buf, size_t count);
rdiに1を入れているので、ファイルディスクリプタ1に書き込むことがわかりますね。また、この2つでは第2引数にbufのポインタ、第3引数にbufの長さ(サイズ)を渡していることがわかりますね。
(ファイルディスクリプタの1は標準出力です)
in("rdx") buf.len(),
in("rsi") buf.as_ptr(),
最後。
"mov rax, 60",
"xor rdi, rdi",
"syscall",
raxに60を書き込んでいますね。先程使ったsystemcallの番号を調べるコマンドを叩けばこれがなんのシステムコールなのか調べることが出来ます
$ echo '#include<sys/syscall.h>' | cpp -dM - | grep '_NR.*60'
#define __NR_exit 60
...
exitを呼んでそうなことが分かりますね。
引数はxor rdi, rdi
としているので、rdiに0がセットされるようですね。
なのでexit status 0でexitしていることが分かります。
レジスタに0を入れたいときはxorを使うのが定石なんでかね?mov register, 0
としたほうが分かりやすそうだけど何かしらの理由がありそう?
ここでは特に深ぼらないことにします。
まとめ
いささか雑でしたが、割とシステムコールやアセンブラの理解が深まった気がしました。
この記事が、なにかのお役に立てれば嬉しいです。