Kernel Exploring
  • 前言
  • 支持
  • 老司机带你探索内核编译系统
    • 编译出你的第一个内核
    • 内核编译中的小目标
    • 可能是kbuild中最直接的小目标 – help
    • 使用了一个kbuild函数的目标 – cscope
    • 内核中单个.o文件的编译过程
    • 根目录vmlinux的编译过程
    • 启动镜像bzImage的前世今生
    • setup.bin的诞生记
    • 真假vmlinux–由vmlinux.bin揭开的秘密
    • bzImage的全貌
    • kbuild系统浅析
  • 启动时的小秘密
    • INIT_CALLS的秘密
    • 内核参数
  • 内核加载全流程
    • bootloader如何加载bzImage
    • 内核压缩与解压
    • 内核加载的几个阶段
    • 保护模式内核代码赏析
  • 内存管理
    • 内核页表成长记
      • 未解压时的内核页表
      • 内核早期的页表
      • cleanup_highmap之后的页表
      • 映射完整物理地址
      • 启用init_level4_pgt
    • 自底而上话内存
      • e820从硬件获取内存分布
      • 原始内存分配器--memblock
      • 页分配器
        • 寻找页结构体的位置
        • 眼花的页结构体
        • Node-Zone-Page
        • 传说的伙伴系统
        • Compound Page
        • GFP的功效
        • 页分配器的用户们
      • slub分配器
        • slub的理念
        • 图解slub
      • 内存管理的不同粒度
      • 挑战和进化
        • 扩展性的设计和实现
        • 减少竞争 per_cpu_pageset
        • 海量内存
        • 延迟初始化
        • 内存热插拔
        • 连续内存分配器
    • 虚拟内存空间
      • 页表和缺页中断
      • 虚拟地址空间的管家--vma
      • 匿名反向映射的前世今生
      • 图解匿名反向映射
      • THP和mapcount之间的恩恩怨怨
      • 透明大页的玄机
      • NUMA策略
      • numa balance
      • 老版vma
    • 内存的回收再利用
      • 水线
      • Big Picture
      • 手动触发回收
      • Page Fram Reclaim Algorithm
      • swapfile原理使用和演进
    • 内存隔离
      • memcg初始化
      • 限制memcg大小
      • 对memcg记账
    • 通用
      • 常用全局变量
      • 常用转换
    • 测试
      • 功能测试
      • 性能测试
  • 中断和异常
    • 从IDT开始
    • 中断?异常?有什么区别
    • 系统调用的实现
    • 异常向量表的设置
    • 中断向量和中断函数
    • APIC
    • 时钟中断
    • 软中断
    • 中断、软中断、抢占和多处理器
  • 设备模型
    • 总线
    • 驱动
    • 设备
    • 绑定
  • nvdimm初探
    • 使用手册
    • 上帝视角
    • nvdimm_bus
    • nvdimm
    • nd_region
    • nd_namespace_X
    • nd_dax
      • dev_dax
  • KVM
    • 内存虚拟化
      • Qemu内存模型
      • KVM内存管理
  • cgroup
    • 使用cgroup控制进程cpu和内存
    • cgroup文件系统
    • cgroup层次结构
    • cgroup和进程的关联
    • cgroup数据统计
  • 同步机制
    • 内存屏障
    • RCU
  • Trace/Profie/Debug
    • ftrace的使用
    • 探秘ftrace
    • 内核热补丁的黑科技
    • eBPF初探
    • TraceEvent
    • Drgn
  • 内核中的数据结构
    • 双链表
    • 优先级队列
    • 哈希表
    • xarray
    • B树
    • Maple Tree
    • Interval Tree
  • Tools
  • Good To Read
    • 内核自带文档
    • 内存相关
    • 下载社区邮件
Powered by GitBook
On this page
  • 计算当前内核被加载的地址
  • 切换GDT
  • 计算解压缩内核用的地址
  • 直接从startup_64开始debug
  • 移动压缩内核

