一、Exceptions(异常) and System Call(系统调用)
1.1 陷阱
陷阱是有意为之的异常,是处理器执行程序的一条指令的结果。陷阱最重要的用途是提供用户程序和内核之间一个像普通过程调用似的接口,名曰:系统调用。用户程序经常需要向内核请求服务,比如读一个文件(read) 、创建一个新的进程(fork) 、加载一个新的程序(execv),或者终止当前进程(exit) 。为了允许对这些内核服务的受控的访问,处理器提供了一条特殊的 “syscall n” 指令,当用户程序想要请求服务n时,可以执行这条指令。执行syscall 指令会导致一个到异常处理程序的陷阱,这个处理程序对参数解码,并调用适当的内核程序。
1.1.1 System Call N
系统调用运行在内核模式中,内核模式允许执行系统调用函数的指令,并访问定义在内核中的栈。
Examples of popular system calls:
Linux provides hundreds of system calls.
Source: /usr/include/sys/syscall.h.
1.1.2 实现
1.在IA32 系统上,系统调用是通过一条称为int n1 的陷阱指令来提供的,其中n可能是IA32 异常表 中256 个条目中任何一个的索引。在历史上,系统调用是通过异常128 (Ox80) 提供的。
2.所有的传到Linux系统调用服务程序的参数都是通过寄存器传递的。按照惯例,寄存器%eax包含系统调用号,寄存器%ebx 、%ecx 、%edx 、%esi 、%edi和%ebp 包含最多六个任意的参数。栈指针%esp不能使用,因为当进入内核模式时,内核会覆盖它。
3.C程序用syscall函数可以直接调用任何存在于系统调用表中的系统调用。然而,实际中几乎没必要这么做。对于大多数系统调用,标准C库提供了一组方便的包装函数。这些包装函数将参数打包到一起,以适当的系统调用号陷入内核,然后将系统调用的返回状态传递回调用程序。我们将系统调用和与它们相关联的包装函数称为系统级函数。
4.内核记录了系统调用表中的所有已注册过的系统调用的列表,存储在sys_call_table中(sys_call_table是一张由指向实现各种系统调用的内核函数的函数指针组成的表)。它与体系结构有关,一般在entry.s中定义。这个表中为每一个有效的系统调用指定了惟一的系统调用号。
1.1.2.1 int 指令
有必要说一下int指令。
中断信息可以来自CPU的外部,例如I/O设备的中断请求,还可以来自CPU的内部,通过执行到某条特定的指令引发内部中断。int n指令就是产生内部中断的指令之一,其中n的取值范围一般是0~256 。
—The INT n instruction uses an interrupt vector as an argument, which allows a program to call any interrupt handler.
CPU执行int n指令,相当于引发一个n号中断的中断过程,可以在程序中使用int指令调用任何一个中断的中断处理程序。
1.1.2.2 int Ox80(陷入内核)
当进程执行系统调用时,先调用系统调用库中定义的某个函数,该函数通常被展开成
_syscallN
的形式通过INT 0x80
来陷入内核,其参数将通过寄存器传往内核。 INT 0x80的中断处理程序名为system_call
。为了保证在内核执行完系统调用后能够返回用户态的调用点继续执行用户代码,必须在进入内核态时往内核栈中压入一个上下文层,在从内核返回时会弹出一个上下文层,这样用户进程就可以继续运行。
Stack Switch on a Call to a Different Privilege level:
在执行INT指令时,实际完成了以下几条操作:
(1) 由于INT指令发生了不同优先级之间的控制转移,所以首先从TSS(任务状态段)中获取高优先级的内核堆栈信息(SS和ESP);
(2) 把低优先级堆栈信息(SS和ESP)保留到高优先级堆栈(即内核栈)中;
(3) 把EFLAGS,外层CS,EIP推入高优先级堆栈(内核栈)中。
(4) 通过IDT加载CS,EIP(控制转移至中断处理函数)
note2
然后就进入了中断0x80的中断处理函数
system_call
了,在该函数中首先使用了一个宏SAVE_ALL
,该宏的定义如下所示:
# define SAVE_ALL
cld;
pushl %es;
pushl %ds;
pushl %eax;
pushl %ebp;
pushl %edi;
pushl %esi;
pushl %edx;
pushl %ecx;
pushl %ebx;
movl $(__KERNEL_DS), %edx; //设置内核数据段地址
movl %edx, %ds; //%ds是数据段寄存器
movl %edx, %es;
该宏的功能一方面是将寄存器上下文压入到内核栈中以供系统调用返回恢复用户态使用,另一方面将用户模式下传递给系统调用的参数压入内核栈中以供系统调用函数内部使用。在用户陷入内核之前会把参数指定到各个寄存器中,然后在陷入核心之后使用SAVE_ALL把这些保存在寄存器中的参数依次压入内核栈,这样内核才能使用用户传入的参数。
system_call
的部分源代码:
//在system_call之前,还在用户模式下的时候,就已经将系统调用函数要用到的参数放在了寄存器中
ENTRY(system_call)
pushl %eax # save orig_eax //为返回值保存%eax
SAVE_ALL //调用SAVE_ALL
GET_CURRENT(%ebx)
cmpl $(NR_syscalls),%eax //比较%eax中的系统调用号,看是否合法。
jae badsys
testb $0x20,flags(%ebx) # PF_TRACESYS
jne tracesys
call *SYMBOL_NAME(sys_call_table)(,%eax,4)
//根据系统调用号在sys_call_table中找系统调用函数指针,并跳入系统调用函数中去。
. . . . . .
//汇编指令相关的知识可以参考《深入理解操作系统》第3章。
在这里所做的所有工作是:
1.保存%eax寄存器。
因为%eax是用来保存系统调用返回值的默认寄存器,里面的内容会被返回值覆盖。
2.调用
SAVE_ALL
保存寄存器上下文。3.判断当前调用是否是合法系统调用(%eax中存放着系统调用号,它应该小于NR_syscalls,在
unistd.h
中,有宏定义#define NR_syscalls 294
)。4.如果设置了PF_TRACESYS标志(PF_TRACESYS是Linux的进程标志Process flag, 表示正在跟踪),则跳转到tracesys,在那里将会把当前进程挂起并向其父进程发送SIGTRAP。
5.如果没有设置PF_TRACESYS标志,则调用查找系统调用函数指针的处理函数 ,然后处理函数以%eax*4作为偏移(%eax存放了系统调用号),在系统调用表sys_call_table中查找系统调用函数的入口地址,并跳转到该入口地址。
1.1.3 实例
1) open
2) write
int main{
write(1, "hello, world\n", 13)
//第一个参数"1"指示写到stdout;第二个参数是要写的字节序列;第三个参数给出要写的字节数。
exit(0);
}
code/ecflhello-asm.sa
.section .data
string:
.ascii "hello, world\n"
string_end:
.equ len, string_end - string
.section .text
.globl main
main:
? //First, call write(1, "hello, world\n", 13)
movl $4, %eax //System call number 4
movl $1, %ebx //stdout has descriptor 1
movl $string, %ecx // Hello world string
movl $len, %edx // String length
int $0x80 //System call code
? //Next, call exit(0)
movl $1, %eax //System call number 1
movl $0, %ebx //Argument is 0
int $0x80 //System call code
- 这里的编号n就是一个异常号。它满足异常号的概念 —-定位异常表中相应的异常向量。 ?
- ESP是作为指针的寄存器,常用于堆栈操作。在这里,ESP被当作栈顶偏移指针,SS 是它的默认段寄存器(栈基地址)。EIP存放指令的偏移地址,同CS一同指向即将执行的那条指令的地址。CS是默认的代码段寄存器,同EIP寄存器一同指向当前正在执行的那个地址。IDT是中断描述表,里面包含有各个异常号及其对应的中断处理程序的入口地址。DS数据段寄存器,这个寄存器连同ESI一同指向指令将要处理的内存,同时,所有的内存操作指令默认情况下都用它指定操作段。ESI:通常在内存中作为“源地址指针”使用,DS是其默认数据段寄存器。 获取更多信息 ?