1.5.9 Linux 内核

    为了方便学习,选择一个稳定版本,比如最新的 4.16.3。

    1. $ wget -c https://cdn.kernel.org/pub/linux/kernel/v4.x/linux-4.16.3.tar.xz
    2. $ tar -xvJf linux-4.16.3.tar.xz
    3. $ cd linux-4.16.3/
    4. $ make clean && make mrproper

    内核的配置选项在 .config 文件中,有两种方法可以设置这些选项,一种是从当前内核中获得一份默认配置:

    1. $ zcat /proc/config.gz > .config
    2. $ make oldconfig

    另一种是自己生成一份配置:

    1. $ make localmodconfig # 使用当前内核配置生成
    2. # OR
    3. $ make defconfig # 根据当前架构默认的配置生成

    为了能够对内核进行调试,需要设置下面的参数:

    1. CONFIG_DEBUG_INFO=y
    2. CONFIG_DEBUG_INFO_REDUCED=n
    3. CONFIG_GDB_SCRIPTS=y

    如果需要使用 kgdb,还需要开启下面的参数:

    1. CONFIG_STRICT_KERNEL_RWX=n
    2. CONFIG_FRAME_POINTER=y
    3. CONFIG_KGDB=y
    4. CONFIG_KGDB_SERIAL_CONSOLE=y

    CONFIG_STRICT_KERNEL_RWX 会将特定的内核内存空间标记为只读,这将阻止你使用软件断点,最好将它关掉。如果希望使用 kdb,在上面的基础上再加上:

    1. CONFIG_RANDOMIZE_BASE=n
    2. CONFIG_RANDOMIZE_MEMORY=n

    将上面的参数写到文件 .config-fragment,然后合并进 .config

    1. $ ./scripts/kconfig/merge_config.sh .config .config-fragment

    最后因为内核编译默认开启了 -O2 优化,可以修改 Makefile 为 -O0

    1. KBUILD_CFLAGS += -O0

    编译内核:

    1. $ make

    完成后当然就是安装,但我们这里并不是真的要将本机的内核换掉,接下来的过程就交给 QEMU 了。(参考章节4.1)

    在 Linux 中,系统调用是一些内核空间函数,是用户空间访问内核的唯一手段。这些函数与 CPU 架构有关,x86-64 架构提供了 322 个系统调用,x86 提供了 358 个系统调用(参考附录9.4)。

    下面是一个用 32 位汇编写的例子,:

    1. .data
    2. msg:
    3. .ascii "hello 32-bit!\n"
    4. len = . - msg
    5. .text
    6. .global _start
    7. _start:
    8. movl $len, %edx
    9. movl $msg, %ecx
    10. movl $1, %ebx
    11. movl $4, %eax
    12. int $0x80
    13. movl $0, %ebx
    14. movl $1, %eax
    15. int $0x80

    可以看到程序将调用号保存到 eax,并通过 来使用系统调用。

    虽然软中断 int 0x80 非常经典,早期 2.6 及以前版本的内核都使用这种机制进行系统调用。但因其性能较差,在往后的内核中使用了快速系统调用指令来替代,32 位系统使用 sysenter(对应sysexit) 指令,而 64 位系统使用 syscall(对应sysret) 指令。

    一个使用 sysenter 的例子:

    1. .data
    2. msg:
    3. .ascii "Hello sysenter!\n"
    4. len = . - msg
    5. .text
    6. .globl _start
    7. _start:
    8. movl $len, %edx
    9. movl $msg, %ecx
    10. movl $1, %ebx
    11. movl $4, %eax
    12. # Setting the stack for the systenter
    13. pushl $sysenter_ret
    14. pushl %ecx
    15. pushl %edx
    16. pushl %ebp
    17. movl %esp, %ebp
    18. sysenter
    19. sysenter_ret:
    20. movl $0, %ebx
    21. movl $1, %eax
    22. # Setting the stack for the systenter
    23. pushl $sysenter_ret
    24. pushl %ecx
    25. pushl %edx
    26. pushl %ebp
    27. movl %esp, %ebp
    28. sysenter
    1. $ gcc -m32 -c sysenter.S
    2. $ ld -m elf_i386 -o sysenter sysenter.o
    3. $ strace ./sysenter
    4. execve("./sysenter", ["./sysenter"], 0x7fff73993fd0 /* 69 vars */) = 0
    5. strace: [ Process PID=7663 runs in 32 bit mode. ]
    6. write(1, "Hello sysenter!\n", 16Hello sysenter!
    7. ) = 16
    8. exit(0) = ?

    可以看到,为了使用 sysenter 指令,需要为其手动布置栈。这是因为在 sysenter 返回时,会执行 __kernel_vsyscall 的后半部分(从0xf7fd5059开始):

    1. Start End Perm Name
    2. 0xf7fd4000 0xf7fd6000 r-xp [vdso]
    3. gdb-peda$ disassemble __kernel_vsyscall
    4. Dump of assembler code for function __kernel_vsyscall:
    5. 0xf7fd5050 <+0>: push ecx
    6. 0xf7fd5051 <+1>: push edx
    7. 0xf7fd5052 <+2>: push ebp
    8. 0xf7fd5053 <+3>: mov ebp,esp
    9. 0xf7fd5055 <+5>: sysenter
    10. 0xf7fd5057 <+7>: int 0x80
    11. 0xf7fd5059 <+9>: pop ebp
    12. 0xf7fd505a <+10>: pop edx
    13. 0xf7fd505b <+11>: pop ecx
    14. 0xf7fd505c <+12>: ret
    15. End of assembler dump.

    __kernel_vsyscall 封装了 sysenter 调用的规范,是 vDSO 的一部分,而 vDSO 允许程序在用户层中执行内核代码。关于 vDSO 的内容我们将在后面的章节中细讲。

    下面是一个 64 位使用 syscall 的例子:

    1. .data
    2. msg:
    3. .ascii "Hello 64-bit!\n"
    4. len = . - msg
    5. .text
    6. .global _start
    7. _start:
    8. movq $1, %rdi
    9. movq $msg, %rsi
    10. movq $len, %rdx
    11. movq $1, %rax
    12. syscall
    13. xorq %rdi, %rdi
    14. movq $60, %rax
    15. syscall
    1. $ gcc -c hello64.S
    2. $ ld -o hello64 hello64.o
    3. $ strace ./hello64
    4. execve("./hello64", ["./hello64"], 0x7ffe11485290 /* 68 vars */) = 0
    5. write(1, "Hello 64-bit!\n", 14Hello 64-bit!
    6. ) = 14
    7. exit(0) = ?
    8. +++ exited with 0 +++

    在这两个例子中我们直接使用了 execvewriteexit 三个系统调用。但一般情况下,应用程序通过在用户空间实现的应用编程接口(API)而不是直接通过系统调用来编程。例如函数 的调用过程是这样的: