前面我们已经进入到了 SeaBIOS 做了一个长跳转回到了实模式可寻地址里。回到 seabios/src/romlayout.S 文件:
1
2
3
4
|
ORG 0xfff0 // Power-up Entry Point
.global reset_vector
reset_vector:
ljmpw $SEG_BIOS, $entry_post
|
entry_post
这里长跳转 ljmpw 不仅跳转到新的偏移地址,还会加载新的段选择子。
一般的跳转指令是 jmp 这里加的前缀 l 表示长跳转,w 表示操作的偏移地址宽度是 16 位,也就是将 SEG_BIOS 加载到 cs 段寄存器然后将 entry_post 加载到 ip寄存器。开始执行该段代码。
entry_post 还在 romlayout.s 里:
1
2
3
4
|
entry_post:
cmpl $0, %cs:HaveRunPost // Check for resume/reboot
jnz entry_resume
ENTRY_INTO32 _cfunc32flat_handle_post // Normal entry point
|
首先检查 cs:HaveRunPost 该变量,cs:HaveRunPost 用于判断系统是否已经完成了加点自检的阶段。如果已经完成也就是设备是从睡眠休眠的方式启动,这样会跳过硬件检测继续运行。
不管 entry_resume,我们看最后一行:ENTRY_INTO32 _cfunc32flat_handle_post,ENTRY_INTO32 实际是一个汇编宏,定义在 seabios/src/entryfuncs.s 内:
1
2
3
4
5
6
7
|
.macro ENTRY_INTO32 cfunc
xorw %dx, %dx
movw %dx, %ss
movl $ BUILD_STACK_ADDR , %esp
movl $ \cfunc , %edx
jmp transition32
.endm
|
这里从 16 位实模式过渡到 32 位保护模式,前面做了一系列的寄存器修改复制。首先清零 dx 寄存器,之后赋值给 ss (栈段寄存器),这样清空栈段寄存器,之后栈位置的实际地址就都由 esp 寄存器确定,因为段寄存器已经置零。
1
|
movl $ BUILD_STACK_ADDR , %esp
|
这里 BUILD_STACK_ADDR 是 SeaBIOS 链接时候确定的地址,我还不了解 SeaBIOS 的链接过程的地址分配,先不展开。反正就是 BUILD_STACK_ADDR 的地址是专门分配用作 32 位栈空间用的,之后将函数指针赋值给 edx 寄存器,从上面我们知道这里的 cfunc 是 _cfunc32flat_handle_post 这个函数。(还是得扯到 SeaBIOS 的链接过程………………)
因为 SeaBIOS 是混合内存模式,同一套 C 语言代码需要编译成三种模式:(16 位实模式、32 位分段模式和 32 为扁平模式)以此来满足不同内存模式的函数调用。这里的 _cfunc32flat_handle_post 就是 handle_post C 函数代码编译成 32 位扁平模式的函数,前面的 _cfunc32flat_ 就是前缀,和 C++ 为了多态的各种前缀有点像哈。
将函数指针赋值给 edx 寄存器后就跳转到 transition32 label 开始从实模式向保护模式转移。
这里又用 grep -rn 'transition32' * 查询定义发现又跳回 romlayout.S 里了……
transition32
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
|
DECLFUNC transition32
.global transition32_nmi_off
transition32:
// Disable irqs (and clear direction flag)
cli
cld
// Disable nmi
movl %eax, %ecx
movl $CMOS_RESET_CODE|NMI_DISABLE_BIT, %eax
outb %al, $PORT_CMOS_INDEX
inb $PORT_CMOS_DATA, %al
// enable a20
inb $PORT_A20, %al
orb $A20_ENABLE_BIT, %al
outb %al, $PORT_A20
movl %ecx, %eax
transition32_nmi_off:
// Set segment descriptors
lidtw %cs:pmode_IDT_info
lgdtw %cs:rombios32_gdt_48
// Enable protected mode
movl %cr0, %ecx
andl $~(CR0_PG|CR0_CD|CR0_NW), %ecx
orl $CR0_PE, %ecx
movl %ecx, %cr0
// start 32bit protected mode code
ljmpl $SEG32_MODE32_CS, $(BUILD_BIOS_ADDR + 1f)
.code32
// init data segments
1: movl $SEG32_MODE32_DS, %ecx
movw %cx, %ds
movw %cx, %es
movw %cx, %ss
movw %cx, %fs
movw %cx, %gs
jmpl *%edx
|
很清晰的实模式到保护模式跳转,我在 x86 从实模式到保护模式 博文中已经有介绍,但是 SeaBIOS 的代码非常清晰包括 gdt 选择子写得也很不错,而且还是用 C 语言实现的,这里就再详细写下。
关闭 INTR、清除方向标志
关闭可屏蔽中断(INTR) 确保切换过程绝对不能被打断。清除方向标志使得操作从低地址向高地址处理和 C 语言环境一致。
关闭 NMI
1
2
3
4
|
movl %eax, %ecx
movl $CMOS_RESET_CODE|NMI_DISABLE_BIT, %eax
outb %al, $PORT_CMOS_INDEX
inb $PORT_CMOS_DATA, %al
|
NMI 是非屏蔽中断,通过 outb 向 CMOS 端口写入关闭 NMI 的位来关闭 NMI。
写入后还需要做一次inb 对数据端口进行读写操作才能将 NMI 的控制位锁存到内部电路中真正启用,关闭 NMI。
开启 A20 地址线
1
2
3
4
|
inb $PORT_A20, %al
orb $A20_ENABLE_BIT, %al
outb %al, $PORT_A20
movl %ecx, %eax
|
读取 系统控制端口(0x92) 当前状态值到 al 寄存器,然后对其与 A20_ENABLE_BIT (0x2) 按位或操作,也就是将 bit1 置 1,该位置就是 A20 的控制位。
之后用 outb 往 PORT_A20 端口写入一字节数据,也就是 al,将 A20 控制位打开后的状态写入,最后恢复之前使用的 eax 寄存器。
设置 IDT
1
|
lidtw %cs:pmode_IDT_info
|
IDT 是中断描述符表,定义在 seabios/src/misc.c 内:
1
2
3
4
5
6
7
8
9
|
// Dummy IDT that forces a machine shutdown if an irq happens in
// protected mode.
u8 dummy_IDT VARFSEG;
// Protected mode IDT descriptor
struct descloc_s pmode_IDT_info VARFSEG = {
.length = sizeof(dummy_IDT) - 1,
.addr = (u32)&dummy_IDT,
};
|
不过在这里发挥不了作用,是 “Dummy” 的。
设置 GDT
1
|
lgdtw %cs:rombios32_gdt_48
|
GDT 是全局描述符表,描述了内存段的信息。也定义在 seabios/src/misc.c 中。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
// GDT
u64 rombios32_gdt[] VARFSEG __aligned(8) = {
// First entry can't be used. 强制占位
0x0000000000000000LL,
// 32 bit flat code segment (SEG32_MODE32_CS)
GDT_GRANLIMIT(0xffffffff) | GDT_CODE | GDT_B,
// 32 bit flat data segment (SEG32_MODE32_DS)
GDT_GRANLIMIT(0xffffffff) | GDT_DATA | GDT_B,
// 16 bit code segment base=0xf0000 limit=0xffff (SEG32_MODE16_CS)
GDT_LIMIT(BUILD_BIOS_SIZE-1) | GDT_CODE | GDT_BASE(BUILD_BIOS_ADDR),
// 16 bit data segment base=0x0 limit=0xffff (SEG32_MODE16_DS)
GDT_LIMIT(0x0ffff) | GDT_DATA,
// 16 bit code segment base=0xf0000 limit=0xffffffff (SEG32_MODE16BIG_CS)
GDT_GRANLIMIT(0xffffffff) | GDT_CODE | GDT_BASE(BUILD_BIOS_ADDR),
// 16 bit data segment base=0 limit=0xffffffff (SEG32_MODE16BIG_DS)
GDT_GRANLIMIT(0xffffffff) | GDT_DATA,
};
// GDT descriptor
struct descloc_s rombios32_gdt_48 VARFSEG = {
.length = sizeof(rombios32_gdt) - 1,
.addr = (u32)rombios32_gdt,
};
|
这里就定义了好几个内存段,仔细看注释还是能看懂的,不细展开。
设置 CRO 开启保护模式
1
2
3
4
5
|
// Enable protected mode
movl %cr0, %ecx
andl $~(CR0_PG|CR0_CD|CR0_NW), %ecx
orl $CR0_PE, %ecx
movl %ecx, %cr0
|
将 cr0 寄存器的数据赋值给 ec0 寄存器,然后清除 PG、CD、NW 位
这里直接贴 deepseek 的解释吧:

