bootloader如何加载bzImage
在进入start_kernel之前,那真的是一片黑暗的路程。因为好多都是用汇编写的,对我来说简直就是抓瞎。
在bzImage的全貌中我们看过了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。人是这么说的:
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。
所以决定内核加载到哪里的,是syslinux决定的。既然都是开源代码,那就。。。看代码吧。
在此跳过细节,直接给出结果。在我编译的内核情况下,实模式内核加载到了0x10000,保护模式内核加载到了0x100000。想要看看syslinux的,可以在syslinux上找到我定位内核加载地址的代码。
确认内核加载地址
知道了内核加载到哪里,我们就可以在对应的地址设置断点,来确认这个发现是不是真的。
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: 感谢《自己动手写操作系统》,没有它我可能还在黑暗中摸索。
Last updated
Was this helpful?