Linux insides阅读笔记(持续更新)
[toc]
引导
从引导加载程序内核
处理器在引导阶段采用实模式工作
x8086处理器有A20寻址总线,最大1MB
实模式下寻址方式
CS:IP
其中CS是段,IP是偏移,X80_86中,每段65536字节(64KB)
PhysicalAddress = Segment * 16 + Offset
如CS:IP = 0x2000:0x0010对应的物理地址:
>>> hex((0x2000 << 4) + 0x0010) = 0x20010
如果地址超出1MB,则舍弃最高位
CPU第一条指令在0xFFFFFFF0,远超1MB,这条指令被映射到ROM上,因此第一条指令来自ROM而不是RAM
实模式下1MB地址空间分配表
0x00000000 - 0x000003FF - Real Mode Interrupt Vector Table
0x00000400 - 0x000004FF - BIOS Data Area
0x00000500 - 0x00007BFF - Unused
0x00007C00 - 0x00007DFF - Our Bootloader
0x00007E00 - 0x0009FFFF - Unused
0x000A0000 - 0x000BFFFF - Video RAM (VRAM) Memory
0x000B0000 - 0x000B7777 - Monochrome Video Memory
0x000B8000 - 0x000BFFFF - Color Video Memory
0x000C0000 - 0x000C7FFF - Video ROM BIOS
0x000C8000 - 0x000EFFFF - BIOS Shadow Area
0x000F0000 - 0x000FFFFF - System BIOS
引导过程
CPU寄存器中预定义:
IP 0xfff0 CS selector 0xf000 CS base 0xffff0000
CPU在实模式执行第一条指令
CPU复位,复位后第一条指令是复位向量,向量值是0xFFFFFFF0,这个地址包含一个跳转指令,通常指向BIOS入口点
BIOS工作,初始化和检查硬件之后,寻找可引导设备
可引导设备列表在BIOS配置中。 如果硬盘上存在MBR分区,那么引导扇区存储在第一个扇区(512BYTES)的头446BYTES。 引导扇区的最后两个字节必须是0x55和0xAA,这两个字节称为**魔术字节**
引导程序
GRUB2
syslinux
Linux通过Boot protocol来定义如何实现引导程序以GRUB2引导程序为例,给出参考例程
BIOS完成工作后,控制权转移给启动扇区代码
启动扇区代码参考:boot.img在启动代码初始化后,跳转到GRUB 2’s core image去执行
Core image代码参考:diskboot.imgcore image的初始化将把整个core image(包括BRUB2的内核代码和文件系统驱动)引导到内存。引导完成后,grub_main将被调用
grub_main参考代码:grub_maingrub_main初始化控制台,计算模块基地址,设置root设备,读取grub配置文件,加载模块。最后将GRUB置于normal模式。
grub_normal_execute被调用完成GRUB2的最后操作,然后显示菜单列出所有可用的操作系统。当操作系统被选择后,grub_menu_execute_entry开始执行,它将调用GRUB的boot命令,来引导被选中的操作系统
内核引导入内存后情况
| 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 0x7C00 001000 +------------------------+ | Reserved for MBR/BIOS | 000800 +------------------------+ | Typically used by MBR | 000600 +------------------------+ | BIOS use only | 000000 +------------------------+
内核设置
在引导完成后,进入内核,但在内核运行之前,还需要正确设置内核,包括启动内存管理、进程管理等
内核代码运行起点: arch/x86/boot/header.S定义的_start函数
.globl _start _start: .byte 0xeb .byte start_of_setup-1f 1: // // rest of the header //
start_of_setup函数功能
将所有段寄存器的值设置成一样的内容
- ds和es段寄存器内容设置相同,并设置sti指令允许中断
```
movw %ds, %ax
movw %ax, %es
sti - 设置cs段与其他寄存器一致
pushw %ds pushw $6f lretw
- ds和es段寄存器内容设置相同,并设置sti指令允许中断
设置堆栈
检查ss寄存器内容并进行更正
movw %ss, %dx cmpw %ax, %dx movw %sp, %dx je 2f
2: andw $~3, %dx jnz 3f movw $0xfffc, %dx 3: movw %ax, %ss movzwl %dx, %esp sti
完成结果如下图
如果ds!=ss且CAN_USE_HEAP已经置位,那么可以通过,那么将heap_end_ptr放入dx再加上STACK_SIZE,若没有溢出,就跳转到2的代码处继续执行,完成结果如下图
若CAN_USE_HEAP未置位,那么将dx加上STACK_SIZE,再跳转到2处代码继续执行,执行结果如下图:
设置bss
- 设置正确BSS段
movw $__bss_start, %di movw $_end+3, %cx ;四字节对齐 xorl %eax, %eax subw %di, %cx shrw $2, %cx rep; stosl
- 检查magic签名
若签名不对,将他跳转到setup_bad执行代码
完成设置后,堆栈情况如下图:cmpl $0x5a5aaa55, setup_sig jne setup_bad
- 设置正确BSS段
跳转到main.c开始执行代码
在内核安装代码的第一步
保护模式
淘汰实模式的原因:内存极其有限
32位地址线-4GB地址空间
两种不同内存管理机制
- 段式内存管理(段的定义和实模式完全不同)
- 大小的定义和起始位置通过
段描述符
进行描述,所有内存段的段描述符存储在全局描述符表(GDT),其地址会保存在特殊寄存器GDTR中。lgdt gdt ;将全局描述符的基址和大小保存在GDTR(48位寄存器)
- GDTR
- 全局描述符表的大小(16位)
- 全局描述符表的基址(32位)
- 段描述符结构(64位)
- LIMIT[20位]:段长度
- G = 0,则内存段长度按照1BYTE进行增长(例子:G=0 LIMIT=0xFFFFF,段长度为1MB)
- G = 1,则内存段按照4K BYTES进行增长(例子:G=1 LIMIT=0xFFFFF,段长为4GB)
- 计算公式:base_seg_length(也就是这里面的G) * (LIMIT + 1)
- Base[32-bits] :段基地址
- Type/Attribute(40-47bits):内存段类型及支持的操作
- S标记(第44位)定义了段的类型,为零说明是该段是系统段,为一说明该段为代码段或是数据段(堆栈段不包括在内,它比较特殊,必须可以进行读写);S为1时,第43位决定了该段时数据段还是代码段,43位=0时时数据段,43位=1时是代码段;(42、41、40)在段为数据段时表示(E扩展、W可写、A可访问),在段是代码段时表示(C一致、R可读、A可访问)
- E:
0
则数据段向上扩展,反之向下 - W:
1
数据段可写,反之不可写,所有数据段均可读 - A:表示该段是否已被CPU访问
- C:
1
代码段可被低优先级代码访问;0
只能同优先级的代码段访问 - R:
1
代码段可读。代码段永无写权限
- E:
- DPL(2-bits,bit45、bit46):定义该段优先级,具体值是0-3
- P(bit47):说明该内存段是否已在内存中,为
0
则访问这个内存段时将报错 - AVL(bit52):在Linux内核中没有被使用
- L(bit53):代码段中有意义,
1
说明该代码段需要运行在64位模式下 - D/B flag(bit54):在段描述符描述的是一个可执行代码段、下扩数据段还是一个堆栈段的不同情况下,该标志具有不同功能(对于32位代码段、数据段,这个标志总是为1,对于16位代码、数据段,这个标志被设置为0)
- 可执行代码段:此时该标志为D标志,用于指出该段中的指令引用有效地址和操作数的默认长度。若为
1
则默认值是32位地址和32位或8位操作数;若为0
则默认值是16位地址和16位或8位操作数。指令前缀0x66可以用来选择非默认值得操作数大小;前缀0x67可用来选择非默认值的地址大小 - 栈段(由SS寄存器指向的数据段):此时该标志为B(big)标志,用于指明隐含堆栈操作(如PUSH、POP、CALL)时的栈指针大小。如果该标志置位
1
,则使用32位栈指针并存放在ESP寄存器中;如果该标志为0
,则使用16位栈指针并存放在SP寄存器中。如果堆栈段被设置成一个下扩数据段,这个B也同时制定了堆栈段的上界限 - 下扩数据段:此时标志位B,用于指明上界限。
1
时,上界限是0xFFFFFFFF(4GB);0
时,上界限是0xFFFF(64GB)31 24 19 16 7 0 ------------------------------------------------------------ | | |B| |A| | | | |0|E|W|A| | | BASE 31:24 |G|/|L|V| LIMIT |P|DPL|S| TYPE | BASE 23:16 | 4 | | |D| |L| 19:16 | | | |1|C|R|A| | ------------------------------------------------------------ | | | | BASE 15:0 | LIMIT 15:0 | 0 | | | ------------------------------------------------------------
- 可执行代码段:此时该标志为D标志,用于指出该段中的指令引用有效地址和操作数的默认长度。若为
- S标记(第44位)定义了段的类型,为零说明是该段是系统段,为一说明该段为代码段或是数据段(堆栈段不包括在内,它比较特殊,必须可以进行读写);S为1时,第43位决定了该段时数据段还是代码段,43位=0时时数据段,43位=1时是代码段;(42、41、40)在段为数据段时表示(E扩展、W可写、A可访问),在段是代码段时表示(C一致、R可读、A可访问)
- 保护模式下,段寄存器不再是一个内存段的基址,而是一个称为
段选择子
的结构。每个段描述符都对应一个段选择子。其是一个16位结构|Index|TI|RPL|
- Index:表示在GDT中,对应段描述符的索引号
- TI:表示要在GDT还是LDT中查找对应的段描述符
- RPL表示请求者的优先级
- 保护模式下,每个段寄存器实际上包含两部分内容
- 可见部分:段选择子
- 隐藏部分:段描述符
- 在保护模式中,CPU寻址步骤
- 代码将相应
段选择子
装入某个段寄存器 - CPU根据
段选择子
从GDT中找到一个匹配的段描述符,然后将段描述符放入段寄存器的隐藏部分 - 在未使用向下扩展段的时候,内存段的基地址就是
段描述符中的基址
,段描述符的LIMIT + 1
就是内存段长度。若知道内存地址的偏移,则在没有开启分页机制的情况下,该内存的物理地址就是基址+偏移
- 代码将相应
- LIMIT[20位]:段长度
- 大小的定义和起始位置通过
- 内存分页
从实模式进入保护模式
从实模式进入保护模式的步骤如下:
- 段式内存管理(段的定义和实模式完全不同)
禁止中断发生
使用
lgdt
命令将GDT表装入GDTR
寄存器设置
CR0
寄存器的PE位为1,使CPU进入保护模式跳转开始执行保护模式代码
将启动参数拷贝到”zeropage”
copy_boot_params(void)
- 将header.S中定义的hdr结构内容拷贝到
boot_params
结构的字段struct setup_header hdr
中 - 若内核是通过老的命令行协议运行起来的,那么就更新内核的命令行指针
注意:拷贝hdr
数据结构的memcpy
函数不是C语言中的函数,而是定义在copy.S中,具体代码见下
可见,所有的方法始于GLOBAL(memcpy) pushw %si ;push si to stack pushw %di ;push di to stack movw %ax, %di ;move &boot_param.hdr to di movw %dx, %si ;move &hdr to si pushw %cx ;push cx to stack ( sizeof(hdr) ) shrw $2, %cx rep; movsl ;copy based on 4 bytes popw %cx ;pop cx andw $3, %cx ;cx = cx % 4 rep; movsb ;copy based on one byte popw %di popw %si retl ENDPROC(memcpy)
GLOBAL
宏定义,结束于ENDPROC
宏定义,这俩定义可以在arch/x86/include/asm/linkage.h中找到(链接有可能失效,在3.18版本的内核代码中相同位置也可找到) - memcpy(&boot_params.hdr, &hdr, sizeof hdr)函数说明
- copy.s中函数使用
fastcall
调用规则,因此参数通过ax
,dx
,cx
寄存器传入,而不是通过堆栈传入。所以这三个寄存器对应的参数如下- ax: boot_param.hdr
- dx: &hdr
- cx: sizeof(hdr)
- 运行过程:
si
,di
寄存器压栈后,将boot_param.hdr地址放入di
寄存器,然后把hdr
的地址放入si
寄存器,并将hdr
数据结构的大小压栈。接着以4字节为单位,将si
寄存器指向内存拷贝到di
指向的内存中。当剩下的字节不足4字节时,代码将原始的hdr
数据结构大小出栈放入cx
,然后对cx
的值对4求模,完事后根据cx
的值(即长度)以字节为单位将si
指向的内存内容拷贝到di
指向的内存。完成拷贝后将si
和di
出栈,返回。控制台初始化
hdr
结构体拷贝完成后,控制台将会被初始化,其通过调用arch/x86/boot/early_serial_console.c中的console_init
函数实现,其实现过程如下
- copy.s中函数使用
- 将header.S中定义的hdr结构内容拷贝到
查看命令行参数是否包含
earlyprintk
选项。其可能的取值为:- serial, 0x3f8, 115200
- serial, ttyS0, 115200
- ttyS0, 115200
串口初始化成功后,若命令行参数包含
debug
选项,则会进入这个逻辑,看到如下输出:if (cmdline_find_option_bool("debug")) puts("early console in setup code\n");
其中
puts
函数定义在tty.c。该函数仅调用putchar
函数将字符串按照字节输出。putchar
函数内容如下:void __attribute__((section(".inittext"))) putchar(int ch) { if (ch == '\n') putchar('\r'); bios_putchar(ch); if (early_serial_base != 0) serial_putchar(ch); }
可以看到,最终输出字符到显示器的函数是```bios_putchar```,这个函数实现如下: ```C static void __attribute__((section(".inittext"))) bios_putchar(int ch) { struct biosregs ireg; initregs(&ireg); ireg.bx = 0x0007; ireg.cx = 0x0001; ireg.ah = 0x0e; ireg.al = ch; intcall(0x10, &ireg, NULL); }
在这个函数中,
initregs
函数接收一个biosregs结构的地址作为输入参数,该函数内容如下:memset(reg, 0, sizeof *reg); reg->eflags |= X86_EFLAGS_CF; reg->ds = ds(); reg->es = ds(); reg->fs = fs(); reg->gs = gs();
可见,
initregs
先将reg通过memset
函数清零,然后为这些寄存器初始化值。```x86asm GLOBAL(memset) pushw %di movw %ax, %di movzbl %dl, %eax imull $0x01010101,%eax pushw %cx shrw $2, %cx rep; stosl popw %cx andw $3, %cx rep; stosb popw %di retl ENDPROC(memset)
这个函数也是采用的
fastcall
调用规则。因此传入参数也是ax、dx和cx三个寄存器。
该函数步骤如下:di
入栈,将biosregs
结构地址从ax
拷贝到di
寄存器。- 使用
movzbl
将dl
寄存器内容拷贝到ax
寄存器低字节,此时ax
包含了需要复制到di
指向地址空间的值。 imull
指令将eax
寄存器的值乘上0x01010101。这样是为了让代码拷贝四字节内容。如eax
本来是0x07
,乘完后就是0x07070707
(4字节的0x7)。- 完成后代码用
rep; stosl
指令将eax
内容拷贝到es:di
指向的内存。 - 最后
popw %di
还原现场。
完成这些后,函数返回到bios_putchar
函数,函数执行调用中断0x10
在显示器上输出一个字符。然后返回到putchar
函数检查是否初始化了串口,若串口已初始化,则调用serial_putchar将字符输出到串口。
堆初始化
当栈和bss段在header.S中被初始化后,内核需要初始化全局堆,该过程通过init_heap函数实现。其实现过程如下
首先检查内核头中的
loadflag
是否设置了CAN_USE_HEAP
标志。若已设置,则代码将计算栈的结束地址:char *stack_end; //%P1 is (-STACK_SIZE) if (boot_params.hdr.loadflags & CAN_USE_HEAP) { asm("leal %P1(%%esp),%0" : "=r" (stack_end) : "i" (-STACK_SIZE));
stack_end = esp - STACK_SIZE
计算堆的结束地址:
//heap_end = heap_end_ptr + 512 heap_end = (char *)((size_t)boot_params.hdr.heap_end_ptr + 0x200);
最后代码检查
heap_end
是否大于stack_end
,若大于,则让stack_end
等于heap_end
。
到此已经完成全局堆的初始化了,在成功结束后,GET_HEAP
方法就可以使用了。检查CPU类型
在堆初始化之后,内核代码会调用arch/x86/boot/cpu.c提供的
validate_cpu
函数检查CPU级别以确定系统是否能在当前CPU上运行。其运行过程如下:```C /*from cpu.c*/ check_cpu(&cpu_level, &req_level, &err_flags); /*after check_cpu call, req_level = req_level defined in cpucheck.c*/ if (cpu_level < req_level) { printf("This kernel requires an %s CPU, ", cpu_name(req_level)); printf("but only detected an %s CPU.\n", cpu_name(cpu_level)); return -1; }
check_cpu
进行了大量检测工作,包括但不限于:- 检查cpu标志,若是64位cpu,就设置long mode
- 检查CPU制造商
内存分布检测
CPU检测完成之后,内核调用detect_memory
函数进行内存侦测,以得到系统当前内存的使用分布。该方法使用多种编程接口,包括0x820
(获取全部内存分配),0x801
和0x88
(获取临近内存大小)。以arch/x86/boot/memory.c为中提供的detect_memory_e820
函数为例讲解流程。
该方法首先调用
initregs
初始化biosregs
结构,然后向该数据结构填入0x820
编程接口所要求的参数:initregs(&ireg); ireg.ax = 0xe820; ireg.cx = sizeof buf; ireg.edx = SMAP; ireg.di = (size_t)&buf;
参数讲解如下:
ax
固定为0xe820
cx
包含数据缓冲区大小,这个缓冲区内将包含系统内存的信息数据edx
必须是SMAP
这个魔术数字:0x534d4150
es:di
包含数据缓冲区的地址ebx
必须为0
通过循环收集内存信息,该循环开始于
0x15
中断调用,该中断调用返回地址分配表中的一项,接着程序返回的ebx
设置到biosregs
数据结构中,然后进行下一次0x15
中断调用,直到返回的eflags
包含标志X86_EFLAGS_CF
:intcall(0x15, &ireg, &oreg); ireg.ebx = oreg.ebx;
循环结束后,内存分配信息将被写入到
e820entry
数组中,这个数组包含三个信息:- 内存段起始地址
- 内存段大小
- 内存段类型(reserved, usable等)
在dmesg
输出中可以看到该数组内容[ 0.000000] e820: BIOS-provided physical RAM map: [ 0.000000] BIOS-e820: [mem 0x0000000000000000-0x000000000009fbff] usable [ 0.000000] BIOS-e820: [mem 0x000000000009fc00-0x000000000009ffff] reserved [ 0.000000] BIOS-e820: [mem 0x00000000000f0000-0x00000000000fffff] reserved [ 0.000000] BIOS-e820: [mem 0x0000000000100000-0x000000003ffdffff] usable [ 0.000000] BIOS-e820: [mem 0x000000003ffe0000-0x000000003fffffff] reserved [ 0.000000] BIOS-e820: [mem 0x00000000fffc0000-0x00000000ffffffff] reserved
键盘初始化
之后内核会调用keyboard_init()
函数进行键盘初始化操作。函数流程如下:
首先,方法调用
initregs
初始化寄存器结构,然后调用0x16
中断来获取键盘状态:initregs(&ireg); ireg.ah = 0x02; /* Get keyboard status */ intcall(0x16, &ireg, &oreg); boot_params.kbd_status = oreg.al;
在获取键盘状态后,代码将会在此调用
0x16
中断来设置键盘按键检测频率。ireg.ax = 0x0305; /* Set keyboard repeat rate */ intcall(0x16, &ireg, NULL);
系统参数查询
最后内核会进行一些列参数查询,在此简单介绍一些参数查询:
query_mca
是一个比较典型的函数,它的执行流程如下:调用
0x15
中断获取机器型号信息、BIOS版本及其他硬件相关属性:int query_mca(void) { struct biosregs ireg, oreg; u16 len; initregs(&ireg); ireg.ah = 0xc0; intcall(0x15, &ireg, &oreg); if (oreg.eflags & X86_EFLAGS_CF) return -1; /* No MCA present */ set_fs(oreg.es); len = rdfs16(oreg.bx); if (len > sizeof(boot_params.sys_desc_table)) len = sizeof(boot_params.sys_desc_table); copy_from_fs(&boot_params.sys_desc_table, oreg.bx, len); return 0; }
设置
ah
寄存器的值为0xc0
,然后调用0x15
BIOS中断。中断返回后代码检查carry flag,若为1,则BIOS不支持MCA。若
CF
被设置成0,则ES:BX
指向系统信息表:Offset Size Description 00h WORD number of bytes following 02h BYTE model (see #00515) 03h BYTE submodel (see #00515) 04h BYTE BIOS revision: 0 for first release, 1 for 2nd, etc. 05h BYTE feature byte 1 (see #00510) 06h BYTE feature byte 2 (see #00511) 07h BYTE feature byte 3 (see #00512) 08h BYTE feature byte 4 (see #00513) 09h BYTE feature byte 5 (see #00514) ---AWARD BIOS--- 0Ah N BYTEs AWARD copyright notice ---Phoenix BIOS--- 0Ah BYTE ??? (00h) 0Bh BYTE major version 0Ch BYTE minor version (BCD) 0Dh 4 BYTEs ASCIZ string "PTL" (Phoenix Technologies Ltd) ---Quadram Quad386--- 0Ah 17 BYTEs ASCII signature string "Quadram Quad386XT" ---Toshiba (Satellite Pro 435CDS at least)--- 0Ah 7 BYTEs signature "TOSHIBA" 11h BYTE ??? (8h) 12h BYTE ??? (E7h) product ID??? (guess) 13h 3 BYTEs "JPN"
调用
set_fs
方法,将es
寄存器值写入fs
寄存器:static inline void set_fs(u16 seg) { asm volatile("movw %0,%%fs" : : "rm" (seg)); }
最后,代码将
es:bx
指向的内存地址内容复制到boot_params.sys_desc_table
之后,内核将继续调用其他函数来获取相关参数,简单列举如下:
query_ist
获取Intel SpeedStep信息。该方法首先检查CPU类型,然后调用0x15
获得该信息并放入boot_params
中。- 接下来,内核调用
query_apm_bios
方法获得高级电源管理信息。query_apm_bios
也调用0x15
中断,但是将ax
设置成0x5300
以得到APM设置信息。中断调用返回后,代码检查bx
和cx
的值。若bx
不是0x504d
(PM标记),或者cx
不是0x02
(表示,支持32为模式),则代码直接返回错误;若无错误,则继续执行下面的步骤 - 接下来代码使用
ax = 0x5304
来调用0x15
中断,以断开APM
接口;然后使用ax = 0x5303
调用0x15
中断,使用32位接口重新连接APM
;最后使用ax = 5300
调用0x15
中断再次获取APM设置,然后将信息写入boot_paramx.apm_bios_info
。
需要注意的是,仅CONFIG_APM
或者CONFIG_APM_MODULE
被设置的情况下,query_apm_bios
方法才会被调用:#if defined(CONFIG_APM) || defined(CONFIG_APM_MODULE) query_apm_bios(); #endif
- 最后是
query_edd
方法调用,这个方法从BIOS中查询Enhanced Disk Drive
(edd)信息,其实现步骤如下- 首先,代码检查是否设置了edd选项,如果edd被设置成off,则该函数不进行动作,直接返回
- 如果edd被激活了,
query_edd
遍历所有BIOS支持的硬盘,并获取EDD信息:
在代码中,for (devno = 0x80; devno < 0x80+EDD_MBR_SIG_MAX; devno++) { if (!get_edd_info(devno, &ei) && boot_params.eddbuf_entries < EDDMAXNR) { memcpy(edp, &ei, sizeof ei); edp++; boot_params.eddbuf_entries++; } ... ... ...
0x80
是第一块硬盘,EDD_MBR_SIG_MAX
是一个宏,值为16。代码将获取的信息放入edd_info
数组中。get_edd_info
方法通过调用0x13
中断调用(设置ah = 0x41)来检查EDD是否被硬盘支持。若支持EDD,则再次调用0x13
中断,在这次调用中ah = 0x48
,且si
指向一个数据缓冲区地址。这个中断调用后,EDD信息将被保存到si
指向的缓冲区地址。
发表评论