C++中嵌入汇编语言的方法

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

你可能感兴趣的:(c++,算法,开发语言)