概要
C言語からアセンブリ言語で定義した関数呼び出し,反対にアセンブリ言語からC言語で定義した関数呼び出しをやってみる.また,この過程をlldbを用いてステップ実行し,ABIの確認もしてみる.
環境
- OS
- MacOSX 10.12.6
- アセンブリ言語
- nasm 2.14.02(home brewでインストール)
NASMのテスト
どこかから拾ってきたHello Worldのサンプルをアセンブルし,実行してみる.
; helloworld.asm
GLOBAL start
SECTION .data
str_hello db "Hello World", 0x0a ; Output string and \n
SECTION .text
start:
mov rax, 0x2000004 ; Set system call to write=4.
mov rdi, 1 ; Set output to stdout.
mov rsi, str_hello ; Set output data.
mov rdx, 12 ; Set output data size.
syscall ; Call system call.
mov rax, 0x2000001 ; Set system call to exit=1.
mov rdi, 0 ; Set success value of exit.
syscall ; Call system call.
アセンブルとリンク
>nasm -f macho64 helloworld.asm
>ld -o helloworld helloworld.o
実行
>./helloworld
Hello World
NASMでプログラムを書き,実行できることを確認できた.
C言語からアセンブリ言語の呼び出し
C言語からアセンブリ言語で定義した関数を呼び出してみる.
#include <stdio.h>
extern int add(int v1, int v2); // アセンブリ言語で定義した関数
int main(void) {
int r = add(1, 2);
printf("r = %d\n", r);
}
GLOBAL _add ; addをグローバルスコープで見えるようにする.
SECTION .text ; TEXTセクション
_add: ; addラベルの定義(C言語からaddとして参照できる)
mov rax, rdi ; 第一引数をraxにコピー
add rax, rsi ; 第二引数をraxに加算
ret
この例では,C言語のmain
からアセンブリ言語のadd
関数を呼び出す.なお,後述するABIにより,関数の返り値にはrax
が使われるので,この関数を呼出した側はrax
の値を受け取ることになる.
コンパイルと実行
>nasm -f macho64 add.asm
>gcc -c main.c
>gcc -o main main.o add.o
>./main
r = 3
アセンブリ言語からC言語の呼び出し
アセンブリ言語からC言語の関数を呼び出す.ここでは,C言語の関数に引数がない場合,引数も返り値もある場合,両方実行してみる
なお,libcをリンクする関係で,プログラムはmain
関数から始まらないといけないので,Cでmain
を定義し,アセンブリ言語の関数を呼び出し,その中でさらにC言語の関数を呼び出す,ということにする.
引数なし/返り値なし関数の実行
#include <stdio.h>
extern void sub();
void test() {
printf("Hello\n");
}
int main(void) {
printf("Main\n");
sub();
}
extern _test ; test関数を呼び出せるように
GLOBAL _sub
SECTION .text
_sub:
call _test
ret
コンパイルと実行
>nasm -f macho64 sub.asm
>gcc -c 1.c
>gcc -o main 1.o sub.o
>./main
Main
Hello
このように,main
-> sub
-> test
という関数呼び出しが実行できていることが分かる
引数あり/返り値あり関数の実行
先程の例に,関数を追加する.
#include <stdio.h>
extern void sub();
void test() {
printf("Hello\n");
}
int add(int v1, int v2) {
int sum = v1 + v2;
printf("sum = %d\n", sum);
return v1 + v2;
}
void show(int i) {
printf("v = %d\n", i);
}
int main(void) {
printf("Main\n");
sub();
}
extern _test,_add,_show
GLOBAL _sub
SECTION .text
_sub:
call _test
mov rdi, 1 ; 第一引数に1を指定
mov rsi, 2 ; 第二引数に2を指定
call _add ; add関数の呼び出し
mov rdi, rax ; show関数の第一引数にaddの結果を代入
call _show ; show関数の呼び出し
ret
実行
>./main
Main
Hello
sum = 3
v = 3
この結果からわかるように,C言語の関数に引数を与えて呼び出し,さらに結果を受け取ってshow
に与えていることが分かる.
ABIの確認
C言語からアセンブリ言語,アセンブリ言語からC言語の関数を呼び出すときには,ABI(Application Binary Interface)というプロトコルが存在する.詳しくはこちら.
これによると,X86_64では以下のような決まりになっている.これによると,引数が6つまではレジスタ経由で渡されることが分かる.
引数の個数が6つを超える場合には,スタックを経由(引数の後ろから積む)ことになっている.
また,ここにあるように,返り値のやり取りにはrax
が利用される.
The number of the syscall has to be passed in register %rax.
rdi - used to pass 1st argument to functions
rsi - used to pass 2nd argument to functions
rdx - used to pass 3rd argument to functions
rcx - used to pass 4th argument to functions
r8 - used to pass 5th argument to functions
r9 - used to pass 6th argument to functions
A system-call is done via the syscall instruction. The kernel destroys registers rcx and r11.
これを確認すべく,更に関数を追加して確認する
#include <stdio.h>
extern void sub();
extern void sub2();
void test() {
printf("Hello\n");
}
int add(int v1, int v2) {
int sum = v1 + v2;
printf("sum = %d\n", sum);
return v1 + v2;
}
void show(int i) {
printf("v = %d\n", i);
}
// 8つの引数を受け取る関数
void show2(int v1, int v2, int v3, int v4, int v5, int v6, int v7, int v8) {
printf("v1 = %d\n", v1);
printf("v2 = %d\n", v2);
printf("v3 = %d\n", v3);
printf("v4 = %d\n", v4);
printf("v5 = %d\n", v5);
printf("v6 = %d\n", v6);
printf("v7 = 0x%x\n", v7);
printf("v8 = 0x%x\n", v8);
}
int main(void) {
printf("Main\n");
sub();
printf("---------------\n");
sub2();
}
extern _test,_add,_show,_show2
GLOBAL _sub,_sub2
SECTION .text
_sub:
call _test
mov rdi, 1
mov rsi, 2
call _add
mov rdi, rax
call _show
ret
_sub2:
mov rdi, 1
mov rsi, 2
mov rdx, 3
mov rcx, 4
mov r8, 5
mov r9, 6
push 0x11111111
push 0x22222222
call _show2
pop rbx
pop rbx
ret
実行
>./main
Main
Hello
sum = 3
v = 3
---------------
v1 = 1
v2 = 2
v3 = 3
v4 = 4
v5 = 5
v6 = 6
v7 = 0x22222222
v8 = 0x11111111
狙ったとおりの出力を得ている("---"以下の部分).スタックには0x11111111
から積んでいるので,"v8"でその値を受け取っている.つまり,引数の後ろからスタックに積めば良いことも確認できる.
LLDB
ABIの確認を,LLDBを使ってやってみる.
LLDBの簡単な使い方
LLDBの使い方は,色々なところで紹介されているので割愛.ここでは,以下の手順でABIの確認をしてみる
- lldbの引数に実行プログラムを指定
- ブレイクポイントの挿入
- run
-
n
(ステップオーバ)またはs
(ステップオーバし,可能ならステップインする)でステップ実行 -
register read
でレジスタの値の確認 -
memory read
でメモリの確認
lldbのコマンドを確認するため,アセンブリ言語で書かれたhelloworldプログラムをlldbで実行してみる
(1). 以下のコマンドでhelloworldをlldbで実行
> lldb helloworld
(lldb) target create "helloworld"
Current executable set to 'helloworld' (x86_64).
(lldb)
(2). ブレイクポイントの挿入
(lldb) breakpoint set -name start
Breakpoint 1: where = helloworld`start, address = 0x0000000000001fd9
ここでは,start
関数(シンボル)にブレイクポイントを指定する.なお,ブレイクポイントに指定できるのは,nm
で確認できるシンボルのため,nm
で確認できる名前を指定する.
(3). 実行
Process 11354 launched: 'somewhere/helloworld' (x86_64)
Process 11354 stopped
* thread #1, stop reason = breakpoint 1.1
frame #0: 0x0000000000001fd9 helloworld`start
helloworld`start:
-> 0x1fd9 <+0>: movl $0x2000004, %eax ; imm = 0x2000004
0x1fde <+5>: movl $0x1, %edi
0x1fe3 <+10>: movabsq $0x2000, %rsi ; imm = 0x2000
0x1fed <+20>: movl $0xd, %edx
Target 0: (helloworld) stopped.
プログラムが実行され,start
の位置(->があるところ)で止まっている.
(4). ステップ実行
(lldb) s
Process 11354 stopped
* thread #1, stop reason = instruction step into
frame #0: 0x0000000000001fde helloworld`start + 5
helloworld`start:
-> 0x1fde <+5>: movl $0x1, %edi
0x1fe3 <+10>: movabsq $0x2000, %rsi ; imm = 0x2000
0x1fed <+20>: movl $0xd, %edx
0x1ff2 <+25>: syscall
Target 0: (helloworld) stopped.
s
でステップ実行.矢印が一つづれで,次の行を実行しようとしているのが分かる.
(5). レジスタの確認
(lldb) register read
General Purpose Registers:
rax = 0x0000000002000004
rbx = 0x0000000000000000
rcx = 0x0000000000000000
rdx = 0x00007fff5fbfef20
rdi = 0x0000000000000000
rsi = 0x0000000000000001
rbp = 0x0000000000000000
rsp = 0x00007fff5fbfef30
r8 = 0x00000000000002c8
r9 = 0x0000000000000008
r10 = 0x33aec6a70e7f00a2
r11 = 0x0000000000000246
r12 = 0x0000000000000000
r13 = 0x0000000000000000
r14 = 0x0000000000000000
r15 = 0x0000000000000000
rip = 0x0000000000001fde helloworld`start + 5
rflags = 0x0000000000000216
cs = 0x000000000000002b
fs = 0x0000000000000000
gs = 0x0000000000000000
レジスタの値を見る.1行目でrax
に0x2000004
を代入しているので,rax
の値がこの値になっていることが確認できる.
(6). メモリの確認
スタックの値を確認してみる
(lldb) memory read --size 1 --format x --count 4 0x00007fff5fbfef30
0x7fff5fbfef30: 0x01 0x00 0x00 0x00
これは,スタックのアドレス(0x00007fff5fbfef30)のメモリを,1バイトを単位として4つぶん出力した例となる.もし,8バイト単位で4つぶん出力するなら
(lldb) memory read --size 8 --format x --count 4 0x00007fff5fbfef30
0x7fff5fbfef30: 0x0000000000000001 0x00007fff5fbff108
0x7fff5fbfef40: 0x0000000000000000 0x00007fff5fbff150
(lldb)
となる.8バイト単位としているので,エンディアンの関係で最初の値が0x01
となっていることが確認できる.
ABIの確認
ここで確認したいのは以下の2点
- NASMで
push
したとき,何バイトスタックに積まれるのか - 第7引数以降スタックを経由して引数を受け渡すけど,関数呼び出し後の後処理はプログラマがやる必要があるか
1は,AT&T記法では,pushlとかpushpという具合に,値の大きさ(4バイトとか8バイト)を明示する命令が用意されているが,NASMはオプションで指定するみたいです.そこで,何も考えずにpush
した場合,どうなるかを確認したい.
2は,引数のためにpushするけど,呼び出した関数から戻ってきたときにはスタックに積んだ値を捨てる必要があるよねってことを,改めて確認したい
関数呼び出しのアセンブリ言語は,
_sub2:
mov rdi, 1
mov rsi, 2
mov rdx, 3
mov rcx, 4
mov r8, 5
mov r9, 6
push 0x11111111
push 0x22222222
call _show2
pop rbx
pop rbx
ret
これなので,ここにブレイクポイントを仕掛けて値を確認していく
>lldb main
(lldb) target create "main"
Current executable set to 'main' (x86_64).
(lldb) breakpoint set -name sub2
Breakpoint 1: where = main`sub2, address = 0x0000000100000edd
(lldb) r
Process 11505 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
frame #0: 0x0000000100000edd main`sub2
main`sub2:
-> 0x100000edd <+0>: movl $0x1, %edi
0x100000ee2 <+5>: movl $0x2, %esi
0x100000ee7 <+10>: movl $0x3, %edx
0x100000eec <+15>: movl $0x4, %ecx
Target 0: (main) stopped.
このまま,pushまで進める
-> 0x100000efd <+32>: pushq $0x11111111 ; imm = 0x11111111
0x100000f02 <+37>: pushq $0x22222222 ; imm = 0x22222222
0x100000f07 <+42>: callq 0x100000da0 ; show2
0x100000f0c <+47>: popq %rbx
Target 0: (main) stopped.
(lldb) register read
General Purpose Registers:
rax = 0x0000000000000000
rbx = 0x0000000000000000
rcx = 0x0000000000000004
rdx = 0x0000000000000003
rdi = 0x0000000000000001
rsi = 0x0000000000000002
rbp = 0x00007fff5fbfef10
rsp = 0x00007fff5fbfeef8
r8 = 0x0000000000000005
r9 = 0x0000000000000006
r10 = 0xffffffffffffffff
r11 = 0x0000000000012068
r12 = 0x0000000000000000
r13 = 0x0000000000000000
r14 = 0x0000000000000000
r15 = 0x0000000000000000
rip = 0x0000000100000efd main`sub2 + 32
rflags = 0x0000000000000206
cs = 0x000000000000002b
fs = 0x0000000000000000
gs = 0x0000000000000000
rsp
(スタックポインタ)の値は0x00007fff5fbfeef8
となっている.さらに進める
-> 0x100000f02 <+37>: pushq $0x22222222 ; imm = 0x22222222
0x100000f07 <+42>: callq 0x100000da0 ; show2
0x100000f0c <+47>: popq %rbx
0x100000f0d <+48>: popq %rbx
Target 0: (main) stopped.
(lldb) register read
General Purpose Registers:
rax = 0x0000000000000000
rbx = 0x0000000000000000
rcx = 0x0000000000000004
rdx = 0x0000000000000003
rdi = 0x0000000000000001
rsi = 0x0000000000000002
rbp = 0x00007fff5fbfef10
rsp = 0x00007fff5fbfeef0
r8 = 0x0000000000000005
r9 = 0x0000000000000006
r10 = 0xffffffffffffffff
r11 = 0x0000000000012068
r12 = 0x0000000000000000
r13 = 0x0000000000000000
r14 = 0x0000000000000000
r15 = 0x0000000000000000
rip = 0x0000000100000f02 main`sub2 + 37
rflags = 0x0000000000000206
cs = 0x000000000000002b
fs = 0x0000000000000000
gs = 0x0000000000000000
rsp
の値は,0x00007fff5fbfeef0
となっており,直前の値と比較すると8バイト少ない.ということは,8バイトデータを積んだことが分かる.さらにもう1ステップ進め,メモリを確認してみる
(lldb) n
Process 11505 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = instruction step over
frame #0: 0x0000000100000f07 main`sub2 + 42
main`sub2:
-> 0x100000f07 <+42>: callq 0x100000da0 ; show2
0x100000f0c <+47>: popq %rbx
0x100000f0d <+48>: popq %rbx
0x100000f0e <+49>: retq
Target 0: (main) stopped.
(lldb) register read
General Purpose Registers:
rax = 0x0000000000000000
rbx = 0x0000000000000000
rcx = 0x0000000000000004
rdx = 0x0000000000000003
rdi = 0x0000000000000001
rsi = 0x0000000000000002
rbp = 0x00007fff5fbfef10
rsp = 0x00007fff5fbfeee8
r8 = 0x0000000000000005
r9 = 0x0000000000000006
r10 = 0xffffffffffffffff
r11 = 0x0000000000012068
r12 = 0x0000000000000000
r13 = 0x0000000000000000
r14 = 0x0000000000000000
r15 = 0x0000000000000000
rip = 0x0000000100000f07 main`sub2 + 42
rflags = 0x0000000000000206
cs = 0x000000000000002b
fs = 0x0000000000000000
gs = 0x0000000000000000
rsp
はさらに8バイト減っている.ここで,rsp
が指すメモリを見てみる
(lldb) memory read --size 8 --format x --count 2 0x00007fff5fbfeee8
0x7fff5fbfeee8: 0x0000000022222222 0x0000000011111111
このように,積んだ値が保存されていることが確認できる.
更に1ステップ進める
(lldb) n
v1 = 1
v2 = 2
v3 = 3
v4 = 4
v5 = 5
v6 = 6
v7 = 0x22222222
v8 = 0x11111111
Process 11505 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = instruction step over
frame #0: 0x0000000100000f0c main`sub2 + 47
main`sub2:
-> 0x100000f0c <+47>: popq %rbx
0x100000f0d <+48>: popq %rbx
0x100000f0e <+49>: retq
0x100000f0f: nop
Target 0: (main) stopped.
(lldb) register read
General Purpose Registers:
rax = 0x0000000000000010
rbx = 0x0000000000000000
rcx = 0x00000c0000000d03
rdx = 0x0000000000012068
rdi = 0x00007fffe0aa1f78 __sFX + 248
rsi = 0x0000000000dcd200
rbp = 0x00007fff5fbfef10
rsp = 0x00007fff5fbfeee8
r8 = 0x0000000000000040
r9 = 0x00007fffe0aa1f70 __sFX + 240
r10 = 0xffffffffffffffff
r11 = 0x0000000000012068
r12 = 0x0000000000000000
r13 = 0x0000000000000000
r14 = 0x0000000000000000
r15 = 0x0000000000000000
rip = 0x0000000100000f0c main`sub2 + 47
rflags = 0x0000000000000206
cs = 0x000000000000002b
fs = 0x0000000000000000
gs = 0x0000000000000000
関数呼び出しから戻ってきた直後の状態で,rsp
を見ると,関数を呼び出す直前の値と等しい.ということは,プログラマがスタックから引数として利用した値を削除しないといけない,という結論となる.