通过《Linux应用程序elf描述》,我们了解到一个应用程序编译后,最终会按照指定方式进行链接,而我们通过ld --verbose可以查看对应应用的默认链接方式。那么对于Linux内核呢?毫无疑问, Linux内核也是按照指定格式进行链接的,只是Linux的链接方式是由arch/arm64/kernel/vmlinux.lds.S指定的(gcc可以在链接的时候指定自定义链接脚本-T)。本章基于Linux内核Linux-4.19.73来作为例子说明的。
首先,我们看看vxlinux.lds.S为何物,如下图:
上图是一个删减了很多其他暂时不关心段的vmlinux.ld.s脚本文件,vmlinux.ld.s脚本文件的语法,这里不作出介绍, 关注的可以去查看相关文档。通过vmlinux.ld.s我们可以看到,bin文件的入口被设置为ENTRY(_text), 因此,_text即为入口函数地址,那么_text又在那里呢?那么接着看这个脚本文件,我们发现在灰色框里面有一个_text = .; 这说明_text的地址就是.head.text的首地址。那么哪些数据被链接到.head.text段了呢?通过搜索发现
点击(此处)折叠或打开
-
#define HEAD_TEXT KEEP(*(.head.text))
-
#define __HEAD .section ".head.text","ax"
-
-
$ grep __HEAD arch/arm64/ -r
-
include/linux/init.h:#define __HEAD .section ".head.text","ax"
-
-
$grep __HEAD arch/arm64/ -r
-
arch/arm64/kernel/head.S: __HEAD
-
因此, 目前只有kernel/head.S有代码被放置在了.head.text段,我们进一步看看arch/arm64/kernel/head.S文件,如下:
点击(此处)折叠或打开
-
#define __PHYS_OFFSET (KERNEL_START - TEXT_OFFSET) // 内核物理地址起始位置
-
-
__HEAD
-
_head:
-
b stext // branch to kernel start, magic
-
.long 0 // reserved
-
le64sym _kernel_offset_le // Image load offset from start of RAM, little-endian
-
le64sym _kernel_size_le // Effective size of kernel image, little-endian
-
le64sym _kernel_flags_le // Informative flags, little-endian
-
.quad 0 // reserved
-
.quad 0 // reserved
-
.quad 0 // reserved
-
.ascii "ARM\x64" // Magic number
-
.long 0 // reserved
-
-
__INIT
-
ENTRY(stext)
-
bl preserve_boot_args
-
bl el2_setup // Drop to EL1, w0=cpu_boot_mode
-
adrp x23, __PHYS_OFFSET // 物理地址偏移
-
and x23, x23, MIN_KIMG_ALIGN - 1 // KASLR offset, defaults to 0,一种内核安全机制,通过物理地址起始位置计算出偏移大小,偏移大小保存在X23寄存器
-
bl set_cpu_boot_mode_flag
-
bl __create_page_tables
-
bl __cpu_setup // initialise processor
-
b __primary_switch
-
ENDPROC(stext)
-
-
$ grep __INIT include/ -r
-
include/linux/init.h:#define __INIT .section ".init.text","ax"
-
$
从上面代码可以看出,只有_head被放在了.head.text段,而下面的stext是放在.init.text段的。因此,当前版本的Linux kernel的入口函数就是_head函数, 而_head函数就只有一条跳转指令:b stext;因此内核启动后, 最终去stext函数运行。而stext主要调用了几个函数,他们的作用如下:
1、preserve_boot_args: 将uboot传入的参数 保存到bootargs[4] 全局变量里面。
2、el2_setup :判断启动的模式是el2还是el1并进行相关级别的系统配置(armv8中el2是hypervisor模式,el1是标准的内核模式,具体的参考手册), 然后返回启动模式
3、set_cpu_boot_mode_flag: 将启动模式保存到全局变量
4、__create_page_tables: 创建内存映射表,一共两张,一张存放在swapper_pg_dir(线性映射),一张存放在idmap_pg_dir(一对一映射)。
5、__cpu_setup : 初始化处理器相关的代码,配置访问权限,内存地址划分等。
6、__primary_switch :开启MMU, 准备0号进程和内核栈,然后跳转到start_kernel运行
首先,我们说说preserve_boot_args函数, 它的实现如下:
点击(此处)折叠或打开
-
preserve_boot_args:
-
mov x21, x0 // 默认x0是uboot传入的第一个参数,通常是fdt的基地址,这里给x21寄存器保存
-
-
adr_l x0, boot_args //adr指令读取boot_args变量的当前地址,而不是链接地址(因为此时还没没有创建映射表,链接地址占时还不能用),boot_args是一个全局变量,默认地址是链接地址。
-
stp x21, x1, [x0] // 将uboot传入的第一个参数和第二个参数保存到boot_args的[0],[1]里面,表示地址和大小
-
stp x2, x3, [x0, #16] // 将uboot传入的第三个核第四个参数保存到boot_args的[2],[3]变量里面
-
dmb sy // 数据存储器栅栏,具体作用参考汇编手册
-
mov x1, #0x20 // boot_args有四个变量,每个变量8字节大小,因此 x1存入boot_args的长度(x0是boot_args的地址),然后调用_inval_dcache_area无效这段地址的缓存
-
b __inval_dcache_area // 无效x0和x1指定区域的缓存
-
ENDPROC(preserve_boot_args)
其次是el2_setup 函数, 它的实现如下:
点击(此处)折叠或打开
-
ENTRY(el2_setup)
-
msr SPsel, #1 // 设置SP的使用方式,是各用各的 还是共用一个,这里设置的是各用各的(armv8的栈使用)
-
mrs x0, CurrentEL // 读取当前的EL模式
-
cmp x0, #CurrentEL_EL2 // 判断当前的模式是不是el2,是 就跳转到el2的处理代码
-
b.eq 1f
-
mov_q x0, (SCTLR_EL1_RES1 | ENDIAN_SET_EL1) // 配置el1模式
-
msr sctlr_el1, x0
-
mov w0, #BOOT_CPU_MODE_EL1 // 返回值设置成 el1模式启动,注:w0是32位寄存器,通常x0/w0作为函数返回值使用
-
isb
-
ret
-
// 下面是el2,即hypervisor模式的处理代码,这里不介绍,在hypervisor会有介绍
-
1: mov_q x0, (SCTLR_EL2_RES1 | ENDIAN_SET_EL2)
-
msr sctlr_el2, x0
-
.............
-
eret
-
ENDPROC(el2_setup)
然后set_cpu_boot_mode_flag函数用于保存启动模式,该函数实现如下:
点击(此处)折叠或打开
-
set_cpu_boot_mode_flag:
-
adr_l x1, __boot_cpu_mode //将_boot_cpu_mode的物理地址读取到x1寄存器
-
cmp w0, #BOOT_CPU_MODE_EL2 // w0是el2_setup返回的值,即模式
-
b.ne 1f
-
add x1, x1, #4
-
1: str w0, [x1] // This CPU has booted in EL1,将模式保存到_boot_cpu_mode变量
-
dmb sy
-
dc ivac, x1 // Invalidate potentially stale cache line
-
ret
-
ENDPROC(set_cpu_boot_mode_flag)
对于__create_page_tables,则主要是创建内存映射表(这里只是简单的映射,只把内核代码段映射进来,用于开启MMU),后期还会做出二次映射。在映射函数中,有两种映射,一个是直接映射(即va=pa, 用于处理开启mmu那一瞬间不会出现异常),一个是线性映射(va = pa + offset)。具体函数如下:
点击(此处)折叠或打开
-
__create_page_tables:
-
mov x28, lr
-
// 无效 idmap_pg_dir和swpper_pg_end直接的数据缓存
-
adrp x0, idmap_pg_dir
-
adrp x1, swapper_pg_end
-
sub x1, x1, x0
-
bl __inval_dcache_area
-
-
// 清楚idmap和swapper映射表里的脏数据
-
adrp x0, idmap_pg_dir
-
adrp x1, swapper_pg_end
-
sub x1, x1, x0
-
1: stp xzr, xzr, [x0], #16
-
stp xzr, xzr, [x0], #16
-
stp xzr, xzr, [x0], #16
-
stp xzr, xzr, [x0], #16
-
subs x1, x1, #64
-
b.ne 1b
-
-
// mmu也属性标记
-
mov x7, SWAPPER_MM_MMUFLAGS
-
-
//创建直接映射 idmap,从idmap_text_start到idmap_text_end
-
adrp x0, idmap_pg_dir
-
adrp x3, __idmap_text_start // __pa(__idmap_text_start)
-
adrp x5, __idmap_text_end
-
clz x5, x5
-
cmp x5, TCR_T0SZ(VA_BITS) // default T0SZ small enough?
-
b.ge 1f // .. then skip VA range extension
-
-
adr_l x6, idmap_t0sz
-
str x5, [x6]
-
dmb sy
-
dc ivac, x6 // Invalidate potentially stale cache line
-
mov x4, #1 << (PHYS_MASK_SHIFT - PGDIR_SHIFT)
-
-
// VA_BITS = 48bit
-
str_l x4, idmap_ptrs_per_pgd, x5
-
ldr_l x4, idmap_ptrs_per_pgd
-
mov x5, x3 // __pa(__idmap_text_start)
-
adr_l x6, __idmap_text_end // __pa(__idmap_text_end)
-
// map_memory用于映射, 具体怎么写映射表, 参考armv8体系结构
-
map_memory x0, x1, x3, x6, x7, x3, x4, x10, x11, x12, x13, x14
-
-
// 线性映射内核代码段, 存放到swapper_pg_dir, 从_text段开始到_end之间的数据
-
adrp x0, swapper_pg_dir
-
mov_q x5, KIMAGE_VADDR + TEXT_OFFSET // compile time __va(_text)
-
add x5, x5, x23 // add KASLR displacement
-
mov x4, PTRS_PER_PGD
-
adrp x6, _end // runtime __pa(_end)
-
adrp x3, _text // runtime __pa(_text)
-
sub x6, x6, x3 // _end - _text
-
add x6, x6, x5 // runtime __va(_end)
-
// map_memory用于映射, 具体怎么写映射表, 参考armv8体系结构
-
map_memory x0, x1, x5, x6, x7, x3, x4, x10, x11, x12, x13, x14
-
-
// 无效映射表对应的缓存
-
adrp x0, idmap_pg_dir
-
adrp x1, swapper_pg_end
-
sub x1, x1, x0
-
dmb sy
-
bl __inval_dcache_area
-
ret x28
-
ENDPROC(__create_page_tables)
注:__idmap_text_start到__idmap_text_end的数据,其实就是启用mmu前后,需调用的那几个函数(因为CPU有加速指令处理的关系, 有些指令是乱序执行,防止开启mmu后,因为地址空间切换,导致的代码混乱的问题),因为有一段是va=pa因此, 之后即使还有code在用老的物理地址,也是不会出问题的。
__cpu_setup主要设置一些访问属性和内存划分等 ,具体函数如下:
-
ENTRY(__cpu_setup)
-
tlbi vmalle1 // 无效TLB
-
dsb nsh
-
-
mov x0, #3 << 20
-
msr cpacr_el1, x0 // 使能FP/ASIMD
-
mov x0, #1 << 12
-
msr mdscr_el1, x0 // 允许EL0访问DCC
-
isb
-
reset_pmuserenr_el0 x0 // 设置EL0禁止PMU访问
-
/*
-
* LPAE内存属性:
-
*
-
* n = AttrIndx[2:0]
-
* n MAIR
-
* DEVICE_nGnRnE 000 00000000
-
* DEVICE_nGnRE 001 00000100
-
* DEVICE_GRE 010 00001100
-
* NORMAL_NC 011 01000100
-
* NORMAL 100 11111111
-
* NORMAL_WT 101 10111011
-
*/
-
ldr x5, =MAIR(0x00, MT_DEVICE_nGnRnE) | \
-
MAIR(0x04, MT_DEVICE_nGnRE) | \
-
MAIR(0x0c, MT_DEVICE_GRE) | \
-
MAIR(0x44, MT_NORMAL_NC) | \
-
MAIR(0xff, MT_NORMAL) | \
-
MAIR(0xbb, MT_NORMAL_WT)
-
msr mair_el1, x5
-
-
mov_q x0, SCTLR_EL1_SET
-
/*
-
* 设置 TCR and TTBR. 用户和内核采用512GB (39-bit) 地址
-
*/
-
ldr x10, =TCR_TxSZ(VA_BITS) | TCR_CACHE_FLAGS | TCR_SMP_FLAGS | \
-
TCR_TG_FLAGS | TCR_KASLR_FLAGS | TCR_ASID16 | \
-
TCR_TBI0 | TCR_A1
-
tcr_set_idmap_t0sz x10, x9
-
-
/*
-
* Set the IPS bits in TCR_EL1.
-
*/
-
tcr_compute_pa_size x10, #TCR_IPS_SHIFT, x5, x6
-
-
msr tcr_el1, x10
-
ret
-
ENDPROC(__cpu_setup)
最后__primary_switch准备好0号进程栈,然后切换到start_kernel运行,具体代码实现如下:
点击(此处)折叠或打开
-
__primary_switch:
-
#ifdef CONFIG_RANDOMIZE_BASE
-
mov x19, x0 // preserve new SCTLR_EL1 value
-
mrs x20, sctlr_el1 // preserve old SCTLR_EL1 value
-
#endif
-
-
bl __enable_mmu // 开启mmu ,就是只是配置一些MMU寄存器
-
#ifdef CONFIG_RELOCATABLE
-
...... // 这里省略掉 内核代码重定位代码,这个主要用于gdb调试驱动
-
#endif
-
ldr x8, =__primary_switched // 将内核的物理地址起始地址作为参数1,调用_primary_switched函数
-
adrp x0, __PHYS_OFFSET
-
br x8
-
ENDPROC(__primary_switch)
-
union thread_union {
-
unsigned long stack[THREAD_SIZE/sizeof(long)];
-
} init_thread_union;
-
__primary_switched:
-
adrp x4, init_thread_union // 读取0号进程的thread_union地址
-
add sp, x4, #THREAD_SIZE // 将init_thread_union +THREAD_SIZE作为内核线程的栈顶地址
-
adr_l x5, init_task // 读取0号进程的task_struct结构
-
msr sp_el0, x5 // 在内核空间中,将当前task_sturct给sp_el0保存
-
-
adr_l x8, vectors // 设置中断向量表,vector在中断章节说明
-
msr vbar_el1, x8 // 系统寄存器vector table address
-
isb
-
-
str_l x21, __fdt_pointer, x5 // X21存放的是fdt指针, 这里将fdt保存到__fdt_pointer
-
-
// 下面是保存虚拟地址和物理地址之差到kimg_voffset变量
-
ldr_l x4, kimage_vaddr // 获取到内核虚拟起始地址
-
sub x4, x4, x0 // x0是传参传入的 内核物理起始地址
-
str_l x4, kimage_voffset, x5
-
-
// 将内核BSS段 请0
-
adr_l x0, __bss_start
-
mov x1, xzr
-
adr_l x2, __bss_stop
-
sub x2, x2, x0
-
bl __pi_memset
-
dsb ishst // Make zero page visible to PTW
-
-
#ifdef CONFIG_KASAN
-
bl kasan_early_init // 一种内存调试手段初始化
-
#endif
-
mov x30, #0 // x30是Lr寄存器, 这里赋值成NULL,不需要返回,返回即异常
-
b start_kernel // 跳转到start_kernel运行
-
ENDPROC(__primary_switched)
最后Linux内核进入C代码空间,start_kernel。
注: PAGE_OFFSET是内核虚拟地址起始地址, PAGE_SHIFT是页大小位数, TEXT_OFFSET是内核代码起始位置到内核起始地址的偏移。
注:在32位CPU中, 内核通常会保留开始的32k(0x8000)地址,前16k(0x4000)保存bootargs参数,后16k用于保存pgd,因此可以看到内核的代码地址基本都是0x8000开始,如0xC0008000.
注:vectors向量表位于:"arch/arm64/kernel/entry.S"文件中,实现如下:
点击(此处)折叠或打开
-
ENTRY(vectors)
-
kernel_ventry 1, sync_invalid // Synchronous EL1t
-
kernel_ventry 1, irq_invalid // IRQ EL1t
-
kernel_ventry 1, fiq_invalid // FIQ EL1t
-
kernel_ventry 1, error_invalid // Error EL1t
-
-
kernel_ventry 1, sync // Synchronous EL1h
-
kernel_ventry 1, irq // IRQ EL1h
-
kernel_ventry 1, fiq_invalid // FIQ EL1h
-
kernel_ventry 1, error // Error EL1h
-
-
kernel_ventry 0, sync // Synchronous 64-bit EL0
-
kernel_ventry 0, irq // IRQ 64-bit EL0
-
kernel_ventry 0, fiq_invalid // FIQ 64-bit EL0
-
kernel_ventry 0, error // Error 64-bit EL0
-
-
#ifdef CONFIG_COMPAT
-
kernel_ventry 0, sync_compat, 32 // Synchronous 32-bit EL0
-
kernel_ventry 0, irq_compat, 32 // IRQ 32-bit EL0
-
kernel_ventry 0, fiq_invalid_compat, 32 // FIQ 32-bit EL0
-
kernel_ventry 0, error_compat, 32 // Error 32-bit EL0
-
#else
-
kernel_ventry 0, sync_invalid, 32 // Synchronous 32-bit EL0
-
kernel_ventry 0, irq_invalid, 32 // IRQ 32-bit EL0
-
kernel_ventry 0, fiq_invalid, 32 // FIQ 32-bit EL0
-
kernel_ventry 0, error_invalid, 32 // Error 32-bit EL0
-
#endif
-
END(vectors)
注:armv8中,每个异常的 向量地址不再是4字节,而是0x80字节,可以放更多的代码在向量表里面。
阅读(6621) | 评论(0) | 转发(0) |