Introduction

回顾 Hello World

// hello.c
#include <stdio.h>
int main(){
   printf("Hello, World\n");
   return 0;
}

使用 objdump -d a.out 查看 gcc 编译出来的结果,会发现编译出来的文件一点也不小(使用 gcc -static.c 会导致更大的可执行文件)。

控制编译流程

让我们删掉一些东西,看看会不会影响我们的程序:首先,#include <stdio.h> 引入了太多代码,先删除。其次,使用 gcc -c hello.c 编译源代码生成目标文件 hello.o (预处理、编译和汇编)。然后,使用 ld hello.o 直接链接,会发现找不到 puts (这就是 gcc 在背后帮我们做的事情)。

删除 printf 再看看,此时可以生成 a.out。然而,在运行时产生了 Segementation Fault。

使用 GDB 在运行时调试程序 (a GDB cheetsheet):

  • starti: 从第一条指令开始执行
  • layout asm: 显示汇编代码布局
  • info registers: 查看寄存器的值
  • si: 单步执行
  • p: 查看某个变量的值
  • x: 查看某个内存地址中的值

系统调用和最小 Hello World

用户程序只能通过系统调用 (以操作系统所允许的方式) 来向操作系统请求服务。在最小的 Hello World 中,我们仅需要请求两个服务:write()exit()

// hello.S
.section .data
msg:    .asciz "Hello World\n" ; 

.section .text
.globl _start

_start:
  movq $1,   %rax   // write system call
  movq $1,   %rdi   // file descriptor
  movq $msg, %rsi   // pointer to message
  movq $12,  %rdx   // message length
  syscall           

  movq $60,  %rax   // exit system call
  movq $0,   %rdi   // return number
  syscall

运行上述代码:

cpp hello.S > hello.i
as hello.i -o hello.o
ld hello.o
./a.out

我们可以通过 strace 来追踪一个应用程序在执行过程中产生的系统调用:

strace ./a.out

CPU Reset 之后的世界

为了让操作系统这个程序能够正确启动,计算机硬件和程序员之间首先约定好 CPU Reset 后的寄存器状态 (例如在 x86 family 中 EIP = 0xfff0),随后由固件 (Firmware) 来将程序员编写的代码载入内存并执行。例如,BIOS 会将第一个可引导设备的第一个 512 字节 (Master Boot Record, MBR) 加载到物理内存的 0x7c00 位置,并规定 CS:IP = 0x7c00。此时,虽然 MBR 只能容纳 400 多字节代码,但系统的控制权已回到程序员手中。

// mbr.S
#define SECT_SIZE  512

.code16  // 16-bit assembly

// Entry of the code
.globl _start
_start:
  lea   (msg), %si   // R[si] = &msg;

again:
  movb  (%si), %al   // R[al] = *R[si]; <--+
  incw  %si          // R[si]++;           |
  orb   %al, %al     // if (!R[al])        |
  jz    done         //   goto done; --+   |
  movb  $0x0e, %ah   // R[ah] = 0x0e;  |   |
  movb  $0x00, %bh   // R[bh] = 0x00;  |   |
  int   $0x10        // bios_call();   |   |
  jmp   again        // goto again; ---+---+

done:                //                |
  jmp   .            // goto done; <---+

// Data: const char msg[] = "...";
msg:
  .asciz "This is a baby step towards operating systems!\r\n"

// Magic number for bootable device
.org SECT_SIZE - 2
.byte 0x55, 0xAA
# Makefile
mbr.img: mbr.S
	gcc -ggdb -c $<
	ld mbr.o -Ttext 0x7c00
	objcopy -S -O binary -j .text a.out $@

run: mbr.img
	qemu-system-x86_64 --nographic $<

# Run QEMU in background
#  -s: open a gdb server on TCP port 1234
#  -S: do not start CPU at startup
debug: mbr.img
	qemu-system-x86_64 -s -S --nographic $< &  

clean:
	rm -f *.img *.o a.out

可以通过 make 来将上述 mbr.S 编译并转换为磁盘镜像文件 mbr.img,随后运行 make run 即可在 QEMU 中启动并执行 mbr.S 代码 (利用 bios_call 打印字符到屏幕)。当然,真实的 MBR 不是用来打印 Hello World 的,而是用于加载磁盘上的操作系统。

我们同样可以通过 gdb 来在 QEMU 之上观察 mbr.S 代码的执行。首先运行 make debug 在后台启动 QEMU,随后运行 gbd 并通过 target remote localhost:1234 连接 QEMU 服务器进行调试。

为了方便调试,也可以编写脚本来自动化调试中的一些步骤:gdb -x init.gdb

# init.gdb
# kill QEMU on gdb exits
define hook-quit
  kill
end

target remote localhost:1234
file a.out
break *0x7c00
continue
layout src