C语言和汇编语言相互调用 不同的语言就像一座孤岛,似乎毫不相干,但是所有的代码最终都要编译成机器指令,他们本质上也是一样的,最终都是变成指令给CPU下达命令。

1. C语言的链接过程 我们知道一个C语言源文件变成可执行文件,需要经过一下几个步骤:

预处理。(hello.c -> hello.i)把头文件包含起来。 编译。(hello.i -> hello.s)编译成汇编代码。 汇编。(hello.s -> hello.o)生成目标文件。 链接。(hello.o -> hello)生成可执行文件。 汇编代码变成可执行文件,也要经过汇编、链接。

比如main.c调用了add.c中的add()函数:

// main.c #include int add(int a, int b); int main(void) {     printf("%d\n", add(1,2));     return 0; } // add.c int add(int a, int b) {     return a + b; } 然后分别编译,再共同链接:

# 编译main.c,生成目标文件main.o gcc -c main.c -o main.o # 编译add.c,生成目标文件add.o gcc -c add.c -o add.o # 将目标文件main.o和add.o链接生成可执行文件main gcc main.c add.o -o main # 执行可执行文件main ./main 结果如下图:

链接过程中有一项重要工作是重定位,把多个目标文件链接在一起,需要确定源文件中函数的内存地址。

比如上面的main.c中调用了add.c中的add()函数,那么在main.c编译成目标文件时,编译器并不会检测add()函数是否存在。但是在链接时,需要确定add()函数的地址。由于main.o和add.o是一起参与链接生成可执行文件的,因此add()函数的地址会在add.o中找到。所以两个目标文件(main.o和add.o)完美地合成一个可执行文件。

所以把汇编源代码经过汇编生成的目标文件,和C语言生成的目标文件,链接成一个可执行文件,应该是可以相互调用的。

2. 函数调用约定 函数在调用时,需要传递参数,可是调用者传递的参数,被调用者怎么知道去哪里找到参数呢?

他们肯定要有一个约定,比如参数是去某个特定寄存器中取,还是在某个栈中取?压栈的顺序又是怎样的?以及,函数调用完后,栈空间谁来回收?

2.1 C语言的调用约定 C语言的调用约定使用的是:cdecl(C Declaration),函数参数是从右到左的顺序入栈的。GNU/Linux GCC,把这一约定作为事实上的标准,x86 架构上的许多 C 编译器也都使用这个约定。在 cdecl 中,参数是在栈中传递的。EAX、ECX 和 EDX 寄存器是由调用者保存的,其余的寄存器由被调用者保存。函数的返回值存储在 EAX 寄存器。由调用者清理栈空间。

总结下约定有下面几点:

参数使用栈传递 参数从右到左入栈 由调用者回收栈空间 2.2 函数调用时栈空间回收 2.1.2 什么是栈空间回收 栈空间回收是什么情况?如下例子,调用者调用add(1,2),被调用者把1 + 2算出来:

调用者传入参数:

push 1 push 2 call add 当上述代码执行完,栈空间应该是如下图所示的:

被调用者取出参数并执行add(1,2):

push ebp            ; 备份ebp,因为取参数时要用ebp来寻址 mov ebp, esp         ; 把栈顶地址给ebp,因为栈顶不能随便动 mov eax, [ebp + 8]  ; 取出参数2 add eax, [ebp + 12] ; 取出参数1 pop ebp                ; 还原ebp ret 当被调用者的代码执行完毕,栈空间应该这样的情况:

所以此时,参数还在栈里面,但是函数都调用完了,栈里面的参数应该要清理。所以在调用函数有个栈空间回收。

2.1.3 如何栈空间回收 栈空间回收很简单,只不过就是把栈顶指针esp移动到调用函数之前的状态即可。

那么这里有个问题,谁来负责栈空间回收?调用者还是被调用者?

2.1.3.1 被调用者回收栈空间 如果是被调用者回收栈空间,那么被调用者应该负责还原esp,代码应该如下:

; 调用者 push 1 push 2 call add ; 被调用者 push ebp            ; 备份ebp,因为取参数时要用ebp来寻址 mov ebp, esp         ; 把栈顶地址给ebp,因为栈顶不能随便动 mov eax, [ebp + 8]  ; 取出参数2 add eax, [ebp + 12] ; 取出参数1 pop ebp                ; 还原ebp ret 8                ; 表示esp + 8,清理栈空间 2.1.3.2 调用者回收栈空间 如果是调用者回收栈空间,那么调用者要负责还原esp,代码应该如下:

