# bootloader如何加载bzImage

在进入start\_kernel之前，那真的是一片黑暗的路程。因为好多都是用汇编写的，对我来说简直就是抓瞎。

在[bzImage的全貌](/kernel-exploring/00_index/14_bzimage_whole_picture.md)中我们看过了make install时安装的bzImage的组成部分。而start\_kernel在这几个组成部分的最后一点。

```
                                     *
                                     | <-- vmlinux.lds.S
                                     |
                                   vmlinux
                                     |
                                     | <-- objdump
                                     |
                             arch/x86/boot/compressed/vmlinux.bin
                                     |
                                     | <-- compress
                                     |
                             arch/x86/boot/compressed/vmlinux.bin.zst
                                     |
                                     | <-- mkpiggy
                                     |
                             arch/x86/boot/compressed/piggy.S
                                     |
                                     |
                                     |  arch/x86/boot/compressed/*
       arch/x86/boot/*                \  /
              |                        \/
              | <-- setup.ld            | <-- vmlinux.lds
              |                         |
              |                         v
              |              arch/x86/boot/compressed/vmlinux
              |                         |
              |                         | <-- objcopy
              |                         |
              v                         v
    arch/x86/boot/setup.bin  arch/x86/boot/vmlinux.bin  
                   \         /
                    \       /
               arch/x86/boot/bzImage
```

这次我们就来看看被安装的内核是通过哪些步骤走到start\_kernel的。

从代码上看，内核加载后到start\_kernel前经历了下面的步骤。

**setup.bin**:

```
_start -> main -> go_to_protected_mode -> protected_mode_jump(boot_params.hdr.code32_start, )
```

**vmlinux.bin**:

```
startup_32 -> startup_64 -> extract_kernel ->
```

**vmlinux**:

```
startup_64 -> initial_code -> x86_64_start_kernel -> x86_64_start_reservations
```

但是真的是这样吗？如何可以确认呢？经过一番研究，发现第一步要确认的是bzImage被bootloader加载到了哪里。

幸好我们知道bochs模拟器，那就请出他来确认一下整个流程吧。

## 安装bochs

bochs是一个x86的模拟器，据说还能运行win98。而且调试友好，在《自己动手写操作系统》一书中就是用bochs来运行手写的操作系统的。这里我们就要再次请出它来帮助我们了解start\_kernel之前的黑暗世界。

```
sudo apt install bochs
sudo apt install bochs-x
```

在ubuntu上运行这两个命令就能安装bochs了。记得安装bochs-x，否则会报错。

## 准备启动镜像

其实x86内核编译里有制作启动盘的目标，包括了软盘、光盘、硬盘。这里我们只用光盘。

```
make isoimage
```

另外记得安装个依赖

```
sudo apt install syslinux_utils
sudo apt install isolinux
```

执行这条命令就可以生成arch/x86/boot/image.iso启动光盘，其中包含了当前目录编译出的最新kernel。 但是这个命令有个问题，不确定是不是内核开发遗漏了，一定要加上这个改动才能制作成功。

```
diff --git a/arch/x86/boot/Makefile b/arch/x86/boot/Makefile
index 3cece19b7473..8b178eded5bc 100644
--- a/arch/x86/boot/Makefile
+++ b/arch/x86/boot/Makefile
@@ -117,7 +117,7 @@ $(obj)/compressed/vmlinux: FORCE
 # bzdisk/fdimage/hdimage/isoimage kernel
 FDARGS =
 # Set this if you want one or more initrds included in the image
-FDINITRD =
+FDINITRD = /boot/initrd.img-6.0.0-rc4yw+
 
 imgdeps = $(obj)/bzImage $(obj)/mtools.conf $(src)/genimage.sh
```

就是一定要指定根文件才能制作。等有空了我问问内核社区这是几个意思。

## bochs配置文件

有了镜像，bochs也装好了，接下来我们就可以启动了。

可以参考下面的配置，

