Unix异常

有四种不同的异常:

  • 中断(interrupt): 当一个外部I/O设备,例如定时器芯片或者一个磁盘控制器,设置了处理器芯片上的中断引脚时,(对于任意指令)中断会异步的发生。中断控制结束后会返回到故障指令后面的那条指令。
  • 故障(fault)终止(abort): 一条指令的执行可能导致故障和终止的同时发生。故障处理程序会重新启动故障指令,而终止处理程序从不将控制返回给被中断的流。
  • 陷阱(trap): 陷阱就像一种函数调用,它是用来向应用提供到操作系统代码的受控的入口点的系统调用。

陷阱和系统调用

陷阱是一种有意的异常,处理器提供了syscall n指令,用户想请求服务n时,可以执行这条指令(n为异常号),这将导致一个到异常处理程序的陷阱,这个处理程序对参数进行解码,并调用适当的内核程序。

当应用程序需要请求内核服务(例如读文件read,创建进程fork,终止进程exit或加载新程序execve)时,就需要使用系统调用。系统调用对程序员来说就像使用函数调用一样,我们可以简单的理解其为一种提供程序到内核的像函数一样的接口。

Linux提供了上百种系统调用,为了使用系统调用,在IA32系统上,(用户程序必须通过系统调用接口间接的访问内核代码和数据)系统调用通过一条int n的陷阱指令来提供(intinterrupt,系统调用的异常号通常为1280x80,所以指令为int $0x80)。

C程序可以通过syscall函数直接调用任何系统调用,但标准C库提供了一组包装函数,这些包装函数将参数打包到一起,以适当系统调用号陷入内核,然后将系统的返回状态传递回调程序。

使用int $0x80系统调用

用C提供的系统函数写的hello程序,write的参数为文件描述符,字节序列,字节数。

int main()
{
    write(1, "hello, world\n", 13);
    exit(0);
}

下面是上面代表的汇编版本,使用int指令来调用writeexit系统调用,系统调用的参数通过寄存器来传递。%eax传递系统调用号(对应一个到内核中跳转表的偏移量),%ebx, %ecx, %edx, %esi, %edi, %ebp可以包含最多六个任意参数(%esp不能使用,因为进入内核态时,内核会覆盖它)。

  .section .data
string:
  .ascii "hello, world\n"
string_end:
  .equ len, string_end - string

  .section .text
  .globl _start
_start:
  # First, call write(1, "hello, world\n", 13)
  movl $4, %eax      # system call number 4
  movl $1, %ebx      # stdout has descriptior 1
  movl $string, %ecx # hello world sting
  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
$ as hello.asm -o hello.o
$ ld hello.o -o hello
$ ./hello

学习之后未解决的问题:

既然可以用int 0x80来代表系统调用,为什么不可以用int 0x04直接调用读操作,是因为0x80是代表异常的异常号,而0x04是系统调用号吗?如果是,它们有什么区别?

又或者是因为用户程序必须通过系统调用接口间接的访问内核代码和数据?