汇编语言与C语言混合编程

函数调用规约

在c语言中有这样的代码

int subtract (int a,int b) {
	return a-b;
}

我们可以用这样的形式调用它

int sub = subtract(3,2)

这样我们就完成了一次函数调用,这是C语言最常见的函数调用手法,可是大家想过没有,计算机是如何知道我们传入的两个参数3和2在哪里的呢?

我们可以保存在寄存器中,但是寄存器的数量是有限的,我们也可以放在内存栈中,调用的时候传入栈的地址,放在栈内存中有几个好处

  • 每个进程都有自己的栈,这就是每个内存自己的专用内存空间。
  • 保存参数的内存地址不用再花精力维护,已经有栈机制来维护地址变化了。

保存在内存栈中,我们第一个问题解决了,怎么找到这两个传入值。那第二个问题,谁来负责收回这部分内存,参数很多的情况下,主调函数将参数以什么样的顺序传递呢?

这两个问题其实也好解决?我自己规定好了,是调用者负责将栈回收到之前的位置还是被调用者将栈回收到之前的位置,参数是从左往右压栈还是从右往左压栈。

实际上高级语言也是这么规定的,不同的高级语言之间也有些许不同,下面就详细罗列出了这些调用规约。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hPbJoAvy-1679378163605)(C:\Users\LoveSS\Desktop\linux内核\picture\image-20230319185501582.png)]

其实我们这里讲的是C语言与汇编语言的混合编程,所以我们只需要关注有关c语言的 cdecl 调用规约就好了。

cdecl调用约定由于起源于C语言,所以又称为C调用约定,是C语言默认的调用约定。cdecl的调用约定意味着

  • 调用者将所有参数从右向左入栈。
  • 调用者清理参数所占的栈空间。

我们将之前的sutract变成汇编看一下

主调用者

push 2
push 3
call subtract
add esp,8

被调用者

push ebp
mov  ebp,esp
mov  eax,[ebp+0x8]
add  eax,[ebp+0xc]
mov  esp,ebp
pop  ebp
ret

看到了嘛,主调用者负责将参数从右到左入栈,主调用者负责将栈顶指针的前八个字节弹出

c库函数与系统调用

我们先学习下Linux系统调用,利用系统调用能够帮助简化演示的模型。

系统调用是Linux内核提供的一套子程序,它和Windows的动态链接库dll文件的功能一样,用来实现一系列在用户态不能或不易实现的功能,比如最常见的读写硬盘文件,只有操作系统有权限去访问硬件,用户程序是没有权限的,用户程序只能向操作系统寻求帮助,故系统调用是供用户程序来使用的,操作系统权利至高无上,不需要使用自己对外发布的功能接口,即系统调用。

系统调用很像BIOS中断调用,只不过系统调用的入口只有一个,即第0x80号中断,具体的子功能在寄存器eax中单独指定。在Linux系统中,系统调用是定义在/usr/include/asm/unistd.h文件中,在asm目录下提供了这两个版本,文件名分别是unistd_32.h 和unistd_64.h,这个很显然一个对应32位一个对应64位,我在32位的版本中找到了435个系统调用,我们摘录前10个看一看

#define __NR_restart_syscall 0
#define __NR_exit 1
#define __NR_fork 2
#define __NR_read 3
#define __NR_write 4
#define __NR_open 5
#define __NR_close 6
#define __NR_waitpid 7
#define __NR_creat 8
#define __NR_link 9

如果不知道某个系统调用的用法的话,可以用man命令来看一看

man 2 #系统调用名
man 2 write
系统调用的两种方式

调用“系统调用”有两种方式

  • 将系统调用指令封装为c库函数,通过库函数进行系统调用,操作简单。
  • 不依赖任何库函数,直接通过汇编指令int与操作系统通信。
库函数系统调用

write的功能是把buf指向的缓冲区中的count个字节写入fd指向的文件描述符,执行成功后返回写入的字节数,失败则返回-1。我们试一试

#include <unistd.h>

int main() {
    write(1,"Hello C\n",8);
    return 0;
}

我们编译执行后发现我们的命令终端输出了这个字符串,因为在Linux中,1号文件是 stdout 也就是标准输出。

直接系统调用

