kakts-log

programming について調べたことを整理していきます

64ビットアーキテクチャにおいてインラインアセンブラでシステムコールを呼び出す

概要

独習アセンブラ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"); // システムコール実行
}

使用しているレジスタについて軽くまとめると以下になります。

このコードをコンパイルして、実行すると文字列が表示されるようになりました。

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ビットにおけるシステムコール実行についての情報は、あらためて別の記事で整理したいと思います。

参考

stackoverflow.com

web.archive.org