```
###############################################################
# Configuration file for Bochs
###############################################################

# how much memory the emulated machine will have
megs: 128

# filename of ROM images
romimage: file=/usr/share/bochs/BIOS-bochs-latest
vgaromimage: file=/usr/share/vgabios/vgabios.bin

# what disk images will be used
ata0-slave:  type=cdrom, path="image.iso", status=inserted

# choose the boot disk.
boot: cdrom

# where do we send log messages?
# log: bochsout.txt

# disable the mouse
mouse: enabled=0

# enable key mapping, using US layout as default.
#keyboard_mapping: enabled=1, map=/usr/share/bochs/keymaps/x11-pc-us.map
```

我是把image.iso放在配置文件同一个目录的，大家可以根据自己习惯调整。

然后，启动，运行！

```
bochs -f bochsrc
```

## 内核加载到了哪里？

一切的都很顺利？但是说好的调试呢？断点在哪里设置？什么时候进入的保护模式？

好像我们什么都不知道。我们想要的是**内核究竟被加载到哪里了**。这样我们才能设置断点，然后调试查看。

这时候我突然想到了内核文档，说不定文档里会有写呢？别说，我还真找到一个文档[boot.rst](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/Documentation/arch/x86/boot.rst?h=v6.7\&id=0dd3ee31125508cd67f7e7172247f05b7fd1753a)。人是这么说的：

```
For a modern bzImage kernel with boot protocol version >= 2.02, a
memory layout like the following is suggested::

		~                        ~
		|  Protected-mode kernel |
	100000  +------------------------+
		|  I/O memory hole	 |
	0A0000	+------------------------+
		|  Reserved for BIOS	 |	Leave as much as possible unused
		~                        ~
		|  Command line		 |	(Can also be below the X+10000 mark)
	X+10000	+------------------------+
		|  Stack/heap		 |	For use by the kernel real-mode code.
	X+08000	+------------------------+
		|  Kernel setup		 |	The kernel real-mode code.
		|  Kernel boot sector	 |	The kernel legacy boot sector.
	X       +------------------------+
		|  Boot loader		 |	<- Boot sector entry point 0000:7C00
	001000	+------------------------+
		|  Reserved for MBR/BIOS |
	000800	+------------------------+
		|  Typically used by MBR |
	000600	+------------------------+
		|  BIOS use only	 |
	000000	+------------------------+
```

实模式的内核加载地址是个X，这有点头大。那究竟是哪里呢？

回忆一下《自己动手写操作系统》，在开机上电到内核运行经历了这么几个步骤：

* 系统先运行BIOS
* 由BIOS找到boot sector
* boot sector加载loader
* loader加载内核，并跳转