我们要看看系统调用输入参数的传递方式。

当输入的参数小于等于5个时,Linux用寄存器传递参数。当参数个数大于5个时,把参数按照顺序放入连续的内存区域,并将该区域的首地址放到ebx寄存器。这里我们只演示参数小于等于5个的情况。

(1)ebx存储第1个参数。
(2)ecx存储第2个参数。
(3)edx存储第3个参数。
(4)esi存储第4个参数。
(5)edi存储第5个参数。

我们实践一下

section .data
str_c_lib: db "C library says: hello c!",0xa;
str_c_lib_len equ $-str_c_lib

str_syscall: db "syscall says: hello c!",0xa;
str_syscall_len equ $-str_syscall

section .text
global _start
_start:
	;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;模拟C语言的调用形式
	push str_c_lib_len
	push str_c_lib
	push 1
	call simu_write
	add  esp,12
	;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;直接进行系统调用
	mov eax,4
	mov ebx,1
	mov ecx,str_syscall
	mov edx,str_syscall_len
	int 0x80
	;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;退出程序
	mov eax,1
	int 0x80
	
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;自己定义的函数模拟c语言调用形式
simu_write:
	push ebp
	mov ebp,esp
	mov eax,4
	mov ebx,[ebp+8]
	mov ecx,[ebp+12]
	mov edx,[ebp+16]
	int 0x80
	pop ebp
	ret

编译

nasm -f elf32 -o wr.o wr.s

链接

ld -m elf_i386 -o wr.bin wr.o

这里我们需要 -m 指定我们链接的文件格式

./wr.bin

执行就会打印下面的字符串

C library says: hello c!
syscall says: hello c!

c与汇编的相互调用

我们这里准备了两个例子

c语言

extern void asm_print(char*,int);

void c_print(char *str) {
	int len = 0;
	while(str[len++]);
	asm_print(str,len);
}

汇编

section .data
str:db "asm_print says hello c",0xa,0
; 0xa指出字符串是ascii编码,0是手动加上的\n
str_len equ $-str

section .text
extern c_print

global _start
_start:
	push str
	call c_print
	add esp,4
	
	mov eax,1  ; 子功能号1是exit
	int 0x80   ; 发起中断通知Linux完成请求的功能

global asm_print
asm_print:
	push ebp
	mov ebp,esp
	mov eax,4
	mov ebx,1
	mov,ecx,[ebp+8]
	mov edx,[ebp+12]
	int 0x80
	pop ebp
	ret

c中的函数c_print是被汇编代码调用的,c_print又是调用汇编代码中的asm_print实现的。在C语言中的第一行是申明外部函数asm_print,通知编译器这个函数并不在当前文件中定义,我们知道这个函数是在汇编中定义的,但是编译器并不知道,所以只能在链接阶段将此函数重新定位,编排地址。

由于我们并不想在链接的时候把C语言标准库也链接进来,所以这里我们是手动的在统计字符串的长度,这也是为什么我们会在汇编中这个字符串的结尾手动加一个0的原因

为了在汇编文件中引用外部的函数(未必是C代码中的),需要用extern来声明所需要的函数名。

在汇编语言中导出符号名用global关键字,这在之前说_start时大伙已有所耳闻,global将符号导出为全局属性,对程序中的所有文件可见,这样其他外部文件中也可以引用被global导出的符号啦,无论该符号是函数,还是变量。

我们可以把这两段代码编译链接一下

编译C文件

gcc -c -m32 -o C_with_S.o C_with_S.c

编译汇编文件

nasm -f elf32 -o S_with_C.o S_with_C.s 

链接这两个文件

ld C_with_S.o S_with_C.o -o CS.bin -m elf_i386

执行

./ CS.bin

得到输出结果

lovetzp@ubuntu:~/temp$ ./CS.bin 
asm_print says hello c
c -m32 -o C_with_S.o C_with_S.c

编译汇编文件

nasm -f elf32 -o S_with_C.o S_with_C.s 

链接这两个文件

ld C_with_S.o S_with_C.o -o CS.bin -m elf_i386

执行

./ CS.bin

得到输出结果

lovetzp@ubuntu:~/temp$ ./CS.bin 
asm_print says hello c
  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

LyaJpunov

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值