概要
独習アセンブラ8.4.1において、Cでインラインアセンブラによってシステムコールwriteを呼び出し、標準出力に文字列を出力するコードを実行させる際に、64bit環境では下記のエラーが出て実行できませんでした。
64bit環境で動作させるための方法を調べて整理します。
前提
gcc --version gcc (Ubuntu 9.4.0-1ubuntu1~20.04.1) 9.4.0
- 今回利用したコード:
GitHub - kakts/assembly-language-training
Dockerでubuntuコンテナを立てて、そのコンテナ内で実行しています。
インラインアセンブラによってwriteシステムコールを呼び出す際にSegmentation Faultエラーが出る
独習アセンブラの著者のgithubリポジトリに今回実行しようとしたコードがあります。 asm/write.c at master · h-ohsaki/asm · GitHub
char *str = "Hello, World!\n"; int main (void) { asm ("movl str, %ecx"); // ECX ← 文字列のアドレス asm ("movl $14, %edx"); // EDX ← 文字列の長さ asm ("movl $4, %eax"); // システムコール 4 番は write asm ("movl $1, %ebx"); // 標準出力 (1) asm ("int $0x80"); // システムコール呼び出し }
このコードをコンパイルし、実行ファイルを実行すると Segmentation faultエラーがでました。
$ gcc -g -no-pie -fno-pic -fomit-frame-pointer -o write write.c user:~/src/inline$ ./write qemu: uncaught target signal 11 (Segmentation fault) - core dumped Segmentation fault
1つずつコードを確かめて原因を探ると、システムコールを呼び出す割り込み命令実行時にエラーになっていることがわかりました。
asm("int $0x80");
原因
原因としては、実行する環境が64bitなのですが、上記コードにおいて、システムコール呼び出しに使っているint 0x80
という割り込みコードが32ビットに対応した方法だったことが原因でした。
64ビット環境では、int 0x80
を使わず、他の方法でシステムコールを実行する必要がありました。
システムコールの呼び出しにint $0x80を実行するのは、32bit x86アーキテクチャ用のもので、x86_64アーキテクチャでは、syscall
という命令を使うことでシステムコールを呼び出せます。
呼び出す命令が変わるのと、システムコールを実行するにあたり、どのシステムコールを呼び出すか、また、引数の値をどのレジスタに値をセットするかの方法も異なります。
対処方法
x86_64ではint $0x80によってシステムコールが実行できないのを知り、さらに深掘りして調べたところ、下記の記事が見つかりました。 stackoverflow.com
一部抜粋しますが、64ビットのアーキテクチャではsyscallを使ってシステムコールを実行します。 実行にあたり利用するレジスタも、ビット数がことなるため32ビットのint 0x80を実行するときとは異なります。
32-bit code: mov eax,4 ; In "int 0x80" style 4 means: write mov ebx,1 ; ... and the first arg. is stored in ebx mov ecx,esp ; ... and the second arg. is stored in ecx mov edx,1 ; ... and the third arg. is stored in edx int 0x80 64-bit code: mov rax,1 ; In "syscall" style 1 means: write mov rdi,1 ; ... and the first arg. is stored in rdi (not rbx) mov rsi,rsp ; ... and the second arg. is stored in rsi (not rcx) mov rdx,1 ; ... and the third arg. is stored in rdx syscall
元々あったコードはコメントアウトし、64ビット環境でも実行できるコードに変更しました。
/** * @file write.c * 8.4.1 writeシステムコールの呼び出し */ char *str = "Hello, World!\n"; int main(void) { /** * 32bitと64bitでシステムコールの呼び方がことなる * https://stackoverflow.com/questions/22503944/using-interrupt-0x80-on-64-bit-linux */ // 32bit版 // asm("movl str, %ecx"); // ECX<-文字列のアドレス // asm("movl $14, %edx"); // EDX<-文字列の長さ // asm("movl $4, %eax"); // システムコール4番はwrite // asm("movl $1, %ebx"); // 標準出力(1) // asm("int 0x80"); // システムコール呼び出し // 64bit(x86_64)版 asm("mov $1, %rax"); // 1番はシステムコールwrite /** * システムコールを呼び出す際、 * 第1引数、第2引数と、引数の順番に合わせて値をセットするレジスターが決まっている。 * syscall実行時に、システムコールごとに所定のレジスタから値を取り出して実行する * * 第1引数: rdi * 第2引数: rsi * 第3引数: rdx * ... */ // writeの引数の値のセット asm("mov $1, %rdi"); // 第1引数 ファイルディスクリプタ stdout asm("mov str, %rsi"); // 第2引数 メッセージをwriteに渡す asm("mov $14, %rdx"); // rdx 文字列の長さ asm("syscall"); // システムコール実行 }
使用しているレジスタについて軽くまとめると以下になります。
- rax: 実行するシステムコールの番号 (1がwriteにあたる)
- syscall実行後の戻り値がraxにセットされる
- rdi: システムコール関数の第1引数
- rsi: システムコール関数の第2引数
- rdi: システムコール関数の第3引数
このコードをコンパイルして、実行すると文字列が表示されるようになりました。
gcc -g -no-pie -fno-pic -fomit-frame-pointer -o write write.c user@50596b389067:~/src/inline$ ./write Hello, World!
x86_64におけるシステムコールの番号
linux v5.0のコードですが、raxに指定するシステムコールの番号はlinuxのソースコードで確認できます。 github.com
x86_64でのlinuxにおけるシステムコール実行時の処理
下記のコードでシステムコール実行時の処理をレジスタの扱いに関するコメントともに確認できます。
github.com
まとめ
今回は64ビットアーキテクチャで、C言語からインラインアセンブラによってシステムコールを実行する方法を整理しました。 64ビットにおけるシステムコール実行についての情報は、あらためて別の記事で整理したいと思います。