如何在64位的linux系统上使⽤汇编和C语⾔混合编程最近在看于渊的⼀个操作系统的实现,在第五章的时候汇编和C 同时使⽤时碰到了问题:代码如下
foo.s
1extern choose
2
3 ;;;;;the data area
4 num1st            dd        3
5 num2nd        dd        4
6
7global        _start
8global        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
21int0x80
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
36int0x80
37    ret
38
bar.c
1/************  bar.c *****************/
2
3void myprint(char *msg,int len);
4
5int choose(int a,int b)
6 {
7if (a>=b) {
8        myprint("the 1st one\n",13);
9    }
10
11else {
12        myprint("the 2st one\n",13);
13    }
14
15return0;
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位的⽬标代码,这两者在链接的时候出错,gcc在64位平台上默认以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)
如果你是⾼版本的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
此时应该就没有什么问题了。
参考地址:aaronbonner.tumblr/post/14969163463/cross-compiling-to-32bit-with-gcc
<2> 让nasm以64位的⽅式编译产⽣⽬标代码,并让gcc的连接器以默认的⽅式链接
但是第⼆种⽅法并不是仅仅更改nasm的编译⽅式那么简单,因为64位平台跟32位平台有很⼤的不同,包括参数的传递,指令集等。所以如果怕⿇烦的话完全可以使⽤第⼀种⽅法,让gcc产⽣32位的⽬标代码,因为32位的代码可以运⾏在64位的平台上,这应该就是所谓的向上兼容。不过64位将来应该会是主流,所以研究⼀下还是很有必要的。
⾸先对gcc 产⽣的32位与64位的汇编语⾔进⾏对⽐:
32位
1 gcc -m3
2 -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位,eax:32位ax:16位printf输出格式linux
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**************/
2extern choose
3
4 ;;;;;the data area
5 num1st        dd        3
6 num2nd        dd        4
7
8global        _start
9global        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
21int0x80
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
37int0x80
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。当参数为7 个以上时,前6 个与前⾯⼀样,但后⾯的依次从"右向左" 放⼊栈中。
例如:
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) 参数个数⼤于7 个的时候
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 的值如果在汇编模块中被改变了,在退出该模块时,必须将
其恢复。
教训:
⽤汇编写模块, 然后与c 整合, ⼀定要搞清楚编译器的⾏为, 特别是参数传递的⽅式. 此外, 我现在⽐较担⼼的⼀点是, 将来如果要把程序移植到WIN/VC 环境怎么办? 以前我⽤cygwin的gcc来处理汇编模块, ⽤vc来处理c模块, 只需要很少改动. 现在的问题是, 如果VC⽤不同的参数传递⽅式, 那我不就⿇烦了?