所谓调用就是让另一个子模块或子系统帮助自己干一些事,然后再返回回来。在我们的口头语中常常用“调XXXX”,这个“调”字被应用的范围太广了,执行一个API函数时我们会说调XXX,执行一个系统调用时我们也会说调XXX,甚至执行一个可执行文件时我们也会说调XXX……
实际上仅就上述三种“调”来说,完全就是不同的三个概念。先说一下函数调用:
函数调用最常见,函数可以来自动态库、静态库以及程序自定义的函数,可能我们一般的理解就是以"函数名(参数...)"的形式就可以完成一次函数调用了。调用之后程序会进入调用的函数内部执行,执行后再返回回来,这就是对函数调用的一般理解。实际上函数调用和返回的时候会有很多隐含的工作,后面再说。
系统调用和函数调用有什么区别呢?最大的区别就是系统调用需要由内核在内核态以高特权完成,而普通的函数调用都是在用户态完成的,不需要陷入内核态。所以系统调用就涉及到了CPU状态的切换,其实就是一个寄存器种几个标志位的改变。但是这样的高级工作不是你执行个普通函数就能完成的,让用户态最直接陷入内核态的方法就是通过中断和异常。0x80号中断是专门为系统调用保留的,当然可能不同处理器之间会有不同,但是处理器一定要提供一个能让用户态陷入内核态执行系统调用的方式。
普通的函数你可能需要执行Call, 那么系统调用你就需要执行int 0x80了。一个系统内核会提供很多系统调用来满足用户态的基本操作需求,常见的如exit, open, read, write, ioctl等,它们在内核中实现,需要应用程序通过中断陷入内核态来调用执行。来看一个简单的例子,以exit()系统调用为例,如果你想写一个程序调用exit系统调用来退出当前进程并返回退出状态值,那么程序可以简单如下(以下仅面向linux系统,并且是x86或x86_64体系结果的机器来说的,如果你是其它处理器,请参考相应手册后自行对号入座。):
1 .section .data
2 .section .text
3 .globl _start
4_start:
5 movl $1, %eax
6 movl $0, %ebx
7
8 int $0x80
第1行是想定义数据段,当然了我们这里不需要数据段。
第2行是在定义代码段的开始。
第3行是告诉汇编器不要在汇编之后废弃_start这个符号,这个符号很常见,必须用.globl标记,否则当计算机加载程序时就不知道该从哪执行了。
第4行就是_start这个标号了,代表一个地址。
第5行将%eax寄存器赋值为1,%eax在系统调用时保存着系统调用号,exit的系统调用号是1,所以将%eax赋值为1。
第6行%ebx寄存器赋值为0,%ebx是exit系统调用的返回值,也就是退出状态值的,这里赋值为0代表执行exit以0返回。如果写成别的数值就返回相应的数值。
第8行就是执行0x80号中断,中断处理会让程序陷入内核态,也会有很多准备现场的工作,这里不多说这些,因为这些属于内核对系统调用的处理,我们主要讲用户态怎么调用系统调用。中断会根据eax寄存器判断执行哪个系统调用,系统调用号都通过eax传递,但是其它寄存器的作用就不一定的,列举几个linux下常见的几个系统调用如下:
%eax
指令名称
%ebx
%ecx
%edx
1
exit
返回值
3
read
文件描述符
缓冲区开始地址
缓冲区大小
4
write
文件描述符
缓冲区开始地址
缓冲区大小
5
open
以空字符结束的文件名
选项列表
许可模式
6
close
文件描述符
12
chdir
以空字符结束的目录名
19
lseek
文件描述符
偏移量
模式
41
dup
文件描述符
42
pipe
管道数组
54
ioctl
文件描述符
请求
参数
http://www.lxhp.in-berlin.de/lhpsyscal.html上可以了解一些更详细的信息。
下面来汇编并执行上面的小程序吧,比如上面的程序名是myexit.s,那么执行如下:
as myexit.s -o myexit.o
将myexit.s汇编为myexit.o目标文件
ld myexit.o -o myexit
将myexit.o连接成myexit可执行文件。
执行./myexit程序,然后执行echo $?,看到返回值是多少。
尝试改变程序第6行的数值,让返回值不同,重新汇编连接,重新执行再查看返回结果。
如果有兴趣可以尝试调用其它系统调用。
差点忘了,还有一个“调”没说,“调”一个可执行程序,那其实就是执行了,这样实际上就是通过execve系统调用执行一个可执行文件,具体可以参考execve系统调用。当然了,一般在执行之前可能会先fork一个子进程,让子进程去execve,但是fork和execve在linux是两个独立的操作,不是一定关联在一起的。