当前位置:   article > 正文

C语言和汇编语言相互调用_c语言调用汇编

c语言调用汇编

C语言和汇编语言相互调用

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

1. C语言的链接过程

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

  1. 预处理。(hello.c -> hello.i)把头文件包含起来。
  2. 编译。(hello.i -> hello.s)编译成汇编代码。
  3. 汇编。(hello.s -> hello.o)生成目标文件。
  4. 链接。(hello.o -> hello)生成可执行文件。

汇编代码变成可执行文件,也要经过汇编、链接。

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

// main.c
#include<stdio.h>
int add(int a, int b);
int main(void)
{
    printf("%d\n", add(1,2));
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
// add.c
int add(int a, int b)
{
    return a + b;
}
  • 1
  • 2
  • 3
  • 4
  • 5

然后分别编译,再共同链接:

# 编译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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

结果如下图:

image-20210328102540044

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

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

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

2. 函数调用约定

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

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

2.1 C语言的调用约定

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

总结下约定有下面几点:

  1. 参数使用栈传递
  2. 参数从右到左入栈
  3. 由调用者回收栈空间

2.2 函数调用时栈空间回收

2.1.2 什么是栈空间回收

栈空间回收是什么情况?如下例子,调用者调用add(1,2),被调用者把1 + 2算出来:

调用者传入参数:

push 1
push 2
call add
  • 1
  • 2
  • 3

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

被调用者取出参数并执行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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

当被调用者的代码执行完毕,栈空间应该这样的情况:

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

2.1.3 如何栈空间回收

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

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

2.1.3.1 被调用者回收栈空间

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

; 调用者
push 1
push 2
call add
  • 1
  • 2
  • 3
  • 4
; 被调用者
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,清理栈空间
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
2.1.3.2 调用者回收栈空间

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

; 调用者
push 1
push 2
call add
add esp, 8			; 还原esp栈顶指针
  • 1
  • 2
  • 3
  • 4
  • 5
; 被调用者
push ebp			; 备份ebp,因为取参数时要用ebp来寻址
mov ebp, esp 		; 把栈顶地址给ebp,因为栈顶不能随便动
mov eax, [ebp + 8]  ; 取出参数2
add eax, [ebp + 12] ; 取出参数1
pop ebp				; 还原ebp
ret 				
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
; 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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

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

# 编译汇编 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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

注意:

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

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

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

输出效果如下:

image-20210328120333601

字符串“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;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
// 文件名: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);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
; 文件名: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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

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

# 汇编 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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

运行结果如下图:

image-20210329105728007

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

一开始,这里的main.asm中定义字符串,是没有添加0作为结尾标志的,然后出现了Segmentation fault

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/从前慢现在也慢/article/detail/346645
推荐阅读
相关标签
  

闽ICP备14008679号