如何在64位的linux系統上使用匯編和C語言混合編程


最近在看於淵的一個操作系統的實現,在第五章的時候匯編和C 同時使用時碰到了問題:代碼如下

foo.s
 1 extern choose 
2
3 ;;;;;the data area
4 num1st dd 3
5 num2nd dd 4
6
7 global _start
8 global myprint
9
10
11 _start:
12
13 push dword [num1st]
14 push dword [num2nd]
15
16 call choose
17 add esp,8
18
19 mov ebx,0
20 mov eax,1
21 int 0x80
22 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
23 ;;;; function protype :void myprint(char *msg, int len)
24 ;;;; display the message
25 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
26
27 myprint:
28
29 mov ecx,[esp+4]
30 mov edx,[esp+8]
31 ;mov edx,esi
32 ;mov ecx,edi
33
34 mov ebx,1
35 mov eax,4
36 int 0x80
37 ret
38

 

bar.c
 1 /************  bar.c *****************/
2
3 void myprint(char *msg,int len);
4
5 int choose(int a,int b)
6 {
7 if (a>=b) {
8 myprint("the 1st one\n",13);
9 }
10
11 else {
12 myprint("the 2st one\n",13);
13 }
14
15 return 0;
16 }


編譯和鏈接的時候使用的指令:(AMD處理器,64位操作系統)

 

編譯鏈接指令
1 nasm -f elf foo.s -o foo.o
2 gcc -c bar.c -o bar.o
3 ld -s -o foobar bar.o foo.o


 

 

匯編語言用nasm編寫並用nasm編譯器編譯,而C語言用的是gcc編譯,這些都沒有問題,但是在鏈接的時候出錯了,提示如下:

 