所以在内核运行前，还有几个步骤要执行。在我们制作出的启动镜像里，这个loader是syslinux完成的。还记得我们制作镜像时安装的依赖么？具体可以看arch/x86/boot/genimage.sh中geniso函数。想进一步了解syslinux的，可以参考[Syslinux Tutorial](https://tool.frogg.fr/Tutorial_Syslinux)。

所以决定内核加载到哪里的，是syslinux决定的。既然都是开源代码，那就。。。看代码吧。

在此跳过细节，直接给出结果。在我编译的内核情况下，实模式内核加载到了**0x10000**，保护模式内核加载到了**0x100000**。想要看看syslinux的，可以在[syslinux](https://github.com/RichardWeiYang/syslinux/tree/debug-kernel-load)上找到我定位内核加载地址的代码。

## 确认内核加载地址

知道了内核加载到哪里，我们就可以在对应的地址设置断点，来确认这个发现是不是真的。

```
pb 0x10000
pb 0x100000
```

但是一直看不到停在实模式内核代码上，这是为什么呢？看了代码想起来了，原来实模式内核的第一个扇区是一个引导盘。实际有功效的代码是在512字节后。所以syslinux是直接条到这里开始的么？

那我们就把断点调整以下，看看效果。

```
pb 0x10200
pb 0x100000
```

怎么样，当你看到在断点停下来的时候，是不是很激动人心！（断点有两次会停在bootloader里，所以前两次的忽略。）

下面上调试的实际结果，来感受以下。

### 反汇编实模式内核代码

```
(0) Breakpoint 1, 0x0000000000010200 in ?? ()
Next at t=135209726
```

实模式内核加载地址+偏移512的断点触发了。确认当前地址是0x10200。

```
<bochs:7> creg
CR0=0x60000010: pg CD NW ac wp ne ET ts em mp pe
CR2=page fault laddr=0x0000000000000000
CR3=0x0000000000000000
    PCD=page-level cache disable=0
    PWT=page-level write-through=0
CR4=0x00000000: smep osxsave pcid fsgsbase smx vmx osxmmexcpt osfxsr pce pge mce pae pse de tsd pvi vme
CR8: 0x0
EFER=0x00000000: ffxsr nxe lma lme sce
```

查看当前寄存器，确认目前在实模式，CR0的pe是小写。页表也没有打开pg是小写。

```
<bochs:9> u /5
00010200: (                    ): jmp .+106                 ; eb6a
00010202: (                    ): dec ax                    ; 48
00010203: (                    ): jb .+83                   ; 647253
00010206: (                    ): lar ax, word ptr ds:[bx+si] ; 0f0200
00010209: (                    ): add byte ptr ds:[bx+si], al ; 0000
<bochs:10> n
Next at t=135209727
(0) [0x000000000001026c] 1020:006c (unk. ctxt): mov ax, ds                ; 8cd8
<bochs:11> u /20
0001026c: (                    ): mov ax, ds                ; 8cd8
0001026e: (                    ): mov es, ax                ; 8ec0
00010270: (                    ): cld                       ; fc
00010271: (                    ): mov dx, ss                ; 8cd2
00010273: (                    ): cmp dx, ax                ; 39c2
00010275: (                    ): mov dx, sp                ; 89e2
00010277: (                    ): jz .+22                   ; 7416
00010279: (                    ): mov dx, 0x53a0            ; baa053
0001027c: (                    ): test byte ptr ds:0x211, 0x80 ; f606110280
00010281: (                    ): jz .+4                    ; 7404
00010283: (                    ): mov dx, word ptr ds:0x224 ; 8b162402
00010287: (                    ): add dx, 0x0400            ; 81c20004
0001028b: (                    ): jnb .+2                   ; 7302
0001028d: (                    ): xor dx, dx                ; 31d2
0001028f: (                    ): and dx, 0xfffc            ; 83e2fc
00010292: (                    ): jnz .+3                   ; 7503
00010294: (                    ): mov dx, 0xfffc            ; bafcff
00010297: (                    ): mov ss, ax                ; 8ed0
00010299: (                    ): movzx esp, dx             ; 660fb7e2
0001029d: (                    ): sti                       ; fb
```

此时赶紧打开arch/x86/boot/header.S确认一下反汇编的结果。

```
	# offset 512, entry point

	.globl	_start
_start:
		# Explicitly enter this as bytes, or the assembler
		# tries to generate a 3-byte jump here, which causes
		# everything else to push off to the wrong offset.
		.byte	0xeb		# short (2-byte) jump
		.byte	start_of_setup-1f
1:

    ...

	.section ".entrytext", "ax"
start_of_setup:
# Force %es = %ds
	movw	%ds, %ax
	movw	%ax, %es
	cld

# Apparently some ancient versions of LILO invoked the kernel with %ss != %ds,
# which happened to work by accident for the old code.  Recalculate the stack
# pointer if %ss is invalid.  Otherwise leave it alone, LOADLIN sets up the
# stack behind its own code, so we can't blindly put it directly past the heap.

	movw	%ss, %dx
	cmpw	%ax, %dx	# %ds == %ss?
	movw	%sp, %dx
	je	2f		# -> assume %sp is reasonably set

	# Invalid %ss, make up a new stack
	movw	$_end, %dx
	testb	$CAN_USE_HEAP, loadflags
	jz	1f
	movw	heap_end_ptr, %dx
1:	addw	$STACK_SIZE, %dx
	jnc	2f
	xorw	%dx, %dx	# Prevent wraparound

2:	# Now %dx should point to the end of our stack space
	andw	$~3, %dx	# dword align (might as well...)
	jnz	3f
	movw	$0xfffc, %dx	# Make sure we're not zero
3:	movw	%ax, %ss
	movzwl	%dx, %esp	# Clear upper half of %esp
	sti			# Now we should have a working stack
```

实模式内核512偏移处先是一个jmp，接下来20条指令和bochs中反汇编的是一模一样啊。这不就是咱要找的吗！

### 反汇编保护模式内核代码

已经看到了实模式内核的真容，那接下来就看看保护模式的内核吧。

```
<bochs:12> c
(0) Breakpoint 2, 0x0000000000100000 in ?? ()
Next at t=135456615
(0) [0x0000000000100000] 0010:0000000000100000 (unk. ctxt): cld                       ; fc
<bochs:13> creg
CR0=0x60000011: pg CD NW ac wp ne ET ts em mp PE
CR2=page fault laddr=0x0000000000000000
CR3=0x0000000000000000
    PCD=page-level cache disable=0
    PWT=page-level write-through=0
CR4=0x00000000: smep osxsave pcid fsgsbase smx vmx osxmmexcpt osfxsr pce pge mce pae pse de tsd pvi vme
CR8: 0x0
EFER=0x00000000: ffxsr nxe lma lme sce
```

我们直接continue后，就停在了0x100000的地址。这个就是我们刚才设置保护模式内核的断点地址。

查看寄存器，此时保护模式确实已经打开，CR0的PE是大写的。不过页表还没有开启。

```
<bochs:14> u /20
00100000: (                    ): cld                       ; fc
00100001: (                    ): cli                       ; fa
00100002: (                    ): lea esp, dword ptr ds:[esi+488] ; 8da6e8010000
00100008: (                    ): call .+0                  ; e800000000
0010000d: (                    ): pop ebp                   ; 5d
0010000e: (                    ): sub ebp, 0x0000000d       ; 83ed0d
00100011: (                    ): lea eax, dword ptr ss:[ebp+10657808] ; 8d8510a0a200
00100017: (                    ): mov dword ptr ds:[eax+2], eax ; 894002
0010001a: (                    ): lgdt ds:[eax]             ; 0f0110
0010001d: (                    ): mov eax, 0x00000018       ; b818000000
00100022: (                    ): mov ds, ax                ; 8ed8
00100024: (                    ): mov es, ax                ; 8ec0
00100026: (                    ): mov fs, ax                ; 8ee0
00100028: (                    ): mov gs, ax                ; 8ee8
0010002a: (                    ): mov ss, ax                ; 8ed0
0010002c: (                    ): lea esp, dword ptr ss:[ebp+10682368] ; 8da50000a300
00100032: (                    ): push 0x00000008           ; 6a08
00100034: (                    ): lea eax, dword ptr ss:[ebp+60] ; 8d853c000000
0010003a: (                    ): push eax                  ; 50
0010003b: (                    ): retf                      ; cb
```

反汇编一下代码，我们继续来看看是不是我们期待的。

这时候要看哪里的代码呢？对了，是arch/x86/boot/compressed/head\_64.S。其中startup\_32就是我们要找的。

```
	.code32
SYM_FUNC_START(startup_32)
	/*
	 * 32bit entry is 0 and it is ABI so immutable!
	 * If we come here directly from a bootloader,
	 * kernel(text+data+bss+brk) ramdisk, zero_page, command line
	 * all need to be under the 4G limit.
	 */
	cld
	cli

	leal	(BP_scratch+4)(%esi), %esp
	call	1f
1:	popl	%ebp
	subl	$ rva(1b), %ebp

	/* Load new GDT with the 64bit segments using 32bit descriptor */
	leal	rva(gdt)(%ebp), %eax
	movl	%eax, 2(%eax)
	lgdt	(%eax)

	/* Load segment registers with our descriptors */
	movl	$__BOOT_DS, %eax
	movl	%eax, %ds
	movl	%eax, %es
	movl	%eax, %fs
	movl	%eax, %gs
	movl	%eax, %ss

	/* Setup a stack and load CS from current GDT */
	leal	rva(boot_stack_end)(%ebp), %esp

	pushl	$__KERNEL32_CS
	leal	rva(1f)(%ebp), %eax
	pushl	%eax
	lretl
```

startup\_32开始的20条指令和反汇编里显示的是不是也是一模一样？那就说明我们又找对啦。

至此，我们已经做好了用bochs探索内核进入start\_kernel前的准备。感觉就像那黑暗的隧道里，照进了光。

PS： 感谢《自己动手写操作系统》，没有它我可能还在黑暗中摸索。


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://richardweiyang.gitbook.io/kernel-exploring/00_index-2/02_how_bzimage_loaded.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
