在之前的章节中, 我们讲到了与系统架构有关的 setup_arch 函数部分,本文会继续从这里开始。 我们为 initrd 预留了内存之后,下一步是执行 olpc_ofw_detect 函数检测系统是否支持 One Laptop Per Child support。 我们不会考虑与平台有关的东西,且会忽略与平台有关的函数。所以我们继续往下看。 下一步是执行 early_trap_init 函数。这个函数会初始化调试功能 (#DB -当 TF 标志位和rflags被设置时会被使用)和 int3 (#BP)中断门。 如果你不了解中断,你可以从 初期中断和异常处理 中学习有关中断的内容。 在 x86 架构中,INT,INT0 和 INT3 是支持任务显式调用中断处理函数的特殊指令。INT3 指令调用断点(#BP)处理函数。 你如果记得,我们在这部分 看到过中断和异常概念:
olpc_ofw_detect
early_trap_init
#DB
TF
int3
#BP
x86
INT
INT0
INT3
---------------------------------------------------------------------------------------------- |Vector|Mnemonic|Description |Type |Error Code|Source | ---------------------------------------------------------------------------------------------- |3 | #BP |Breakpoint |Trap |NO |INT 3 | ----------------------------------------------------------------------------------------------
调试中断 #DB 是激活调试器的重要方法。early_trap_init 函数的定义在 arch/x86/kernel/traps.c 中。这个函数用来设置 #DB 和 #BP 处理函数,并且实现重新加载 IDT。
void __init early_trap_init(void) { set_intr_gate_ist(X86_TRAP_DB, &debug, DEBUG_STACK); set_system_intr_gate_ist(X86_TRAP_BP, &int3, DEBUG_STACK); load_idt(&idt_descr); }
我们之前中断相关章节中看到过 set_intr_gate 的实现。这里的 set_intr_gate_ist 和 set_system_intr_gate_ist 也是类似的实现。 这两个函数都需要三个参数:
set_intr_gate
set_intr_gate_ist
set_system_intr_gate_ist
Interrupt Stack Table
IST
x86_64
16kb
CPU
DEBUG_STACK
set_intr_gate_ist 和 set_system_intr_gate_ist 与 set_intr_gate 的工作原理几乎一样,只有一个区别。 这些函数检查中断号并在内部调用 _set_gate :
_set_gate
BUG_ON((unsigned)n > 0xFF); _set_gate(n, GATE_INTERRUPT, addr, 0, ist, __KERNEL_CS);
其中, set_intr_gate 把 dpl 和 ist 置为 0 来调用 _set_gate。 但是 set_intr_gate_ist 和 set_system_intr_gate_ist 把 ist 设置为 DEBUG_STACK,并且 set_system_intr_gate_ist 把 dpl 设置为优先级最低的 0x3。 当中断发生时,硬件加载这个描述符,然后硬件根据 IST 的值自动设置新的栈指针。 之后激活对应的中断处理函数。所有的特殊内核栈会在 cpu_init 函数中设置好(我们会在后文中提到)。
ist
dpl
0x3
cpu_init
当 #DB 和 #BP 门向 idt_descr 有写操作,我们会调用 load_idt 函数来执行 ldtr 指令来重新加载 IDT 表。 现在我们来了解下中断处理函数并尝试理解它的工作原理。当然,我们不可能在这本书中讲解所有的中断处理函数。 深入学习 linux 的内核源码是很有意思的事情,我们会在这里讲解 debug 处理函数的实现。请自行学习其他的中断处理函数实现。
idt_descr
load_idt
ldtr
IDT
debug
像上文中提到的,我们在 set_intr_gate_ist 中通过 &debug 的地址传送 #DB 处理函数。lxr.free-electorns.com 是很好的用来搜索 linux 源代码中标识符的资源。 遗憾的是,你在其中找不到 debug 处理函数。你只能在 arch/x86/include/asm/traps.h 中找到 debug 的定义:
&debug
asmlinkage void debug(void);
从 asmlinkage 属性我们可以知道 debug 是由 assembly 语言实现的函数。是的,又是汇编语言 :)。 和其他处理函数一样,#DB 处理函数的实现可以在 arch/x86/kernel/entry_64.S 文件中找到。 都是由 idtentry 汇编宏定义的:
asmlinkage
idtentry
idtentry debug do_debug has_error_code=0 paranoid=1 shift_ist=DEBUG_STACK
idtentry 是一个定义中断/异常指令入口点的宏。它需要五个参数:
现在我们来看下 idtentry 宏的实现。这个宏的定义也在相同的汇编文件中,并且定义了有 ENTRY 宏属性的 debug 函数。 首先,idtentry 宏检查所有的参数是否正确,是否需要切换到特殊栈。接下来检查中断返回的错误码。例如本案例中的 #DB 不会返回错误码。 如果有错误码返回,它会调用 INTR_FRAME 或者 XCPT_FRAM 宏。其实 XCPT_FRAME 和 INTR_FRAME 宏什么也不会做,只是对中断初始状态编译的时候有用。 它们使用 CFI 指令用来调试。你可以查阅更多有关 CFI 指令的信息 CFI。 就像 arch/x86/kernel/entry_64.S 中解释:CFI 宏是用来产生更好的回溯的 dwarf2 的解开信息。 它们不会改变任何代码。因此我们可以忽略它们。
ENTRY
INTR_FRAME
XCPT_FRAM
XCPT_FRAME
CFI
dwarf2
.macro idtentry sym do_sym has_error_code:req paranoid=0 shift_ist=-1 ENTRY(\sym) /* Sanity check */ .if \shift_ist != -1 && \paranoid == 0 .error "using shift_ist requires paranoid=1" .endif .if \has_error_code XCPT_FRAME .else INTR_FRAME .endif ... ... ...
当中断发生后经过初期的中断/异常处理,我们可以知道栈内的格式是这样的:
+-----------------------+ | | +40 | SS | +32 | RSP | +24 | RFLAGS | +16 | CS | +8 | RIP | 0 | Error Code | <---- rsp | | +-----------------------+
idtentry 实现中的另外两个宏分别是
ASM_CLAC PARAVIRT_ADJUST_EXCEPTION_FRAME
第一个 ASM_CLAC 宏依赖于 CONFIG_X86_SMAP 这个配置项和考虑安全因素,你可以从这里了解更多内容。 第二个 PARAVIRT_EXCEPTION_FRAME 宏是用来处理 Xen 类型异常(这章只讲解内核初始化,不会考虑虚拟化的内容)。 下一段代码会检查中断是否有错误码。如果没有则会把 $-1(在 x86_64 架构下值为 0xffffffffffffffff)压入栈:
ASM_CLAC
CONFIG_X86_SMAP
PARAVIRT_EXCEPTION_FRAME
Xen
$-1
0xffffffffffffffff
.ifeq \has_error_code pushq_cfi $-1 .endif
为了保证对于所有中断的栈的一致性,我们会把它处理为 dummy 错误码。下一步我们从栈指针中减去 $ORIG_RAX-R15:
dummy
$ORIG_RAX-R15
subq $ORIG_RAX-R15, %rsp
其中,ORIG_RAX,R15 和其他宏都定义在 arch/x86/include/asm/calling.h 中。ORIG_RAX-R15 是 120 字节。 我们在中断处理过程中需要把所有的寄存器信息存储在栈中,所有通用寄存器会占用这个 120 字节。 为通用寄存器设置完栈之后,下一步是检查从用户空间产生的中断:
ORIG_RAX
R15
ORIG_RAX-R15
testl $3, CS(%rsp) jnz 1f
我们查看段寄存器 CS 的前两个比特位。你应该记得 CS 寄存器包含段选择器,它的前两个比特是 RPL。所有的权限等级是0-3范围内的整数。 数字越小代表权限越高。因此当中断来自内核空间,我们会调用 save_paranoid,如果不来自内核空间,我们会跳转到标签 1 处处理。 在 save_paranoid 函数中,我们会把所有的通用寄存器存储到栈中,如果需要的话会用户态 gs 切换到内核态 gs:
CS
RPL
save_paranoid
1
gs
movl $1,%ebx movl $MSR_GS_BASE,%ecx rdmsr testl %edx,%edx js 1f SWAPGS xorl %ebx,%ebx 1: ret
下一步我们把 pt_regs 指针存在 rdi 中,如果存在错误码就把它存储到 rsi 中,然后调用中断处理函数,例如就像 arch/x86/kernel/trap.c中的 do_debug。 do_debug 像其他处理函数一样需要两个参数:
pt_regs
rdi
rsi
do_debug
中断处理函数完成工作后会调用 paranoid_exit 还原栈区。如果中断来自用户空间则切换回用户态并调用 iret。我们会在不同的章节继续深入分析中断。 这是用在 #DB 中断中的 idtentry 宏的基本介绍。所有的中断都和这个实现类似,都定义在 idtentry中。early_trap_init 执行完后,下一个函数是 early_cpu_init。 这个函数定义在 arch/x86/kernel/cpu/common.c 中,负责收集 CPU 和其供应商的信息。
paranoid_exit
iret
early_cpu_init
下一步是初始化早期的 ioremap。通常有两种实现与设备通信的方式:
ioremap
我们在 linux 内核启动过程中见过第一种方法(通过 outb/inb 指令实现)。 第二种方法是把 I/O 的物理地址映射到虚拟地址。当 CPU 读取一段物理地址时,它可以读取到映射了 I/O 设备的物理 RAM 区域。 ioremap 就是用来把设备内存映射到内核地址空间的。
outb/inb
I/O
RAM
像我上面提到的下一个函数时 early_ioremap_init,它可以在正常的像 ioremap 这样的映射函数可用之前,把 I/O 内存映射到内核地址空间以方便读取。 我们需要在初期的初始化代码中初始化临时的 ioremap 来映射 I/O 设备到内存区域。初期的 ioremap 实现在 arch/x86/mm/ioremap.c 中可以找到。 在 early_ioremap_init 的一开始我们可以看到 pmd_t 类型的 pmd 指针定义(代表页中间目录条目 typedef struct {pmdval_t pmd; } pmd_t; 其中 pmdval_t 是无符号长整型)。 然后检查 fixmap 是正确对齐的:
early_ioremap_init
pmd_t
pmd
typedef struct {pmdval_t pmd; } pmd_t;
pmdval_t
fixmap
pmd_t *pmd; BUILD_BUG_ON((fix_to_virt(0) + PAGE_SIZE) & ((1 << PMD_SHIFT) - 1));
fixmap - 是一段从 FIXADDR_START 到 FIXADDR_TOP 的固定虚拟地址映射区域。它在子系统需要知道虚拟地址的编译过程中会被使用。 之后 early_ioremap_init 函数会调用 mm/early_ioremap.c 中的 early_ioremap_setup 函数。 early_ioremap_setup 会填充512个临时的启动时固定映射表来完成无符号长整型矩阵 slot_virt 的初始化:
FIXADDR_START
FIXADDR_TOP
early_ioremap_setup
slot_virt
for (i = 0; i < FIX_BTMAPS_SLOTS; i++) slot_virt[i] = __fix_to_virt(FIX_BTMAP_BEGIN - NR_FIX_BTMAPS*i);
之后我们就获得了 FIX_BTMAP_BEGIN 的页中间目录条目,并把它赋值给了 pmd 变量,把启动时间页表 bm_pte 写满 0。然后调用 pmd_populate_kernel 函数设置给定的页中间目录的页表条目:
FIX_BTMAP_BEGIN
bm_pte
pmd_populate_kernel
pmd = early_ioremap_pmd(fix_to_virt(FIX_BTMAP_BEGIN)); memset(bm_pte, 0, sizeof(bm_pte)); pmd_populate_kernel(&init_mm, pmd, bm_pte);
这就是所有过程。如果你仍然觉得困惑,不要担心。在 内核内存管理,第二部分 章节会有单独一部分讲解 ioremap 和 fixmaps。
fixmaps
ioremap 初始化完成后,紧接着是执行下面的代码:
ROOT_DEV = old_decode_dev(boot_params.hdr.root_dev);
这段代码用来获取根设备的主次设备号。后面 initrd 会通过 do_mount_root 函数挂载到这个根设备上。其中主设备号用来识别和这个设备有关的驱动。 次设备号用来表示使用该驱动的各设备。注意 old_decode_dev 函数是从 boot_params_structure 中获取了一个参数。我们可以从 x86 linux 内核启动协议中查到:
initrd
do_mount_root
old_decode_dev
boot_params_structure
Field name: root_dev Type: modify (optional) Offset/size: 0x1fc/2 Protocol: ALL The default root device device number. The use of this field is deprecated, use the "root=" option on the command line instead
现在我们来看看 old_decode_dev 如何实现的。实际上它只是根据主次设备号调用了 MKDEV 来生成一个 dev_t 类型的设备。它的实现很简单:
MKDEV
dev_t
static inline dev_t old_decode_dev(u16 val) { return MKDEV((val >> 8) & 255, val & 255); }
其中 dev_t 是用来表示主/次设备号对的一个内核数据类型。但是这个奇怪的 old 前缀代表了什么呢?出于历史原因,有两种管理主次设备号的方法。 第一种方法主次设备号占用 2 字节。你可以在以前的代码中发现:主设备号占用 8 bit,次设备号占用 8 bit。但是这会引入一个问题:最多只能支持 256 个主设备号和 256 个次设备号。 因此后来引入了 32 bit 来表示主次设备号,其中 12 位用来表示主设备号,20 位用来表示次设备号。你可以在 new_decode_dev 的实现中找到:
old
new_decode_dev
static inline dev_t new_decode_dev(u32 dev) { unsigned major = (dev & 0xfff00) >> 8; unsigned minor = (dev & 0xff) | ((dev >> 12) & 0xfff00); return MKDEV(major, minor); }
如果 dev 的值是 0xffffffff,经过计算我们可以得到用来表示主设备号的 12 位值 0xfff,表示次设备号的20位值 0xfffff。因此经过 old_decode_dev 我们最终可以得到在 ROOT_DEV 中根设备的主次设备号。
dev
0xffffffff
0xfff
0xfffff
ROOT_DEV
下一步是调用 setup_memory_map 函数设置内存映射。但是在这之前我们需要设置与显示屏有关的参数(目前有行、列,视频页等,你可以在 显示模式初始化和进入保护模式 中了解), 与拓展显示识别数据,视频模式,引导启动器类型等参数:
setup_memory_map
screen_info = boot_params.screen_info; edid_info = boot_params.edid_info; saved_video_mode = boot_params.hdr.vid_mode; bootloader_type = boot_params.hdr.type_of_loader; if ((bootloader_type >> 4) == 0xe) { bootloader_type &= 0xf; bootloader_type |= (boot_params.hdr.ext_loader_type+0x10) << 4; } bootloader_version = bootloader_type & 0xf; bootloader_version |= boot_params.hdr.ext_loader_ver << 4;
我们可以从启动时候存储在 boot_params 结构中获取这些参数信息。之后我们需要设置 I/O 内存。众所周知,内核主要做的工作就是资源管理。其中一个资源就是内存。 我们也知道目前有通过 I/O 口和设备内存两种方法实现设备通信。所有有关注册资源的信息可以通过 /proc/ioports 和 /proc/iomem 获得:
boot_params
/proc/ioports
/proc/iomem
cat /proc/iomem 00000000-00000fff : reserved 00001000-0009d7ff : System RAM 0009d800-0009ffff : reserved 000a0000-000bffff : PCI Bus 0000:00 000c0000-000cffff : Video ROM 000d0000-000d3fff : PCI Bus 0000:00 000d4000-000d7fff : PCI Bus 0000:00 000d8000-000dbfff : PCI Bus 0000:00 000dc000-000dffff : PCI Bus 0000:00 000e0000-000fffff : reserved 000e0000-000e3fff : PCI Bus 0000:00 000e4000-000e7fff : PCI Bus 0000:00 000f0000-000fffff : System ROM
可以看到,根据不同属性划分为以十六进制符号表示的一段地址范围。linux 内核提供了用来管理所有资源的一种通用 API。全局资源(比如 PICs 或者 I/O 端口)可以划分为与硬件总线插槽有关的子集。 resource 的主要结构是:
resource
struct resource { resource_size_t start; resource_size_t end; const char *name; unsigned long flags; struct resource *parent, *sibling, *child; };
例如下图中的树形系统资源子集示例。这个结构提供了资源占用的从 start 到 end 的地址范围(resource_size_t 是 phys_addr_t 类型,在 x86_64 架构上是 u64)。 资源名(你可以在 /proc/iomem 输出中看到),资源标记(所有的资源标记定义在 include/linux/ioport.h 文件中)。最后三个是资源结构体指针,如下图所示:
start
end
resource_size_t
phys_addr_t
u64
+-------------+ +-------------+ | | | | | parent |------| sibling | | | | | +-------------+ +-------------+ | | +-------------+ | | | child | | | +-------------+
每个资源子集有自己的根范围资源。iomem 的资源 iomem_resource 的定义是:
iomem
iomem_resource
struct resource iomem_resource = { .name = "PCI mem", .start = 0, .end = -1, .flags = IORESOURCE_MEM, }; EXPORT_SYMBOL(iomem_resource); TODO EXPORT_SYMBOL
iomem_resource 利用 PCI mem 名字和 IORESOURCE_MEM (0x00000200) 标记定义了 io 内存的根地址范围。就像上文提到的,我们目前的目的是设置 iomem 的结束地址,我们需要这样做:
PCI mem
IORESOURCE_MEM (0x00000200)
io
iomem_resource.end = (1ULL << boot_cpu_data.x86_phys_bits) - 1;
我们对1左移 boot_cpu_data.x86_phys_bits。boot_cpu_data 是我们在执行 early_cpu_init 的时候初始化的 cpuinfo_x86 结构。从字面理解,x86_phys_bits 代表系统可达到的最大内存地址时需要的比特数。 另外,iomem_resource 是通过 EXPORT_SYMBOL 宏传递的。这个宏可以把指定的符号(例如 iomem_resource)做动态链接。换句话说,它可以支持动态加载模块的时候访问对应符号。 设置完根 iomem 的资源地址范围的结束地址后,下一步就是设置内存映射。它通过调用 setup_memory_map 函数实现:
boot_cpu_data.x86_phys_bits
boot_cpu_data
cpuinfo_x86
x86_phys_bits
EXPORT_SYMBOL
void __init setup_memory_map(void) { char *who; who = x86_init.resources.memory_setup(); memcpy(&e820_saved, &e820, sizeof(struct e820map)); printk(KERN_INFO "e820: BIOS-provided physical RAM map:\n"); e820_print_map(who); }
首先,我们来看下 x86_init.resources.memory_setup。x86_init 是一种 x86_init_ops 类型的结构体,用来表示项资源初始化,pci 初始化平台特定的一些设置函数。 x86_init 的初始化实现在 arch/x86/kernel/x86_init.c 文件中。我不会全部解释这个初始化过程,因为我们只关心一个地方:
x86_init.resources.memory_setup
x86_init
x86_init_ops
pci
struct x86_init_ops x86_init __initdata = { .resources = { .probe_roms = probe_roms, .reserve_resources = reserve_standard_io_resources, .memory_setup = default_machine_specific_memory_setup, }, ... ... ... }
我们可以看到,这里的 memory_setup 赋值为 default_machine_specific_memory_setup,它是我们在对 内核启动 过程中的所有 e820 条目经过整理和把内存分区填入 e820map 结构体中获得的。 所有收集的内存分区会用 printk 打印出来。你可以通过运行 dmesg 命令找到类似于下面的信息:
memory_setup
default_machine_specific_memory_setup
e820map
printk
dmesg
[ 0.000000] e820: BIOS-provided physical RAM map: [ 0.000000] BIOS-e820: [mem 0x0000000000000000-0x000000000009d7ff] usable [ 0.000000] BIOS-e820: [mem 0x000000000009d800-0x000000000009ffff] reserved [ 0.000000] BIOS-e820: [mem 0x00000000000e0000-0x00000000000fffff] reserved [ 0.000000] BIOS-e820: [mem 0x0000000000100000-0x00000000be825fff] usable [ 0.000000] BIOS-e820: [mem 0x00000000be826000-0x00000000be82cfff] ACPI NVS [ 0.000000] BIOS-e820: [mem 0x00000000be82d000-0x00000000bf744fff] usable [ 0.000000] BIOS-e820: [mem 0x00000000bf745000-0x00000000bfff4fff] reserved [ 0.000000] BIOS-e820: [mem 0x00000000bfff5000-0x00000000dc041fff] usable [ 0.000000] BIOS-e820: [mem 0x00000000dc042000-0x00000000dc0d2fff] reserved [ 0.000000] BIOS-e820: [mem 0x00000000dc0d3000-0x00000000dc138fff] usable [ 0.000000] BIOS-e820: [mem 0x00000000dc139000-0x00000000dc27dfff] ACPI NVS [ 0.000000] BIOS-e820: [mem 0x00000000dc27e000-0x00000000deffefff] reserved [ 0.000000] BIOS-e820: [mem 0x00000000defff000-0x00000000deffffff] usable ... ... ...
BIOS
下面两部是通过 parse_setup_data 函数解析 setup_data,并且把 BIOS 的 EDD 信息复制到安全的地方。 setup_data 是内核启动头中包含的字段,我们可以在 x86 的启动协议中了解:
parse_setup_data
setup_data
EDD
Field name: setup_data Type: write (special) Offset/size: 0x250/8 Protocol: 2.09+ The 64-bit physical pointer to NULL terminated single linked list of struct setup_data. This is used to define a more extensible boot parameters passing mechanism.
它用来存储不同类型的设置信息,例如设备树 blob,EFI 设置数据等等。第二步是从 boot_params 结构中复制我们在 arch/x86/boot/edd.c 中 BIOS 的 EDD 信息到 edd 结构中。
blob
EFI
edd
static inline void __init copy_edd(void) { memcpy(edd.mbr_signature, boot_params.edd_mbr_sig_buffer, sizeof(edd.mbr_signature)); memcpy(edd.edd_info, boot_params.eddbuf, sizeof(edd.edd_info)); edd.mbr_signature_nr = boot_params.edd_mbr_sig_buf_entries; edd.edd_info_nr = boot_params.eddbuf_entries; }
下一步是在初始化阶段完成内存描述符的初始化。我们知道每个进程都有自己的运行内存地址空间。通过调用 memory descriptor 可以看到这些特殊数据结构。 在 linux 内核源码中内存描述符是用 mm_struct 结构体表示的。mm_struct 包含许多不同的与进程地址空间有关的字段,像内核代码/数据段的起始和结束地址, brk 的起始和结束,内存区域的数量,内存区域列表等。这些结构定义在 include/linux/mm_types.h 中。task_struct 结构的 mm 和 active_mm 字段包含了每个进程自己的内存描述符。 我们的第一个 init 进程也有自己的内存描述符。在之前的章节我们看到过通过 INIT_TASK 宏实现 task_struct 的部分初始化信息:
memory descriptor
mm_struct
brk
task_struct
mm
active_mm
init
INIT_TASK
#define INIT_TASK(tsk) \ { ... ... ... .mm = NULL, \ .active_mm = &init_mm, \ ... }
mm 指向进程地址空间,active_mm 指向像内核线程这样子不存在地址空间的有效地址空间(你可以在这个文档 中了解更多内容)。 接下来我们在初始化阶段完成内存描述符中内核代码段,数据段和 brk 段的初始化:
init_mm.start_code = (unsigned long) _text; init_mm.end_code = (unsigned long) _etext; init_mm.end_data = (unsigned long) _edata; init_mm.brk = _brk_end;
init_mm 是初始化阶段的内存描述符定义:
init_mm
struct mm_struct init_mm = { .mm_rb = RB_ROOT, .pgd = swapper_pg_dir, .mm_users = ATOMIC_INIT(2), .mm_count = ATOMIC_INIT(1), .mmap_sem = __RWSEM_INITIALIZER(init_mm.mmap_sem), .page_table_lock = __SPIN_LOCK_UNLOCKED(init_mm.page_table_lock), .mmlist = LIST_HEAD_INIT(init_mm.mmlist), INIT_MM_CONTEXT(init_mm) };
其中 mm_rb 是虚拟内存区域的红黑树结构,pgd 是全局页目录的指针,mm_user 是使用该内存空间的进程数目,mm_count 是主引用计数,mmap_sem 是内存区域信号量。 在初始化阶段完成内存描述符的设置后,下一步是通过 mpx_mm_init 完成 Intel 内存保护扩展的初始化。下一步是代码/数据/bss 资源的初始化:
mm_rb
pgd
mm_user
mm_count
mmap_sem
mpx_mm_init
Intel
bss
code_resource.start = __pa_symbol(_text); code_resource.end = __pa_symbol(_etext)-1; data_resource.start = __pa_symbol(_etext); data_resource.end = __pa_symbol(_edata)-1; bss_resource.start = __pa_symbol(__bss_start); bss_resource.end = __pa_symbol(__bss_stop)-1;
通过上面我们已经知道了一小部分关于 resource 结构体的样子。在这里,我们把物理地址段赋值给代码/数据/bss 段。你可以在 /proc/iomem 中看到:
00100000-be825fff : System RAM 01000000-015bb392 : Kernel code 015bb393-01930c3f : Kernel data 01a11000-01ac3fff : Kernel bss 在 [arch/x86/kernel/setup.c](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/arch/x86/kernel/setup.c) 中有所有这些结构体的定义: static struct resource code_resource = { .name = "Kernel code", .start = 0, .end = 0, .flags = IORESOURCE_BUSY | IORESOURCE_MEM };
本章节涉及的最后一部分就是 NX 配置。NX-bit 或者 no-execute 位是页目录条目的第 63 比特位。它的作用是控制被映射的物理页面是否具有执行代码的能力。 这个比特位只会在通过把 EFER.NXE 置为1使能 no-execute 页保护机制的时候被使用/设置。在 x86_configure_nx 函数中会检查 CPU 是否支持 NX-bit,以及是否被禁用。 经过检查后,我们会根据结果给 _supported_pte_mask 赋值:
NX
NX-bit
no-execute
EFER.NXE
x86_configure_nx
_supported_pte_mask
void x86_configure_nx(void) { if (cpu_has_nx && !disable_nx) __supported_pte_mask |= _PAGE_NX; else __supported_pte_mask &= ~_PAGE_NX; }
以上是 linux 内核初始化过程的第五部分。在这一章我们讲解了有关架构初始化的 setup_arch 函数。内容很多,但是我们还没有学习完。其中,setup_arch 是一个很复杂的函数,甚至我不确定我们能在以后的章节中讲完它的所有内容。在这一章节中有一些很有趣的概念像 Fix-mapped 地址,ioremap 等等。 如果没听明白也不用担心,在 内核内存管理,第二部分 还会有更详细的解释。在下一章节我们会继续讲解有关结构初始化的东西, 以及初期内核参数的解析,pci 设备的早期转存,直接媒体接口扫描等等。
setup_arch
Fix-mapped
如果你有任何问题或者建议,你可以留言,也可以直接发送消息给我twitter。
很抱歉,英语并不是我的母语,非常抱歉给您阅读带来不便,如果你发现文中描述有任何问题,清提交一个 PR 到 linux-insides。
Copyright© 2013-2020
All Rights Reserved 京ICP备2023019179号-8