ld: i386 architecture of input file `foo.o' is incompatible with i386:x86-64 output

 

 

google了一下,意思就是nasm 編譯產生的是32位的目標代碼,gcc 64位平台上默認產生的是64位的目標代碼,這兩者在鏈接的時候出錯,gcc64位平台上默認以64位的方式鏈接。

 

 

這樣在解決的時候就會有兩種解決方案:

 

<1> gcc 產生32位的代碼,並在鏈接的時候以32位的方式進行鏈接

 

在這種情況下只需要修改編譯和鏈接指令即可,具體如下:

 

32位的編譯鏈接指令
1 nasm -f elf foo.s  -o  foo.o
2 gcc -m32 -c bar.c -o bar.o
3 ld -m elf_i386 -s -o foobar foo.o bar.o

 

 

具體的-m32 -m elf_i386 請自行查閱gcc man gcc

 

參考地址:http://bbs.chinaunix.net/thread-2018497-1-1.html

如果你是高版本的gcc(可能是由於更新內核造成的),可能簡單的使用-m32 的時候會提示以下錯誤(使用別人的歷程,自己未曾遇到):

 

 

> In file included from /usr/include/stdio.h:28:0,
> from test.c:1:
> /usr/include/features.h:323:26: fatal error: bits/predefs.h: No such file or directory
> compilation terminated.

 

 

這應該是缺少構建32 位可執行程序缺少的包,使用以下指令安裝:
sudo apt-get install libc6-dev-i386
此時應該就沒有什么問題了。

參考地址:http://aaronbonner.tumblr.com/post/14969163463/cross-compiling-to-32bit-with-gcc

<2> nasm64位的方式編譯產生目標代碼,並讓gcc的連接器以默認的方式鏈接

但是第二種方法並不是僅僅更改nasm的編譯方式那么簡單,因為64位平台跟32位平台有很大的不同,包括參數的傳遞,指令集等。所以如果怕麻煩的話完全可以使用第一種方法,讓gcc產生32位的目標代碼,因為32位的代碼可以運行在64位的平台上,這應該就是所謂的向上兼容。不過64位將來應該會是主流,所以研究一下還是很有必要的。

首先對gcc 產生的32位與64位的匯編語言進行對比:

32位
 1 gcc -m32 -S -o bar.s -c bar.c
2
3 /************** 32 位的匯編語言 *************/
4 choose:
5 .LFB0:
6 .cfi_startproc
7 pushl %ebp
8 .cfi_def_cfa_offset 8
9 .cfi_offset 5, -8
10 movl %esp, %ebp
11 .cfi_def_cfa_register 5
12 subl $24, %esp
13 movl 8(%ebp), %eax
14 cmpl 12(%ebp), %eax
15 jl .L2
16 movl $13, 4(%esp)
17 movl $.LC0, (%esp)
18 call myprint
19 jmp .L3
20 .L2:
21 movl $13, 4(%esp)
22 movl $.LC1, (%esp)
23 call myprint

 

 

movl 8(%ebp), %eax

 

cmpl 12(%ebp), %eax

 

jl .L2

 

movl $13, 4(%esp)

 

movl $.LC0, (%esp)


上面只取了我們感興趣的地方:ebp指向的是剛進入choose函數的堆棧棧頂指針,此時只想的是剛入棧的ebp的值,ebp+4指向的函數調用入棧的ip地址(這里應該是段內調用,具體原因不太清楚,因為兩個文件之間調用函數屬於段內還是段外,我真的不清楚,如果你知道,可以告訴我),ebp+8指向的是調用者壓棧的第二個參數,也是從左邊數第一個參數,ebp+12 是調用者壓棧的第一個參數,也就是從左邊數第二個參數。這樣我們知道了c語言的參數傳遞機制,就能編寫相應的匯編程序調用C語言了,而C 語言調用匯編函數則以此類推,先將第二個參數壓棧,再將第一個參數壓棧。不再贅述。

例: void fun(int a, int b) 函數在調用fun時首先將參數b 壓棧,然后將參數 a壓棧,這樣fun 函數在取參數的時候就能先取a了,然后再取b,因為堆棧是先入后出。如果這樣你還不明白,建議你看一下趙迥老師的linux 0.11內核完全剖析的第三章。

(

注:rax:64位,eax32ax16

movl: 移動32位,movq:移動64位,movd:移動16位,movb:移動8

其他帶標志的指令類似。

)

64位
 1 gcc -S -o bar.s -c bar.c (64位的操作系統默認)
2 /************** 64位的匯編程序 ***********/
3
4
5 choose:
6 .LFB0:
7 .cfi_startproc
8 pushq %rbp
9 .cfi_def_cfa_offset 16
10 .cfi_offset 6, -16
11 movq %rsp, %rbp
12 .cfi_def_cfa_register 6
13 subq $16, %rsp
14 movl %edi, -4(%rbp)
15 movl %esi, -8(%rbp)
16 movl -4(%rbp), %eax
17 cmpl -8(%rbp), %eax
18 jl .L2
19 movl $13, %esi
20 movl $.LC0, %edi
21 call myprint
22 jmp .L3
23 .L2:
24 movl $13, %esi
25 movl $.LC1, %edi
26 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位下的參數傳遞有了改變,而且寄存器也有了改變,不過我們既然使用了nasm匯編,對於64位寄存器的改變暫時不必操心,只需要先關心參數傳遞的格式。

可以看出參數傳遞不是用壓棧的方式傳遞了,而是使用的寄存器來傳遞給被調用者,再由被調用者將其壓棧使用。上述代碼顯示先將第一個參數給edi,然后由被調用者壓入-4%rbp),然后再將第二個參數給esi,由被調用者要入-8%rbp),這一點倒是和32位下參數的入棧方式一致。

至於用寄存器傳遞函數參數取代用堆棧傳遞函數參數的原因,個人感覺是函數的調用者不用再操心入棧和釋放棧了,完全由被調用者操心,至少我在函數的調用者里面經常是記得給函數參數入棧,但是函數調用完成后卻忘記了把棧恢復。

這樣我們就能根據上述規則來修改我們的foo.s,使其能夠與64位的gcc產生的目標代碼鏈接在一起。

64位模式下的foo.s
 1 /**************foo.s**************/
2 extern choose
3
4 ;;;;;the data area
5 num1st dd 3
6 num2nd dd 4
7
8 global _start
9 global myprint
10
11
12 _start:
13
14
15 mov edi,[num1st]
16 mov esi,[num2nd]
17 call choose
18
19 mov ebx,0
20 mov eax,1
21 int 0x80
22 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
23 ;;;; function protype :void myprint(char *msg, int len)
24 ;;;; display the message
25
26 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
27
28 myprint:
29
30 ;mov ecx,[esp+4]
31 ;mov edx,[esp+8]
32 mov ecx,edi
33 mov edx,esi
34
35 mov ebx,1
36 mov eax,4
37 int 0x80
38 ret



至於用寄存器傳遞函數參數的規則見以下參考資料:

==========================================

版權為 win_hate 所有轉載請保留作者名字

我這段時間要把以前的一個 x86_32 的 linux 程序移植到 x86_64(AMD) 的 linux 環境里由於寫的是數學算 法, 64 與 32 位有很大不同代碼實際上要重寫看了點資料后覺得 AMD64 的擴展於以前 16 到 32 位的擴展很類 似, e**, 擴展為 r**, 此外還多了8個通用寄存器 r8~r15.指令格式與32位的極為相似我覺得比較容易所以沒再仔細看就開 始動手寫了.

我的程序由若干個匯編模塊於與若干個c模塊構成很多c模塊要調用匯編模塊作為試驗我先寫了個簡單的匯編函數然后用c來調用結果 算出來的值始終是錯誤的這令我很惱火因為函數很簡單沒有多少出錯的余地后來我把程序反匯編出來錯誤馬上浮現出來了函數的參數居然 是通過寄存器來傳遞的我憑以前的經驗從堆棧里取參數算出的結果當然不對了我以前不是沒碰到過用寄存器傳遞參數的情況但所在的環境都不 是 pc. 在 x86_32/linux 即使用 -O3 優化選項, gcc 仍通過棧來傳遞參數的.

所以我們現在知道在 x86_64/linux/gcc3.2 即使不打開優化選項函數的參數也會通過寄存器來傳遞這肯定是闊了的表現(通用寄存器多了).

我試驗了多個參數的情況,發現一般規則為, 當參數少於7個時, 參數從左到右放入寄存器: rdi, rsi, rdx, rcx, r8, r9。當參數為 個以上時, 前 個與前面一樣, 但后面的依次從 "右向左放入棧中。

例如:
CODE

(1) 參數個數少於7:
f (a, b, c, d, e, f);
a->%rdi, b->%rsi, c->%rdx, d->%rcx, e->%r8, f->%r9

g (a, b)
a->%rdi, b->%rsi

有趣的是實際上將參數放入寄存器的語句是從右到左處理參數表的這點與32位的時候一致.

CODE

2) 參數個數大於 個的時候
H(a, b, c, d, e, f, g);
a->%rdi, b->%rsi, c->%rdx, d->%rcx, e->%rax
g->8(%esp)
f->(%esp)
call H


易失寄存器:
%rax, %rcx, %rdx, %rsi, %rdi, %r8, %r9 為易失寄存器, 被調用者不必恢復它們的值。
顯然,這里出現的寄存器大多用於參數傳遞了, 值被改掉也無妨。而 %rax, %rdx 常用於
數值計算, %rcx 常用於循環計數,它們的值是經常改變的。其它的寄存器為非易失的,也
就是 rbp, rbx, rsp, r10~r15 的值如果在匯編模塊中被改變了,在退出該模塊時,必須將
其恢復。

教訓:
用匯編寫模塊然后與 整合一定要搞清楚編譯器的行為特別是參數傳遞的方式此外我現在比較擔心的一點是將來如果要把程序移植 到 WIN/VC 環境怎么辦以前我用cygwingcc來處理匯編模塊vc來處理c模塊只需要很少改動現在的問題是如果VC用 不同的參數傳遞方式那我不就麻煩了?

補充:
前面的參數 a, b, c, d 都是整數長整數或指針也就是說能放到寄存器里頭的如果你要傳遞一個很大的結構我估計編譯器也只能通過棧來傳遞了.

環境為 AMD Athlon64, Mandrak linux 9.2, GCC3.3.1

==============================

 

 

參考地址:http://hi.baidu.com/bluebanboom/blog/item/381959af65ff36fbfaed5068.html

















免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM