在c语言中有这样的代码
int subtract (int a,int b) {
return a-b;
}
我们可以用这样的形式调用它
int sub = subtract(3,2)
这样我们就完成了一次函数调用,这是C语言最常见的函数调用手法,可是大家想过没有,计算机是如何知道我们传入的两个参数3和2在哪里的呢?
我们可以保存在寄存器中,但是寄存器的数量是有限的,我们也可以放在内存栈中,调用的时候传入栈的地址,放在栈内存中有几个好处
保存在内存栈中,我们第一个问题解决了,怎么找到这两个传入值。那第二个问题,谁来负责收回这部分内存,参数很多的情况下,主调函数将参数以什么样的顺序传递呢?
这两个问题其实也好解决?我自己规定好了,是调用者负责将栈回收到之前的位置还是被调用者将栈回收到之前的位置,参数是从左往右压栈还是从右往左压栈。
实际上高级语言也是这么规定的,不同的高级语言之间也有些许不同,下面就详细罗列出了这些调用规约。
其实我们这里讲的是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
看到了嘛,主调用者负责将参数从右到左入栈,主调用者负责将栈顶指针的前八个字节弹出
我们先学习下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
调用“系统调用”有两种方式
write的功能是把buf指向的缓冲区中的count个字节写入fd指向的文件描述符,执行成功后返回写入的字节数,失败则返回-1。我们试一试
#include
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语言
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