Was this helpful?

  1. 内核加载全流程

保护模式内核代码赏析

Previous内核加载的几个阶段Next内存管理

Last updated 1 year ago

Was this helpful?

进入保护模式内核后,还有很多和体系结构相关的工作需要准备。 下面我们就利用bochs的调试功能,对这部分代码作一些分析。

具体如何进入到这部分调试界面,可参考

计算当前内核被加载的地址

进入保护模式内核后,有这么一段代码是用来计算保护模式内核被加载到哪里,并把这个地址保存到epb中。 arch/x86/boot/compressed/head_64.S

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

原理是call的短调用,会将下一条指令的地址压栈。也就是popl %ebp的地址会被压栈。而rva(1b)是这条指令相对于startup_32的偏移,所以两者相减就得到了本次运行个过程中,实际被加载到的地址。

现在我们就用bochs来验证一下,看看保护模式的内核是不是加载到了0x100000。

(0) Breakpoint 2, 0x0000000000100000 in ?? ()
Next at t=90656425
(0) [0x0000000000100000] 0010:0000000000100000 (unk. ctxt): cld                       ; fc
<bochs:7> u /10
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

在0x100000处断点停止后,先看一下反汇编。popl %ebp指令的地址是0x10000d。而这条指令的相对地址是0x0d。接下来我们来确认压栈的地址,和最后计算的结果。

<bochs:12> 
Next at t=90656430
(0) [0x000000000010000e] 0010:000000000010000e (unk. ctxt): sub ebp, 0x0000000d       ; 83ed0d
<bochs:13> r
rax: 0x00000000_00100000 rcx: 0x00000000_00000000
rdx: 0x00000000_00000000 rbx: 0x00000000_00000000
rsp: 0x00000000_00014338 rbp: 0x00000000_0010000d
...

在执行减法前,查看一下寄存器。发现ebp的值已经是0x10000d了。

<bochs:14> n
Next at t=90656431
(0) [0x0000000000100011] 0010:0000000000100011 (unk. ctxt): lea eax, dword ptr ss:[ebp+10657808] ; 8d8510a0a200
<bochs:15> r
rax: 0x00000000_00100000 rcx: 0x00000000_00000000
rdx: 0x00000000_00000000 rbx: 0x00000000_00000000
rsp: 0x00000000_00014338 rbp: 0x00000000_00100000

接下去再执行一条,也就是减去0x0d,ebp的值就是0x100000了。这就是我们这次保护模式内核被加载的地址,也是我们想要的。

我后来想到,这么计算还需要依赖一个条件,就是代码段的基址需要是0。否则计算出的值需要加上基址才是。那就来看看当前的代码段设置。

<bochs:16> info gdt
Global Descriptor Table (base=0x0000000000013c50, limit=39):
GDT[0x00]=??? descriptor hi=0x00000000, lo=0x00000000
GDT[0x01]=??? descriptor hi=0x00000000, lo=0x00000000
GDT[0x02]=Code segment, base=0x00000000, limit=0xffffffff, Execute/Read, Non-Conforming, Accessed, 32-bit
GDT[0x03]=Data segment, base=0x00000000, limit=0xffffffff, Read/Write, Accessed
GDT[0x04]=32-Bit TSS (Busy) at 0x00001000, length 0x00067
You can list individual entries with 'info gdt [NUM]' or groups with 'info gdt [NUM] [NUM]'

看来确实是0。

切换GDT

紧接着内核切换了gdt.

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

而这里的gdt定义是

SYM_DATA_START_LOCAL(gdt)
	.word	gdt_end - gdt - 1
	.long	0
	.word	0
	.quad	0x00cf9a000000ffff	/* __KERNEL32_CS */
	.quad	0x00af9a000000ffff	/* __KERNEL_CS */
	.quad	0x00cf92000000ffff	/* __KERNEL_DS */
	.quad	0x0080890000000000	/* TS descriptor */
	.quad   0x0000000000000000	/* TS continued */
