# 8.1.3 Linux/x86-64 系统中的异常

为了使描述更具体,让我们来看看为 x86-64 系统定义的一些异常。有高达 256 种不同的异常类型 [50]。0~31 的号码对应的是由 Intel 架构师定义的异常,因此对任何 x86-64 系统都是一样的。32~255 的号码对应的是操作系统定义的中断和陷阱。图 8-9 展示了一些示例。

异常号 描述 异常类别
0 除法错误 故障
13 一般保护故障 故障
14 缺页 故障
18 机器检查 终止
32~255 操作系统定义的异常 中断或陷阱

图 8-9 x86-64 系统中的异常示例

# 1. Linux/x86-64 故障和终止

除法错误。 当应用试图除以零时,或者当一个除法指令的结果对于目标操作数来说太大了的时候,就会发生除法错误(异常 0)。Unix 不会试图从除法错误中恢复,而是选择终止程序。Linux shell 通常会把除法错误报告为“浮点异常(Floating exception)”。

一般保护故障。 许多原因都会导致不为人知的一般保护故障(异常 13),通常是因为一个程序引用了一个未定义的虚拟内存区域,或者因为程序试图写一个只读的文本段。Linux 不会尝试恢复这类故障。Linux shell 通常会把这种一般保护故障报告为“段故障(Segmentation fault)”。

缺页。 缺页(异常 14)是会重新执行产生故障的指令的一个异常示例。处理程序将适当的磁盘上虚拟内存的一个页面映射到物理内存的一个页面,然后重新执行这条产生故障的指令。我们将在第 9 章中看到缺页是如何工作的细节。

机器检查。 机器检查(异常 18)是在导致故障的指令执行中检测到致命的硬件错误时发生的。机器检查处理程序从不返回控制给应用程序。

# 2. Linux/86-64 系统调用

Linux 提供几百种系统调用,当应用程序想要请求内核服务时可以使用,包括读文件、写文件或是创建一个新进程。图 8-10 给出了一些常见的 Linux 系统调用。每个系统调用都有一个唯一的整数号,对应于一个到内核中跳转表的偏移量。(注意:这个跳转表和异常表不一样。)

C 程序用 syscall 函数可以直接调用任何系统调用。然而,实际中几乎没必要这么做。对于大多数系统调用,标准 C 库提供了一组方便的包装函数。这些包装函数将参数打包到一起,以适当的系统调用指令陷入内核,然后将系统调用的返回状态传递回调用程序。在本书中,我们将系统调用和与它们相关联的包装函数都称为系统级函数,这两个术语可以互换地使用。

在 x86-64 系统上,系统调用是通过一条称为 syscall 的陷阱指令来提供的。研究程序能够如何使用这条指令来直接调用 Linux 系统调用是很有趣的。所有到 Linux 系统调用的参数都是通过通用寄存器而不是栈传递的。按照惯例,寄存器 %rax 包含系统调用号,寄存器 %rdi%rsi%rdx%r10%r8%r9 包含最多 6 个参数。第一个参数在 %rdi 中,第二个在 %rsi 中,以此类推。从系统调用返回时,寄存器 %rcx%r11 都会被破坏,%rax 包含返回值。−4095 到 −1 之间的负数返回值表明发生了错误,对应于负的 errno

编号 名字 描述 编号 名字 描述
0 read 读文件 33 pause 挂起进程直到信号到达
1 write 写文件 37 alarm 调度告警信号的传送
2 open 打开文件 39 getpid 获得进程 ID
3 close 关闭文件 57 fork 创建进程
4 stat 获得文件信息 59 execve 执行一个程序
9 mmap 将内存页映射到文件 60 _exit 终止进程
12 brk 重置堆顶 61 wait4 等待一个进程终止
32 dup2 复制文件描述符 62 kill 发送信号到一个进程

图 8-10 Linux x86-64 系统中常用的系统调用示例

例如,考虑大家熟悉的 hello 程序的下面这个版本,用系统级函数 write(见 10.4 节)来写,而不是用 printf

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

write 函数的第一个参数将输出发送到 stdout。第二个参数是要写的字节序列,而第三个参数是要写的字节数。

图 8-11 给出的是 hello 程序的汇编语言版本,直接使用 syscall 指令来调用 writeexit 系统调用。第 9~13 行调用 write 函数。首先,第 9 行将系统调用 write 的编号存放在 %rax 中,第 10~12 行设置参数列表。然后第 13 行使用 syscall 指令来调用系统调用。类似地,第 14~16 行调用 _exit 系统调用。

code/ecf/hello-asm64.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)
    movq $1, %rax       # write is system call 1
    movq $1, %rdi       # Arg1: stdout has descriptor 1
    movq $string, %rsi  # Arg2: hello world string
    movq $len, %rdx     # Arg3: string length
    syscall             # Make the system call

    # Next, call _exit(0)
    movq $60, %rax      # _exit is system call 60
    movq $0, %rdi       # Arg1: exit status is 0
    syscall             # Make the system call

code/ecf/hello-asm64.sa

图 8-11 直接用 Linux 系统调用来实现 hello 程序

旁注 关于术语的注释

各种异常类型的术语根据系统的不同而有所不同。处理器 ISA 规范通常会区分异步“中断”和同步“异常”,但是并没有提供描述这些非常相似的概念的概括性的术语。为了避免不断地提到“异常和中断”以及“异常或者中断”,我们用单词“异常”作为通用的术语,而且只有在必要时才区别异步异常(中断)和同步异常(陷阱、故障和终止)。正如我们提到过的,对于每个系统而言,基本的概念都是相同的,但是你应该意识到一些制造厂商的手册会用“异常”仅仅表示同步事件引起的控制流的改变。