
操作系统启动引导
操作系统的起点:
磁盘引导程序,需要将内核等移入内存进行运行,并初始化多种模块和硬件
终点:运行第一个应用程序,系统的根文件系统
大致流程
BIOS/bootloader
由PC机的BIOS(0xFFFF0是BIOS存储的总线地址)把bootsect.s从某个固定的地址拿到了内存中某个固定的地址(0x90000),并且进行了一系列的硬件初始化和参数设置
bootsect.s
磁盘引导块程序,在磁盘的第一个扇区中的程序(0磁道 0磁头 1扇区)
bootsect.s被bios加载到0x7c00(31KB)处,并将自己移到了0x90000(576KB)处,并跳转到那里
然后使用BIOS中断将setup.s直接加载到自己的后面(0x90200)(576.5KB),并将system加载到地址0x10000处
作用:
- 首先将后续的
setup.s代码从磁盘中加载到紧接着bootsect.s的地方 - 在显示屏上显示loading system,再将system(操作系统)模块加载到0x10000的地方
- 最后跳转到
setup.s中运行
! 定义的SEG后都需要加0
SETUPLEN = 4 ! nr of setup-sectors
BOOTSEG = 0x07c0 ! original address of boot-sector
INITSEG = 0x9000 ! we move boot here - out of the way
SETUPSEG = 0x9020 ! setup starts here
SYSSEG = 0x1000 ! system loaded at 0x10000 (65536).
ENDSEG = SYSSEG + SYSSIZE ! where to stop loading
setup.s
setup.s是一个操作系统加载程序,它的主要作用时利用ROM BIOS中断读取及其系统数据,并将这些数据保存到0x90000开始的位置(覆盖掉了bootsect程序所在的地方),所取得的参数和保留的内存位置如图:
| 内存地址 | 长度(字节) | 名称 | 描述 |
|---|---|---|---|
| 0x90000 | 2 | 光标位置 | 列号(0x00最左端,0x00最顶端) |
| 0x90002 | 2 | 扩展内存数 | 系统从1MB开始的拓展内存数值(KB) |
| 0x90004 | 2 | 显示页面 | 当前显示页面 |
| 0x90006 | 1 | 显示模式 | |
| … | |||
| 0x90080 | 16 | 硬盘参数表 | 第一个硬盘的参数表 |
| 0x90090 | 16 | 硬盘参数表 | 第二个硬盘的参数表(没有则清零) |
| 0x901FC | 2 | 根设备号 | 根文件系统所在的设备号(bootsect.s中设置) |
设置系统内核运行的LDT(局部描述符寄存器),IDT(中断描述符寄存器),GDT(全局描述符寄存器)
设置中断控制芯片,进入保护模式运行(svc32保护模式,设置寄存器中的值)
跳转到system模块的最前面的代码运行(head.s)
head.s
加载内核运行时的各数据段寄存器,并且重新设置一个中断描述符表
开启内核正常运行时的协处理器等资源
设置内存管理的分页机制
跳转到main.c开始运行
main.c
在main.c文件开始有一段宏定义
/*
* This is set up by the setup-routine at boot-time
*/
#define EXT_MEM_K (*(unsigned short *)0x90002) //1M 以后的扩展内存大小
#define DRIVE_INFO (*(struct drive_info *)0x90080) // 来自BIOS信息,硬盘参数表(机器系统数据)
#define ORIG_ROOT_DEV (*(unsigned short *)0x901FC) // 来自BIOS信息,根设备号(软盘)
这里的地址正是对应上面setup.s中保存的读取的系统数据
在main函数开始有一段保存系统数据的代码
1.设置操作系统的根设备号
ROOT_DEV = ORIG_ROOT_DEV;
2.设置操作系统驱动参数
drive_info = DRIVE_INFO;
3.获取系统内存参数,设置系统的内存大小
本身内存(1MB)+扩展内存大小
memory_end = (1<<20) + (EXT_MEM_K<<10); // 内存大小=1Mb 字节+扩展内存(k)*1024 字节
memory_end &= 0xfffff000; // 忽略不到 4Kb(1 页)的内存数,内存对齐4k
// 假设物理内存为16M,进入第二个分支
// 主内存开始地址和缓冲区结束地址相同,均为4M位置
if (memory_end > 16*1024*1024)
memory_end = 16*1024*1024; // 如果内存超过 16Mb,则按 16Mb 计
if (memory_end > 12*1024*1024)
buffer_memory_end = 4*1024*1024; //设置高速缓冲区大小
else if (memory_end > 6*1024*1024)
buffer_memory_end = 2*1024*1024;
else
buffer_memory_end = 1*1024*1024;
main_memory_start = buffer_memory_end;
#ifdef RAMDISK
main_memory_start += rd_init(main_memory_start, RAMDISK*1024); //虚拟磁盘,把内存一部分做磁盘用
#endif
EXT_MEM_K的单位是KB,所以是左移10位
memory_end &= 0xfffff000; 这个代码是让系统内存大小内存对齐4KB
memory_end = 16*1024*1024;这个代码是设置内存最大为16MB
buffer_memory_end = 4*1024*1024; //设置高速缓冲区大小根据总的内存大小设置高速缓冲区
#ifdef RAMDISK这个是虚拟盘是否启用
Linux内核识别不同的板子
这里以linux内核3.4.2版本的为例,这时候还没有设备树
一种高效的编程结构
内核链接脚本写法
//arch/arm/kernel/vmlinux.lds.S
.init.arch.info : {
__arch_info_begin = .;
*(.arch.info.init)
__arch_info_end = .;
}
基础语法含义
SECTION_NAME : { ... }:定义一个输出段(Output Section),告诉链接器将所有符合规则的输入段(Input Sections)合并到这里。*(.name):*是通配符,表示将所有目标文件(.o)中名为.name的段全部收集进来。符号 = .;:.代表当前地址计数器(Location Counter)。这里定义了全局可见的链接器符号(如__arch_info_begin),C 代码可以通过extern声明这些符号来获取该段在内存中的起始/结束地址(用于遍历表格)。.init.arch.info(机器描述符表)*(.arch.info.init)收集所有使用MACHINE_START/MACHINE_END宏定义的struct machine_desc结构体。__arch_info_begin和__arch_info_end围成了这个结构体数组。内核启动的汇编头阶段(head.S)会遍历这个表,根据硬件 ID(或设备树)匹配当前单板,然后调用对应的初始化函数。这是 ARM 32 位时代支持多平台(Multi-platform)的核心机制。
arch.h中的宏定义
这里的##是一种连接符
//arch/arm/include/asm/mach/arch.h
/*
* Set of macros to define architecture features. This is built into
* a table by the linker.
*/
#define MACHINE_START(_type,_name) \
static const struct machine_desc __mach_desc_##_type \
__used \
__attribute__((__section__(".arch.info.init"))) = { \
.nr = MACH_TYPE_##_type, \
.name = _name,
#define MACHINE_END \
};
__attribute__ 是 GCC 编译器(及 Clang)的一个特性,用于向编译器传递“特殊指令”,告诉编译器对某个变量、函数或类型进行特定的处理。
语法格式为:__attribute__((属性列表))。
在这段代码里,它起了两个关键作用:
__attribute__((__section__(".arch.info.init"))):
这是定位指令。它强制编译器将这个machine_desc结构体变量生成的二进制数据,放到目标文件(.o)中名为.arch.info.init的节(Section)里,而不是默认的.data或.bss段。这样就方便后续链接器按名称把这些零散的数据“黏合”在一起。
这个 machine_desc 结构体虽然在 C 代码层面是“静态”的,也没有直接的 C 函数去调用它,但它需要被链接器脚本通过地址(__arch_info_begin)去隐式访问。编译器感知不到这种隐式访问。
- 如果没有
__used:编译器会把它当成死代码直接删除,导致内核启动时遍历表格找不到对应的机器 ID,最终无法启动(挂死)。 - 加了
__used:等于告诉编译器:“这变量虽然看着没人用,但你别动它,必须给我保留在目标文件里!”
实际使用案例
以smdk6440的一个使用MACHINE_START为示例
MACHINE_START(SMDK6440, "SMDK6440")
/* Maintainer: Kukjin Kim <kgene.kim@samsung.com> */
.atag_offset = 0x100,
.init_irq = s5p6440_init_irq,
.handle_irq = vic_handle_irq,
.map_io = smdk6440_map_io,
.init_machine = smdk6440_machine_init,
.timer = &s5p_timer,
.restart = s5p64x0_restart,
MACHINE_END
宏展开
结合上面两个文件我们可以得到最终宏定义展开的结果:
static const struct machine_desc __mach_desc_SMDK6440
__used
__attribute__((__section__(".arch.info.init"))) = {
.nr = MACH_TYPE_SMDK6440,
.name = "SMDK6440",
/* Maintainer: Kukjin Kim <kgene.kim@samsung.com> */
.atag_offset = 0x100,
.init_irq = s5p6440_init_irq,
.handle_irq = vic_handle_irq,
.map_io = smdk6440_map_io,
.init_machine = smdk6440_machine_init,
.timer = &s5p_timer,
.restart = s5p64x0_restart,
};
通过宏定义灵活的定义了一个专属于SMDK6440的machine_desc结构体,并且这个结构体在编译后被存放在.arch.info.init段里面而不是默认的.data或者.bss段
machine_desc结构体
machine_desc结构体是linux用于设备板子识别的结构体
struct machine_desc {
unsigned int nr; /* architecture number */
const char *name; /* architecture name */
unsigned long atag_offset; /* tagged list (relative) */
const char *const *dt_compat; /* array of device tree
* 'compatible' strings */
unsigned int nr_irqs; /* number of IRQs */
#ifdef CONFIG_ZONE_DMA
unsigned long dma_zone_size; /* size of DMA-able area */
#endif
unsigned int video_start; /* start of video RAM */
unsigned int video_end; /* end of video RAM */
unsigned char reserve_lp0 :1; /* never has lp0 */
unsigned char reserve_lp1 :1; /* never has lp1 */
unsigned char reserve_lp2 :1; /* never has lp2 */
char restart_mode; /* default restart mode */
void (*fixup)(struct tag *, char **,
struct meminfo *);
void (*reserve)(void);/* reserve mem blocks */
void (*map_io)(void);/* IO mapping function */
void (*init_early)(void);
void (*init_irq)(void);
struct sys_timer *timer; /* system tick timer */
void (*init_machine)(void);
#ifdef CONFIG_MULTI_IRQ_HANDLER
void (*handle_irq)(struct pt_regs *);
#endif
void (*restart)(char, const char *);
};
通过上面使用MACHINE_START和链接脚本的写法,将这些不同板子的结构体都限定在了内存的某一区域,并且通过uboot传递过来的参数对结构体进行配置。在之后的启动或其他函数中对该结构体进行调用
移植linux时候也要对这些结构体的变量进行赋值
识别CPU信息
获取芯片ID的指令
mrc p15, 0, r9, c0, c0 @ get processor id
bl __lookup_processor_type
lookup_processor_type
//arch/arm/kernel/head-common.S
__lookup_processor_type:
adr r3, __lookup_processor_type_data //将__lookup_processor_type_data地址载入r3
lookup_processor_type_data
//arch/arm/kernel/head-common.S
/*
* Look in <asm/procinfo.h> for information about the __proc_info structure.
*/
.align 2
.type __lookup_processor_type_data, %object
__lookup_processor_type_data:
.long .
.long __proc_info_begin
.long __proc_info_end
.size __lookup_processor_type_data, . - __lookup_processor_type_data
对应链接脚本
//arch/arm/kernel/vmlinux.lds.S
#define PROC_INFO \
. = ALIGN(4); \
VMLINUX_SYMBOL(__proc_info_begin) = .; \
*(.proc.info.init) \
VMLINUX_SYMBOL(__proc_info_end) = .;
.proc.info.init描述的是CPU(缓存大小、TLB类型、MMU属性、是否支持Thumb指令等)。
这段 proc.info.init 段就是内核的 “CPU 自检 ROM 表”。硬件上电后,内核完全不知道自己在什么 CPU 上跑,必须靠这张表来识别自己,并获取对应的缓存/TLB 操作方法,这是 ARM Linux 启动流程中最底层的硬件抽象层(HAL)。
具体芯片示例
以arm925示例
//arch/arm/mm/proc-arm925.S
.section ".proc.info.init", #alloc, #execinstr
.macro arm925_proc_info name:req, cpu_val:req, cpu_mask:req, cpu_name:req, cache
.type __\name\()_proc_info,#object
__\name\()_proc_info:
.long \cpu_val
.long \cpu_mask
.long PMD_TYPE_SECT | \
PMD_BIT4 | \
PMD_SECT_AP_WRITE | \
PMD_SECT_AP_READ
.long PMD_TYPE_SECT | \
PMD_BIT4 | \
PMD_SECT_AP_WRITE | \
PMD_SECT_AP_READ
b __arm925_setup
.long cpu_arch_name
.long cpu_elf_name
.long HWCAP_SWP | HWCAP_HALF | HWCAP_THUMB
.long cpu_arm925_name
.long arm925_processor_functions
.long v4wbi_tlb_fns
.long v4wb_user_fns
.long arm925_cache_fns
.size __\name\()_proc_info, . - __\name\()_proc_info
.endm
arm925_proc_info arm925, 0x54029250, 0xfffffff0, cpu_arm925_name
arm925_proc_info arm915, 0x54029150, 0xfffffff0, cpu_arm925_name
设置架构信息setup_arch
在内核main函数有一个设置架构信息的函数setup_arch
void __init setup_arch(char **cmdline_p)
{
struct machine_desc *mdesc;
setup_processor();
mdesc = setup_machine_fdt(__atags_pointer);
if (!mdesc)
mdesc = setup_machine_tags(machine_arch_type);
machine_desc = mdesc;
machine_name = mdesc->name;
....
}
这个函数开始完成了两步:
- 获取CPU信息并且保存到全局变量
- 获取设备信息并且保存到全局变量
setup_processor
static void __init setup_processor(void)
{
struct proc_info_list *list;
/*
* locate processor in the list of supported processor
* types. The linker builds this table for us from the
* entries in arch/arm/mm/proc-*.S
*/
list = lookup_processor_type(read_cpuid_id());
if (!list) {
printk("CPU configuration botched (ID %08x), unable "
"to continue.\n", read_cpuid_id());
while (1);
}
首先创建一个CPU指令集结构体proc_info_list,然后从制定的内存中(lookup_processor_type,对应上文的.proc.info.init段)获取到该描述结构体。最后将获取到的CPU信息存放到全局变量中,在末尾初始化CPU相关信息
setup_machine_fdt
setup_machine_fdt根据uboot传递来的设备数参数信息去寻找最匹配的设备信息,并且 保存到全局变量中以便后续调用
struct machine_desc * __init setup_machine_fdt(unsigned int dt_phys)
struct machine_desc *mdesc, *mdesc_best = NULL;
for_each_machine_desc(mdesc) {
score = of_flat_dt_match(dt_root, mdesc->dt_compat);
if (score > 0 && score < mdesc_score) {
mdesc_best = mdesc;
mdesc_score = score;
}
