LoginSignup
16
13

More than 5 years have passed since last update.

C言語とアセンブリ言語の相互呼び出し

Last updated at Posted at 2019-05-02

概要

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言語からアセンブリ言語で定義した関数を呼び出してみる.

main.c
#include <stdio.h>

extern int add(int v1, int v2); // アセンブリ言語で定義した関数

int main(void) {
    int r = add(1, 2);    
    printf("r = %d\n", r);
}
add.asm

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言語の関数を呼び出す,ということにする.

引数なし/返り値なし関数の実行

1.c
#include <stdio.h>

extern void sub();

void test() {
    printf("Hello\n");
}

int main(void) {
        printf("Main\n");
        sub();
}
sub.asm
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という関数呼び出しが実行できていることが分かる

引数あり/返り値あり関数の実行

先程の例に,関数を追加する.

1.c
#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();
}
sub.asm

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.

これを確認すべく,更に関数を追加して確認する

1.c
#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();
}
sub.asm

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の確認をしてみる

  1. lldbの引数に実行プログラムを指定
  2. ブレイクポイントの挿入
  3. run
  4. n(ステップオーバ)またはs(ステップオーバし,可能ならステップインする)でステップ実行
  5. register readでレジスタの値の確認
  6. 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行目でrax0x2000004を代入しているので,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点

  1. NASMでpushしたとき,何バイトスタックに積まれるのか
  2. 第7引数以降スタックを経由して引数を受け渡すけど,関数呼び出し後の後処理はプログラマがやる必要があるか

1は,AT&T記法では,pushlとかpushpという具合に,値の大きさ(4バイトとか8バイト)を明示する命令が用意されているが,NASMはオプションで指定するみたいです.そこで,何も考えずにpushした場合,どうなるかを確認したい.
2は,引数のためにpushするけど,呼び出した関数から戻ってきたときにはスタックに積んだ値を捨てる必要があるよねってことを,改めて確認したい

関数呼び出しのアセンブリ言語は,

_sub2(再掲)
_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を見ると,関数を呼び出す直前の値と等しい.ということは,プログラマがスタックから引数として利用した値を削除しないといけない,という結論となる.

16
13
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
16
13