SYM_DATA_END_LABEL(gdt, SYM_L_LOCAL, gdt_end)

所以,gdt这块区域的开头几个字节是用作gdtr的,而且16位的界限已经定义好了。但是基址没有定义,因为运行时的地址我们事先不知道。但是还记得我们刚才分析的代码么?对了,就是后把当前加载的地址保存到了ebp。所以这第一句 leal rva(gdt)(%ebp), %eax就是计算了gdt的基址。

分析完了,那就用bochs调试一下看看。我们在leal这句执行完后,查看一下。

<bochs:24> r
rax: 0x00000000_00b2a010 rcx: 0x00000000_00000000
rdx: 0x00000000_00000000 rbx: 0x00000000_00000000
...

说明gdt的地址在0xb2a010,我们用内存查看工具查看一下。

<bochs:26> xp /4xw 0x00b2a010
[bochs]:
0x0000000000b2a010 <bogus+       0>:	0x0000002f	0x00000000	0x0000ffff	0x00cf9a00

其中0x2f是界限,意思是gdt的大小是0x2f + 0x01 = 0x30 也就是有48字节。而我们gdt中,正好占了48字节,符合! 再来看dump出来的第二块8字节,因为x86是little endian的,所以实际值就是0x00cf9a000000ffff。这个不正好和__KERNEL32_CS所应以的值一样吗?完美!

执行完movl %eax, 2(%eax)后,我们再查看一下内存。因为gdt的最开始两个字节是界限,我们跳过这部分,直接dump后面的四个字节。

<bochs:33> xp /xw 0x00b2a012
[bochs]:
0x0000000000b2a012 <bogus+       0>:	0x00b2a010

显示此时这部分内存的内容已经是我们刚才所得到的gdt基址0x00b2a010了。

切换gdt的前后,我们都查看一下gdt。

<bochs:35> info gdt
Global Descriptor Table (base=0x0000000000013c50, limit=39):
GDT[0x00]=??? descriptor hi=0x00000000, lo=0x00000000
GDT[0x01]=??? descriptor hi=0x00000000, lo=0x00000000
GDT[0x02]=Code segment, base=0x00000000, limit=0xffffffff, Execute/Read, Non-Conforming, Accessed, 32-bit
GDT[0x03]=Data segment, base=0x00000000, limit=0xffffffff, Read/Write, Accessed
GDT[0x04]=32-Bit TSS (Busy) at 0x00001000, length 0x00067
<bochs:36> n
Next at t=109056323
(0) [0x000000000010001d] 0010:000000000010001d (unk. ctxt): mov eax, 0x00000018       ; b818000000
<bochs:37> info gdt
Global Descriptor Table (base=0x0000000000b2a010, limit=47):
GDT[0x00]=??? descriptor hi=0x000000b2, lo=0xa010002f
GDT[0x01]=Code segment, base=0x00000000, limit=0xffffffff, Execute/Read, Non-Conforming, 32-bit
GDT[0x02]=Code segment, base=0x00000000, limit=0xffffffff, Execute/Read, Non-Conforming, 64-bit
GDT[0x03]=Data segment, base=0x00000000, limit=0xffffffff, Read/Write
GDT[0x04]=32-Bit TSS (Available) at 0x00000000, length 0x00000
GDT[0x05]=??? descriptor hi=0x00000000, lo=0x00000000

这里可以看到,gdt确实发生了变化。大功告成!

计算解压缩内核用的地址

接下来有一段看着很长,实际功能简单的代码。注释上说计算解压缩的内核的之,并保存到ebx中。

#ifdef CONFIG_RELOCATABLE
	movl	%ebp, %ebx
	movl	BP_kernel_alignment(%esi), %eax # eax = align
	decl	%eax                            # eax = (align - 1)
	addl	%eax, %ebx			# ebx = ebx + (align -1)
	notl	%eax				# eax = !eax
	andl	%eax, %ebx			# ebx = ebx & !eax
	cmpl	$LOAD_PHYSICAL_ADDR, %ebx
	jae	1f
#endif
	movl	$LOAD_PHYSICAL_ADDR, %ebx
1:

我翻译了一下代码,并写道了注释里。如果用c来写,其实就是这么一句。

#define ALIGN(x, a)	(((x) + (a) - 1) & ~((a) - 1))

ALIGN(ebp, kernel_alignment)

那这个alignment是多少呢?这个值是从BP_kernel_alignment(%esi)这个地址取出来的。这个esi实际指向了bootparam,而BP_kernel_alignment是其中hdr里kernel_alignemnt的字段。看了下arch/x86/boot/header.S,这个值是在编译的时候定义号的。

kernel_alignment:  .long CONFIG_PHYSICAL_ALIGN	#physical addr alignment
						#required for protected mode
						#kernel

而这个CONFIG_PHYSICAL_ALIGN是一个配置项,看了下当前配置的值是0x200000。也就是2M。

分析完了,我们用bochs来验证一下:

  • kernel_alignment是不是0x200000

  • 计算完后,ebx是不是等于ebp向上2M对齐。ebp是刚才计算出来保护模式内核的加载地址1M,所以ebx预期也是2M

<bochs:43> n
Next at t=121856756
(0) [0x0000000000100050] 0008:0000000000100050 (unk. ctxt): mov eax, dword ptr ds:[esi+560] ;
<bochs:44> r
rax: 0x00000000_00000000 rcx: 0x00000000_00002028
rdx: 0x00000000_06000000 rbx: 0x00000000_00100000
...
<bochs:45> n
Next at t=121856757
(0) [0x0000000000100056] 0008:0000000000100056 (unk. ctxt): dec eax                   ; 48
<bochs:46> r
rax: 0x00000000_00200000 rcx: 0x00000000_00002028
rdx: 0x00000000_06000000 rbx: 0x00000000_00100000

在从kernel_alignment取值前后各查看了eax。可以看出,kernel_alignment确实是0x200000。

然后我们确认一下ebx的值。

<bochs:52> n
Next at t=121856761
(0) [0x000000000010005d] 0008:000000000010005d (unk. ctxt): cmp ebx, 0x01000000       ; 81fb00000001
<bochs:53> r
rax: 0x00000000_ffe00000 rcx: 0x00000000_00002028
rdx: 0x00000000_06000000 rbx: 0x00000000_00200000
rsp: 0x00000000_00b30000 rbp: 0x00000000_00100000

我们在cmp这条语句断点,查看rbx确实也是0x200000。

这时候我发现了一个定义,

#define CONFIG_PHYSICAL_START 0x1000000

这也是一个内核配置,默认是16M。所以我们辛辛苦苦算了半天,最后因为ebx小于16M,导致ebx还是强制设置成了16M。 好吧,白看了半天。

直接从startup_64开始debug

32位下的工作确实有限,我们也不能每次都从startup_32开始一步步debug。那怎么直接设置断点到startup_64呢?其实答案就在代码里。

	.code64
	.org 0x200
SYM_CODE_START(startup_64)
	/*
	 * 64bit entry is 0x200 and it is ABI so immutable!
	 ...

也就是说,startup_64的偏移一定是0x200(又是这512字节)。应为startup_32的偏移是0,所以如果要直接断点在startup_64,可以在0x100200地址设置断点。我们来验证一下:

<bochs:64> pb 0x100200
<bochs:65> c
(0) Breakpoint 4, 0x0000000000100200 in ?? ()
Next at t=173074964
(0) [0x0000000000100200] 0010:0000000000100200 (unk. ctxt): cld                       ; fc
<bochs:66> u /10
00100200: (                    ): cld                       ; fc
00100201: (                    ): cli                       ; fa
00100202: (                    ): xor eax, eax              ; 31c0
00100204: (                    ): mov ds, ax                ; 8ed8
00100206: (                    ): mov es, ax                ; 8ec0
00100208: (                    ): mov ss, ax                ; 8ed0
0010020a: (                    ): mov fs, ax                ; 8ee0
0010020c: (                    ): mov gs, ax                ; 8ee8
0010020e: (                    ): lea rbp, qword ptr ds:[rip-533] ; 488d2debfdffff
00100215: (                    ): mov eax, dword ptr ds:[rsi+560] ; 8b8630020000

瞧,正如我们所料。

移动压缩内核

/*
 * Copy the compressed kernel to the end of our buffer
 * where decompression in place becomes safe.
 */
	leaq	(_bss-8)(%rip), %rsi
	leaq	rva(_bss-8)(%rbx), %rdi
	movl	$(_bss - startup_32), %ecx
	shrl	$3, %ecx
	std
	rep	movsq
	cld

其中rbx是搬移后压缩内核的起始地址,但是为了避免在搬运过程中破坏内存,所以是从高地址到低地址搬运。好了,我们搬运完后来查看一下。

<bochs:54> u /7
00100288: (                    ): lea rsi, qword ptr ds:[rip+10665321] ; 488d3569bda200
0010028f: (                    ): lea rdi, qword ptr ds:[rbx+10665976] ; 488dbbf8bfa200
00100296: (                    ): mov ecx, 0x00a2c000       ; b900c0a200
0010029b: (                    ): shr ecx, 0x03             ; c1e903
0010029e: (                    ): std                       ; fd
0010029f: (                    ): rep movsq qword ptr es:[rdi], qword ptr ds:[rsi] ; f348a5
001002a2: (                    ): cld                       ; fc
<bochs:55> r
rax: 0x00000000_00000000 rcx: 0x00000000_6c65746e
rdx: 0x00000000_49656e69 rbx: 0x00000000_0359a000
rsp: 0x00000000_03fca000 rbp: 0x00000000_01000000
...
<bochs:56> u 0x0359a000 0x0359a010
0359a000: (                    ): add byte ptr ds:[rax], al ; 0000
0359a002: (                    ): add byte ptr ds:[rax], al ; 0000
0359a004: (                    ): add byte ptr ds:[rax], al ; 0000
0359a006: (                    ): add byte ptr ds:[rax], al ; 0000
0359a008: (                    ): add byte ptr ds:[rax], al ; 0000
0359a00a: (                    ): add byte ptr ds:[rax], al ; 0000
0359a00c: (                    ): add byte ptr ds:[rax], al ; 0000
0359a00e: (                    ): add byte ptr ds:[rax], al ; 0000

搬运先查看一下寄存器,rbx的值是0x0359a000。也就是一会儿我们预期把压缩的内核搬运到这个地址。并且我们先反汇编这个地址,发现此时地址内的内容都是0。

<bochs:63> u 0x0359a000 0x0359a010
0359a000: (                    ): cld                       ; fc
0359a001: (                    ): cli                       ; fa
0359a002: (                    ): lea esp, dword ptr ds:[rsi+488] ; 8da6e8010000
0359a008: (                    ): call .+0                  ; e800000000
0359a00d: (                    ): pop rbp                   ; 5d
0359a00e: (                    ): sub ebp, 0x0000000d       ; 83ed0d

搬运完后,我们再反汇编一下。发现此时已经有内容了。而且可以对比一下,这段代码和startup_32正好一样!

/*
 * Jump to the relocated address.
 */
	leaq	rva(.Lrelocated)(%rbx), %rax
	jmp	*%rax

接下来内核就会跳转到 rbx + Lrelocated继续运行。因为rbx就是刚才新搬运的地址0x0359a000,所以从这里开始内核就在新的地址运行了。差不多是在53M左右的地址空间。

在中,我们看到安装的内核是被压缩过,再打包的。而为了解压缩,需要先把已加载的,压缩内核搬移到新的位置,再来做解压缩。

bootloader如何加载bzImage
bzImage的全貌