; 调用者 push 1 push 2 call add add esp, 8            ; 还原esp栈顶指针 ; 被调用者 push ebp            ; 备份ebp,因为取参数时要用ebp来寻址 mov ebp, esp         ; 把栈顶地址给ebp,因为栈顶不能随便动 mov eax, [ebp + 8]  ; 取出参数2 add eax, [ebp + 12] ; 取出参数1 pop ebp                ; 还原ebp ret                  3. C语言调用汇编语言 这里调用者是C语言,被调用者是汇编语言。

这里是main.c中调用print.asm中的print()函数:

// main.c extern void print(char*, int); // 表示print函数不在本文件内,使用extern声明 int main(void) {     print("hello\n", 6);     return 0; } ; print.asm global print                 ; 设置print为全局可见 print:     push ebp     mov ebp, esp     mov eax, 4              ; 发起系统调用     mov ebx, 1              ; ebx表示stdout     mov ecx, [ebp + 8]      ; 取出传入的第二个参数,表示字符串的地址     mov edx, [ebp + 12]     ; 取出传入的第一个参数,表示字符的个数     int 0x80                ; int 0x80表示系统调用

    pop ebp     ret 然后分别生成目标文件,然后链接成一个可执行文件:

# 编译汇编 main.c,生成目标文件main.o gcc -m32 -c main.c -o main.o # 汇编 print.asm,生成目标文件print.o nasm -f elf print.asm -o print.o # 链接两个目标文件main.o和print.o,生成可执行文件hello ld -m elf_i386 -s -o hello main.o print.o -e main # 执行可执行文件hello ./hello 注意:

​ print.asm汇编生成的目标文件是32位的

​ 如果使用gcc直接编译汇编生成的目标文件是64位的,会无法链接

​ 因此在使用gcc编译时要使用参数-m32指定生成32位的目标文件

输出效果如下:

字符串“hello”是成功输出了,然后后面报错了Segmentation fault (core dumped) ,也不知道是为什么报错,最终也没解决。

错误原因我猜测是在main.c中传递字符串时,没有字符串结尾标志吧,但是传入’\0’似乎在汇编中无法识别?

4. 汇编语言调用C语言 使用汇编语言调用C语言,为了避免使用库函数,因此C语言还需使用调用汇编语言,但是C语言调用汇编语言上面讲过了。

这里的例子依然是输出字符串,文件之间的函数调用关系如下图:

; 文件名:main.asm extern print_c ; print_c来自外部的C语言源程序

section .data     str: db "fuck", 0xa, 0     str_len equ $ - str section .text ;---调用print_c.c中的print_c函数---; global _start _start:     push str_len   ; 传入参数,表示字符的个数     push str       ; 传入参数,表示字符串的地址     call print_c     add esp, 8     ;栈空间回收

;---退出程序---;     mov eax, 1     ; 系统调用的第1号子程序是exit     int 0x80       ; 相当于return 0;

// 文件名:print_c.c extern void print_asm(char*, int); // print_c()函数调用print_asm.asm文件中的print_asm函数 void print_c(char* s, int count) {     print_asm(s, count); } ; 文件名:print_asm.asm global print_asm                 ; 设置print_asm函数为全局可见 print_asm:     push ebp     mov ebp, esp     mov eax, 4              ; 发起系统调用     mov ebx, 1              ; ebx表示stdout     mov ecx, [ebp + 8]      ; 取出传入的第二个参数,表示字符串的地址     mov edx, [ebp + 12]     ; 取出传入的第一个参数,表示字符的个数     int 0x80                ; int 0x80表示系统调用

    pop ebp     ret 然后分别编译汇编,然后把这三个文件链接成一个可执行文件:

# 汇编 main.asm生成目标文件 main.o nasm -f elf main.asm -o main.o # 编译汇编print_c.c 生成目标文件 print_c.o gcc -m32 -c print_c.c -o print_c.o # 汇编print_asm.asm生成目标文件print_asm.o nasm -f elf print_asm.asm -o print_asm.o # 把上述的三个文件链接成一个可执行文件,名为fuck ld -m elf_i386 main.o print_c.o print_asm.o -o fuck ./fuck 运行结果如下图:

这里就没有出现Segmentation fault这样的错误了。

一开始,这里的main.asm中定义字符串,是没有添加0作为结尾标志的,然后出现了Segmentation fault ———————————————— 版权声明:本文为CSDN博主「Freestyle Coding」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:https://blog.csdn.net/m0_55708805/article/details/115296625

精彩文章

评论可见,请评论后查看内容,谢谢!!!评论后请刷新页面。