orl $CRO_PE, $ecx 设置保护模式位,确定打开保护模式然后将修改后的数据覆盖 cr0 寄存器。
到目前为止就完成了进入保护模式的准备过程。
长跳转到 32 位代码段
1
|
ljmpl $SEG32_MODE32_CS, $(BUILD_BIOS_ADDR + 1f)
|
长跳转到 32 位代码段,并执行当前标签为 1 的代码。
设置 32 位数据段
1
2
3
4
5
6
|
1: movl $SEG32_MODE32_DS, %ecx
movw %cx, %ds
movw %cx, %es
movw %cx, %ss
movw %cx, %fs
movw %cx, %gs
|
这里就是标签为 1 的位置。实际就是将 32 位平坦数据段选择子通过 ecx 赋值给 32 位平坦数据段寄存器。
跳转 C 函数
还记得在最开始 edx 存了什么吗?最开始的汇编宏 ENTRY_INTO32 将 _cfunc32flat_handle_post 函数地址放入了 edx,现在这段代码就要跳转到 handle_post 里。我们得以逃离汇编语言,走向 C 语言!
handle_post
handle_post(void) 函数定义在 /seabios/src/post.c 中。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
void VISIBLE32FLAT
handle_post(void)
{
if (!CONFIG_QEMU && !CONFIG_COREBOOT)
return;
serial_debug_preinit();
debug_banner();
// Check if we are running under Xen.
xen_preinit();
// Allow writes to modify bios area (0xf0000)
make_bios_writable();
// Now that memory is read/writable - start post process.
dopost();
}
|
前面的 VISIBLE32FLAT 宏用于告诉编译器 handle_post 将编译成 32 位平坦代码段。
handle_post 只有几行函数调用,前面用于确定是否开启串口调试,还有检查是否在 xen 下。
这里的 make_bios_writable() 将 BIOS 区域设置为可写,让我们再一次回到 qemu,回到将 bios 放入内存段的时候:
1
2
|
// qemu/hw/i386/x86-common.c load_bios_from_file
memory_region_set_readonly(&x86ms->bios, !isapc_ram_fw); // 将 BIOS 内存区域设为只读
|
也就是在最开始,bios 区域的内容都是只读的。但是在进入 bios 之后需要对其的变量,数据段进行读写,此时需要做一些 trick 达到这一目的。
这个函数主要是通过 shadow memory 技术来实现的。具体不展开,反正结果就是我们可以读写 bios 的数据和栈段了。
该函数使得 0xc0000 ~ 0xfffff 可读写,接下来就调用 dopost() 进行代码的重定位并继续初始化。
dopost
handle_post 程序最后调用 dopost 函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
void VISIBLE32INIT
dopost(void)
{
code_mutable_preinit();
// Detect ram and setup internal malloc.
qemu_preinit();
coreboot_preinit();
malloc_preinit(); // 初始化内存分配器
// Relocate initialization code and call maininit().
reloc_preinit(maininit, NULL);
}
|
dopost 彻底的将 BIOS 从 ROM 中移动到 RAM。将 bios 从 ISA 地址的顶端到更低地址的合适区域。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
code_mutable_preinit();
void
code_mutable_preinit(void)
{
if (HaveRunPost)
// Already run
return;
// Setup reset-vector entry point (controls legacy reboots).
rtc_write(CMOS_RESET_CODE, 0);
barrier();
HaveRunPost = 1;
barrier();
}
|
在 dopost 调用时,计算机的初始化已经基本完成,可以确定此时已经上电完成。
code_mutable_preinit(void) 用于标记 POST 已执行。其通过将 CMOS 的 CMOS_RESET_CODE 位置 0 。
下面的两次 barrier() 也是 trick 。
1
|
#define barrier() __asm__ __volatile__("": : :"memory")
|
没有任何汇编代码,但是用于告诉编译器做了内存修改不能对指令顺序重排,防止编译器优化。
最后将 HaveRunPost 赋值为 1。再一次 barrier 后回到 dopost。
1
2
3
|
qemu_preinit();
coreboot_preinit();
malloc_preinit();
|
根据特定平台做初始化之后初始化内存分配器,这里就暂不展开了。
1
|
reloc_preinit(maininit, NULL);
|
reloc_preinit 重定位初始化代码并调用主初始化函数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
void __noreturn
reloc_preinit(void *f, void *arg)
{
void (*func)(void *) __noreturn = f;
if (!CONFIG_RELOCATE_INIT)
func(arg);
// Allocate space for init code.
u32 initsize = SYMBOL(code32init_end) - SYMBOL(code32init_start);
u32 codealign = SYMBOL(_reloc_min_align);
void *codedest = memalign_tmp(codealign, initsize);
void *codesrc = VSYMBOL(code32init_start);
if (!codedest)
panic("No space for init relocation.\n");
// Copy code and update relocs (init absolute, init relative, and runtime)
dprintf(1, "Relocating init from %p to %p (size %d)\n"
, codesrc, codedest, initsize);
s32 delta = codedest - codesrc;
memcpy(codedest, codesrc, initsize);
updateRelocs(codedest, VSYMBOL(_reloc_abs_start), VSYMBOL(_reloc_abs_end)
, delta);
updateRelocs(codedest, VSYMBOL(_reloc_rel_start), VSYMBOL(_reloc_rel_end)
, -delta);
updateRelocs(VSYMBOL(code32flat_start), VSYMBOL(_reloc_init_start)
, VSYMBOL(_reloc_init_end), delta);
if (f >= codesrc && f < VSYMBOL(code32init_end))
func = f + delta;
// Call function in relocated code.
barrier();
func(arg);
}
|
函数有点复杂。
1
|
void (*func)(void *) __noreturn = f;
|
声明一个函数指针 func,指向 f 函数。
1
2
|
if (!CONFIG_RELOCATE_INIT)
func(arg);
|
CONFIG_RELOCATE_INIT 判断是否需要重定位,如果不要就不做下面的代码,直接调用 func(arg)。
1
2
3
4
|
u32 initsize = SYMBOL(code32init_end) - SYMBOL(code32init_start); // 计算 code32init 段大小
u32 codealign = SYMBOL(_reloc_min_align); // 确定重定位所需最小的对齐值
void *codedest = memalign_tmp(codealign, initsize); // 寻找到合适的内存块
void *codesrc = VSYMBOL(code32init_start); // 将 code32init_start 转换成 void *
|
code32init_start 这类变量被声明是 extern char[] 但是这类实际上是连接器定义的地址标记符号,用于配合链接的,好神奇,好抽象……
计算新旧地址的偏移量:
1
|
s32 delta = codedest - codesrc;
|
将初始化代码(.init) 段从 codesrc 复制到 codedest。
1
|
memcpy(codedest, codesrc, initsize);
|
修正地址引用:
1
2
3
4
5
6
|
updateRelocs(codedest, VSYMBOL(_reloc_abs_start), VSYMBOL(_reloc_abs_end)
, delta); // 修正 init 段内绝对地址的引用
updateRelocs(codedest, VSYMBOL(_reloc_rel_start), VSYMBOL(_reloc_rel_end)
, -delta); // 修正 init 段内相对地址的引用
updateRelocs(VSYMBOL(code32flat_start), VSYMBOL(_reloc_init_start)
, VSYMBOL(_reloc_init_end), delta); // 修正运行时段对 init 段的引用
|
调整目标函数指针:
1
2
|
if (f >= codesrc && f < VSYMBOL(code32init_end))
func = f + delta;
|
确保内存修改后调用目标函数,这里的目标函数就是 maininit。
1
2
|
barrier();
func(arg);
|
进入 maininit
到这一步,终于到主初始化函数了,前期工作才刚刚完成。 init 段迁移到新的内存段, runtime 段代码还永远地留在原处。
这一步就要按顺序一步步初始化所有子系统:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
|
static void
maininit(void)
{
// Initialize internal interfaces.
interface_init();
// Setup platform devices.
platform_hardware_setup();
// Start hardware initialization (if threads allowed during optionroms)
if (threads_during_optionroms())
device_hardware_setup();
// Run vga option rom
vgarom_setup();
sercon_setup();
enable_vga_console();
// Do hardware initialization (if running synchronously)
if (!threads_during_optionroms()) {
device_hardware_setup();
wait_threads();
}
// Run option roms
optionrom_setup();
// Allow user to modify overall boot order.
interactive_bootmenu();
wait_threads();
// Prepare for boot.
prepareboot();
// Write protect bios memory.
make_bios_readonly();
// Invoke int 19 to start boot process.
startBoot();
}
|
看着好复杂,但回头望去我们已经走好远了。已经到达 32 位实模式,虽然仅仅将 bios.bin 加载完毕……

参考资料
- seabios/docs/Linking_overview.md
- 中断描述符表(Interrupt Descriptor Table,IDT)
- SeaBIOS实现简单分析
演示:本讲内容速览
下面是本讲的 Reveal.js 演示,覆盖从 reset vector 到 32 位保护模式、再到 maininit
的整条调用链。右上角可切换深浅主题,按 → / ← 翻页。
小提示:点击右上角 ◐ 主题 在深色 / 浅色 / 跟随系统间切换;
按 S 打开演讲者视图。