関数呼び出し規約
C言語にこんなコードあります
int subtract (int a,int b) {
return a-b;
}
このように呼び出すことができます
int sub = subtract(3,2)
このようにして、C 言語で最も一般的な関数呼び出し方法である関数呼び出しが完了しました。しかし、コンピューターが、渡した 2 つのパラメーター 3 と 2 がどこにあるのかをどのようにして認識しているか考えたことがありますか?
レジスタに保存できますが、レジスタの数には制限があります。レジスタをメモリ スタックに置くこともできます。呼び出すときに、スタックのアドレスを渡します。スタック メモリに置くことにはいくつかの利点があります。
- 各プロセスには独自のスタックがあり、これは各メモリの専用メモリ空間です。
- パラメータを保存するためにメモリ アドレスを維持する手間は必要なく、アドレスの変更を維持するためのスタック メカニズムがすでに存在します。
メモリ スタックに保存されると、これら 2 つの受信値を見つける方法という最初の問題が解決されます。次に 2 番目の質問は、メモリのこの部分を再利用する責任があるのは誰ですか。パラメータが多い場合、呼び出し関数はどのような順序でパラメータを渡すのでしょうか?
これら 2 つの問題は簡単に解決できますか? 呼び出し元がスタックを前の位置にリサイクルする責任があるのか、呼び出し先がスタックを前の位置にリサイクルする責任があるのか、パラメータが左から右にプッシュされるか右から左にプッシュされるか、私は自分で規定しました。
実は高級言語もこのように規定されており、高級言語ごとに若干の違いがありますが、これらの呼び出しプロトコルについては以下に詳しく列挙します。
実際、ここで話しているのは C 言語とアセンブリ言語の混合プログラミングなので、C 言語のcdecl
呼び出し。
cdecl 呼び出し規則は C 言語に由来するため、C 呼び出し規則とも呼ばれ、C 言語のデフォルトの呼び出し規則です。cdecl の呼び出し規約の意味は、
- 呼び出し元は、すべてのパラメーターをスタックに右から左にプッシュします。
- 呼び出し元は、パラメーターによって占有されているスタック領域をクリーンアップします。
前回の抜粋をコンパイルして見てみましょう
発信者
push 2
push 3
call subtract
add esp,8
着信者
push ebp
mov ebp,esp
mov eax,[ebp+0x8]
add eax,[ebp+0xc]
mov esp,ebp
pop ebp
ret
呼び出し元はパラメータをスタックに右から左にプッシュする責任があり、呼び出し元はスタック トップ ポインターの最初の 8 バイトをポップする責任があります。
C ライブラリ関数とシステム コール
まず Linux システム コールを学習し、システム コールを使用してデモ モデルを簡素化しましょう。
システム コールは、Linux カーネルによって提供される一連のサブルーチンであり、Windows のダイナミック リンク ライブラリ DLL ファイルと同じ機能を持ち、ユーザー モードでは実装できない、または実装が容易ではない一連の機能を実装するために使用されます。システムにはハードウェアへのアクセス権限があるが、ユーザプログラムには権限がないため、ユーザプログラムはオペレーティングシステムに助けを求めることしかできないため、システムコールはユーザプログラムが使用するもの、つまりシステムコールです。
このシステム コールは、システム コールのエントリが 1 つだけ、つまり割り込み番号 0x80 だけであることと、特定のサブ関数がレジスタ eax で個別に指定されることを除いて、BIOS 割り込みコールと非常に似ています。Linux システムではシステムコールは/usr/include/asm/unistd.h
ファイルで定義されており、asm ディレクトリに 2 つのバージョンが用意されています。ファイル名は unistd_32.h と unistd_64.h です。当然、1 つは 32 ビットに対応し、もう 1 つは 64 ビットに対応します。 32 ビット バージョンでは am 435 システム コールが見つかりました。最初の 10 個を見てみましょう
#define __NR_restart_syscall 0
#define __NR_exit 1
#define __NR_fork 2
#define __NR_read 3
#define __NR_write 4
#define __NR_open 5
#define __NR_close 6
#define __NR_waitpid 7
#define __NR_creat 8
#define __NR_link 9
特定のシステムコールの使用法がわからない場合は、man コマンドを使用して調べることができます。
man 2 #系统调用名
man 2 write
2種類のシステムコール
「システムコール」を呼び出すには2つの方法があります
- システムコール命令はCライブラリ関数としてパッケージ化されており、ライブラリ関数を通じてシステムコールを行うため操作が容易です。
- ライブラリ関数に依存せず、アセンブリ命令 int を通じてオペレーティング システムと直接通信します。
ライブラリ関数システムコール
write の機能は、buf が指すバッファ内の count バイトを fd が指すファイル記述子に書き込み、実行が成功した後に書き込まれたバイト数を返し、失敗した場合は -1 を返すことです。やってみよう
#include <unistd.h>
int main() {
write(1,"Hello C\n",8);
return 0;
}
コンパイルして実行すると、コマンド ターミナルがこの文字列を出力することがわかりました。これは、Linux ではファイル No. 1 が標準出力stdout
である。
直接システムコール
システムコールの入力パラメータがどのように渡されるかを見ていきます。
入力パラメータが 5 以下の場合、Linux はレジスタを使用してパラメータを渡します。パラメータの数が 5 個を超える場合は、パラメータを連続したメモリ領域に順番に配置し、その領域の先頭アドレスを ebx レジスタに格納します。ここでは、パラメーターの数が 5 以下の場合のみを示します。
(1) ebx には最初のパラメータが格納されます。
(2) ecx には第 2 パラメータが格納されます。
(3) edx には 3 番目のパラメータが格納されます。
(4) esi には 4 番目のパラメータが格納されます。
(5) edi には第 5 パラメータが格納されます。
練習しましょう
section .data
str_c_lib: db "C library says: hello c!",0xa;
str_c_lib_len equ $-str_c_lib
str_syscall: db "syscall says: hello c!",0xa;
str_syscall_len equ $-str_syscall
section .text
global _start
_start:
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;模拟C语言的调用形式
push str_c_lib_len
push str_c_lib
push 1
call simu_write
add esp,12
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;直接进行系统调用
mov eax,4
mov ebx,1
mov ecx,str_syscall
mov edx,str_syscall_len
int 0x80
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;退出程序
mov eax,1
int 0x80
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;自己定义的函数模拟c语言调用形式
simu_write:
push ebp
mov ebp,esp
mov eax,4
mov ebx,[ebp+8]
mov ecx,[ebp+12]
mov edx,[ebp+16]
int 0x80
pop ebp
ret
コンパイル
nasm -f elf32 -o wr.o wr.s
リンク
ld -m elf_i386 -o wr.bin wr.o
ここでリンクのファイル形式-m
を指定する
./wr.bin
実行すると次の文字列が出力されます
C library says: hello c!
syscall says: hello c!
cとアセンブリ間の相互呼び出し
ここでは 2 つの例を用意しました
C言語
extern void asm_print(char*,int);
void c_print(char *str) {
int len = 0;
while(str[len++]);
asm_print(str,len);
}
編集
section .data
str:db "asm_print says hello c",0xa,0
; 0xa指出字符串是ascii编码,0是手动加上的\n
str_len equ $-str
section .text
extern c_print
global _start
_start:
push str
call c_print
add esp,4
mov eax,1 ; 子功能号1是exit
int 0x80 ; 发起中断通知Linux完成请求的功能
global asm_print
asm_print:
push ebp
mov ebp,esp
mov eax,4
mov ebx,1
mov,ecx,[ebp+8]
mov edx,[ebp+12]
int 0x80
pop ebp
ret
c の関数 c_print はアセンブリ コードによって呼び出され、c_print はアセンブリ コードで asm_print を呼び出すことによって実装されます。C 言語の最初の行は、外部関数 asm_print を宣言することで、この関数が現在のファイルに定義されていないことをコンパイラに通知します。この関数がアセンブリで定義されていることはわかっていますが、コンパイラはそれを認識していません。リンクフェーズでしか使用できないため、この機能を再配置し、アドレスを配置してください。
リンク時に C 言語の標準ライブラリをリンクしたくないため、ここでは文字列の長さを手動でカウントしています。そのため、アセンブリで文字列の末尾に 0 を手動で追加しています。
アセンブリ ファイル内で外部関数 (C コードである必要はない) を参照するには、extern を使用して必要な関数名を宣言する必要があります。
global キーワードは、アセンブリ言語でシンボル名をエクスポートするために使用されます。これは、_start と言ったときに聞いたことがあります。Global は、プログラム内のすべてのファイルに表示されるグローバル属性としてシンボルをエクスポートするため、他の外部ファイルも参照できます。シンボルが関数であるか変数であるかに関係なく、グローバルによってエクスポートされるシンボル。
これら 2 つのコードをコンパイルしてリンクできます。
C ファイルをコンパイルします
gcc -c -m32 -o C_with_S.o C_with_S.c
アセンブリファイルをコンパイルする
nasm -f elf32 -o S_with_C.o S_with_C.s
2つのファイルをリンクします
ld C_with_S.o S_with_C.o -o CS.bin -m elf_i386
埋め込む
./ CS.bin
出力を取得する
lovetzp@ubuntu:~/temp$ ./CS.bin
asm_print says hello c
c -m32 -o C_with_S.o C_with_S.c
アセンブリファイルをコンパイルする
nasm -f elf32 -o S_with_C.o S_with_C.s
2つのファイルをリンクします
ld C_with_S.o S_with_C.o -o CS.bin -m elf_i386
埋め込む
./ CS.bin
出力を取得する
lovetzp@ubuntu:~/temp$ ./CS.bin
asm_print says hello c