首先我們先編寫匯編
extern choose ; int choose(int a, int b)
[section .data]; 數(shù)據(jù)在此
num1st dd 3
num2nd dd 4
[section .text] ; 代碼在此
global _start ; 我們必須導(dǎo)出_start這個入口,以便讓連接器識別
global myprint ; 導(dǎo)出這個函數(shù)為了讓 bar.c 使用
_start:
push dword [num2nd] ;
push dword [num1st] ;
call choose ; | choose(num1st, num2nd)
add esp, 8
mov ebx, 0
mov eax, 1 ; sys_exit
int 0x80 ; 系統(tǒng)調(diào)用
; void myprint(char * msg, int len)
myprint:
mov edx, [esp + 8] ; len
mov ecx, [esp + 4] ; msg
mov ebx, 1
mov eax, 4 ; sys_write
int 0x80 ; 系統(tǒng)調(diào)用
ret
上述代碼中需要說明的有三點:
- 由于在bar.c中用到函數(shù)mprint(),所以要用關(guān)鍵字global將其導(dǎo)出。
- 由于用到本文件外定義的函數(shù)choose(),所以要用關(guān)鍵字extern聲明。
- 不管是myprint()還是choose(),遵循的都是C調(diào)用約定(C Calling Convention),后面的參數(shù)先入棧,并由調(diào)用者(Caller)清理堆棧。
C語言代碼,其中包含函數(shù)myprint()的聲明和函數(shù)choose()的主體。
void myprint(char * msg, int len);
int choose(int a, int b)
{
if (a >= b) {
myprint("the 1st one\n", 13);
} else {
myprint("the 2nd one\n", 13);
}
return 0;
}
編譯鏈接和執(zhí)行的過程:
~$ ls
bar.c foo.asm
~$ nasm -f elf -o foo.o foo.asm
~$ gcc -c -o bar.o bar.c
~$ ld -s -o foobar foo.o bar.o
~$ ls
bar.c bar.o foo.asm foobar foo.o
~$ ./foobar
the 2nd one
如果當(dāng)我們執(zhí)行上面的操作到 ld -s foo.o bar.o -o foobar 會產(chǎn)生 ld: i386 architecture of input file `foo.o’ is incompatible with i386:x86-64 output錯誤 意思是nasm編譯產(chǎn)生的是32位的目標(biāo)代碼,gcc在64位平臺上默認(rèn)產(chǎn)生的是64位的目標(biāo)代碼,這兩者在鏈接的時候出錯,gcc在64位平臺上默認(rèn)以64位的方式鏈接。
方法一:
讓gcc產(chǎn)生32位的代碼,并在鏈接的時候以32位的方式進(jìn)行鏈接
在這種情況下只需要修改編譯和鏈接指令即可,具體如下:
32位的編譯鏈接指令
$ nasm -f elf foo.asm -o foo.o
$ gcc -m32 -c bar.c -o bar.o
$ ld -m elf_i386 -s foo.o bar.o -o foobar
$ ./foobar
the 2nd one
方法二:
讓nasm以64位的方式編譯產(chǎn)生目標(biāo)代碼,并讓gcc的鏈接器以默認(rèn)的方式鏈接
但是第二種方法并不是僅僅更改nasm的編譯方式那么簡單,因為64位平臺跟32位平臺有很大的不同,包括參數(shù)的傳遞,指令集等。所以如果怕麻煩的話完全可以使用第一種方法,讓gcc產(chǎn)生32位的目標(biāo)代碼,因為32位的代碼可以運行在64位的平臺上,這應(yīng)該就是所謂的向上兼容。不過64位將來應(yīng)該會是主流,所以研究一下還是很有必要的。
首先對gcc產(chǎn)生的32位于64位的匯編語言進(jìn)行對比:
32位
gcc -m32 -s -o bar.o bar.c
/************** 32 位的匯編語言 *************/ choose: .LFB0: .cfi_startproc
pushl %ebp
.cfi_def_cfa_offset 8
.cfi_offset 5, -8
movl %esp, %ebp
.cfi_def_cfa_register 5
subl $24, %esp
movl 8(%ebp), %eax
cmpl 12(%ebp), %eax
jl .L2
movl $13, 4(%esp)
movl $.LC0, (%esp)
call myprint
jmp .L3
.L2:
movl $13, 4(%esp)
movl $.LC1, (%esp)
call myprint
movl 8(%ebp), %eax
cmpl 12(%ebp), %eax
jl .L2
movl $13, 4(%esp)
movl $.LC0, (%esp)
上面只取了我們感興趣的地方:ebp指向的是剛進(jìn)入choose函數(shù)的堆棧棧頂指針,此時只想的是剛?cè)霔5膃bp的值,ebp+4指向的函數(shù)調(diào)用入棧的ip地址(這里應(yīng)該是段內(nèi)調(diào)用,具體原因不太清楚,因為兩個文件之間調(diào)用函數(shù)屬于段內(nèi)還是段外,我真的不清楚,如果你知道,可以告訴我),ebp+8指向的是調(diào)用者壓棧的第二個參數(shù),也是從左邊數(shù)第一個參數(shù),ebp+12 是調(diào)用者壓棧的第一個參數(shù),也就是從左邊數(shù)第二個參數(shù)。這樣我們知道了c語言的參數(shù)傳遞機制,就能編寫相應(yīng)的匯編程序調(diào)用C語言了,而C 語言調(diào)用匯編函數(shù)則以此類推,先將第二個參數(shù)壓棧,再將第一個參數(shù)壓棧。不再贅述。
(例: void fun(int a, int b) 函數(shù)在調(diào)用fun時首先將參數(shù)b 壓棧,然后將參數(shù) a壓棧,這樣fun 函數(shù)在取參數(shù)的時候就能先取a了,然后再取b,因為堆棧是先入后出。如果這樣你還不明白,建議你看一下趙迥老師的linux 0.11內(nèi)核完全剖析的第三章。)
(注:rax:64位,eax:32位 ax:16位
movl: 移動32位,movq:移動64位,movd:移動16位,movb:移動8位
其他帶標(biāo)志的指令類似。)
64位
gcc -c -o bar.o bar.c (64位的操作系統(tǒng)默認(rèn))
/************** 64位的匯編程序 ***********/
choose:
.LFB0:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
subq $16, %rsp
movl %edi, -4(%rbp)
movl %esi, -8(%rbp)
movl -4(%rbp), %eax
cmpl -8(%rbp), %eax
jl .L2
movl $13, %esi
movl $.LC0, %edi
call myprint
jmp .L3
.L2:
movl $13, %esi
movl $.LC1, %edi
call myprint
movl %edi, -4(%rbp)
movl %esi, -8(%rbp)
movl -4(%rbp), %eax
cmpl -8(%rbp), %eax
jl .L2
movl $13, %esi
movl $.LC0, %edi
注意64位下的參數(shù)傳遞有了改變,而且寄存器也有了改變,不過我們既然使用了nasm匯編,對于64位寄存器的改變暫時不必操心,只需要先關(guān)心參數(shù)傳遞的格式。
可以看出參數(shù)傳遞不是用壓棧的方式傳遞了,而是使用的寄存器來傳遞給被調(diào)用者,再由被調(diào)用者將其壓棧使用。上述代碼顯示先將第一個參數(shù)給edi,然后由被調(diào)用者壓入-4(%rbp),然后再將第二個參數(shù)給esi,由被調(diào)用者要入-8(%rbp),這一點倒是和32位下參數(shù)的入棧方式一致。
至于用寄存器傳遞函數(shù)參數(shù)取代用堆棧傳遞函數(shù)參數(shù)的原因,個人感覺是函數(shù)的調(diào)用者不用再操心入棧和釋放棧了,完全由被調(diào)用者操心,至少我在函數(shù)的調(diào)用者里面經(jīng)常是記得給函數(shù)參數(shù)入棧,但是函數(shù)調(diào)用完成后卻忘記了把?;謴?fù)。
這樣我們就能根據(jù)上述規(guī)則來修改我們的foo.s,使其能夠與64位的gcc產(chǎn)生的目標(biāo)代碼鏈接在一起。
64位模式下的foo.asm
extern choose ; int choose(int a, int b)
num1st dd 3
num2nd dd 4
global _start ; 我們必須導(dǎo)出_start這個入口,以便讓連接器識別
global myprint ; 導(dǎo)出這個函數(shù)為了讓 bar.c 使用
_start:
mov edi, [num2nd] ;
mov esi, [num1st] ;
call choose ; | choose(num1st, num2nd)
mov ebx, 0
mov eax, 1 ; sys_exit
int 0x80 ; 系統(tǒng)調(diào)用
; void myprint(char * msg, int len)
myprint:
;mov edx, [esp + 8] ; len
;mov ecx, [esp + 4] ; msg
mov edx, esi
mov ecx, edi
mov ebx, 1
mov eax, 4 ; sys_write
int 0x80 ; 系統(tǒng)調(diào)用
ret