发布时间:2025-12-10 11:20:57 浏览次数:13
首先将 linux kernel 代码编译好以后,在目录 arch/arm/kernel 下生成链接脚本文件 vmlinux.lds (vmlinux.lds由vmlinux.lds.S编译而来)。首先分析此脚本来熟悉 linux kernel 二进制代码分布结构。
在 vmlinux.lds.S 中
ENTRY(stext)指明了linux内核入口,入口为stext。符号stext定义在 arch/arm/kernel/head.S 文件中:
.arm__HEADENTRY(stext)ARM_BE8(setendbe )@ ensure we are in BE8 modeTHUMB(adrr9, BSYM(1f))@ Kernel is always entered in ARM.THUMB(bxr9)@ If this is a Thumb-2 kernel,THUMB(.thumb)@ switch to Thumb now.THUMB(1:)ENTRY在 include/linux/linkage.h 中定义
#ifndef ENTRY#define ENTRY(name) \.globl name ASM_NL \ALIGN ASM_NL \name:#endif通过代码可以看到 ENTRY 宏只是对全局符号的导出起到包装作用,
下面分析ENTRY语句下的五条指令
1、ARM_BE8(setend be )
ARM_BE8在 arch/arm/include/asm/assembler.h 中定义
如果开启 CONFIG_CPU_ENDIAN_BE8 选项,code(setend be)则会被原封不动编译到内核中,如果没有开启此选项,code(setend be)就不会被编译到内核中。
(extension) #define后的省略号的意义如下(以PDEBUG为例):
#define PDEBUG(fmt, args...) printk( KERN_DEBUG "scull: " fmt, ## args)// examplePDEBUG("a=%d, b=%d", a, b);// 展开后printk( KERN_DEBUG "scull: " "a=%d, b=%d", a, b);(1)arm BE8 mode 是什么
根据博客 https://blog.richliu.com/2010/04/08/907/arm11-be8-and-be32 所述:
BE8 和 BE32 是字节序相关的概念,举例:
假设需要将 0x11223344 存入内存中:
little endian:
| memory address | 0 | 1 | 2 | 3 |
| data | 0x44 | 0x33 | 0x22 | 0x11 |
big endian:
| memory address | 0 | 1 | 2 | 3 |
| data | 0x11 | 0x22 | 0x33 | 0x44 |
现假设对0x11223344内存存储分布如下:
| memory address | 0 | 1 | 2 | 3 |
| data | 0x11 | 0x22 | 0x33 | 0x44 |
在BE32 mode下:
LDR r0, [0]# r0 = 0x44332211LDRB r0, [0]# r0 = 0x00000044LDRB r0, [3]# r0 = 0x00000011在BE8 mode下:
LDR r0, [0]# r0 = 0x11223344LDRB r0, [0]# r0 = 0x00000011LDRB r0, [3]# r0 = 0x00000044关于 byte invariant endianness 的概念可以参照博客 https://blog.csdn.net/moreaction/article/details/5280067/
(2)setend be 指令的含义
setend 指令选择数据访问的字节序,在百度文库 https://wenku.baidu.com/view/6f83c4c2951ea76e58fafab069dc5022aaea46e5.html 解释了这个指令。在linux内核代码中,如果开启了 CONFIG_CPU_ENDIAN_BE8 选项,则 setend be 指令会被编译到内核中并执行,此指令的执行代表选择数据访问的字节序为 BE8 模式,BE8 模式的数据访问效果如上文所述。
2、THUMB( adr r9, BSYM(1f) )
3、THUMB( bx r9 )
4、THUMB( .thumb )
5、THUMB(1: )
在 2\3\4\5 中,THUMB、BSYM都定义在 arch/arm/include/asm/unified.h 中:
若没有开启 CONFIG_THUMB2_KERNEL 选项,则这四条语句都不会被编译进内核中。且开启此选项需要 gcc 版本高于 4:
#if __GNUC__ < 4#error Thumb-2 kernel requires gcc >= 4#endif如果 CONFIG_THUMB2_KERNEL 选项开启且定义了 __ASSEMBLY__,则这四条语句会被预编译为:
adrr9, 1f + 1bxr9.thumb1:至于为什么需要 1f+1 原因在于分支指令 bx:
bx 为带状态切换的跳转指令,其语法格式为 bx 其中 RM 只能是寄存器,并且当 RM 的bit[0]为1时,切换到 Thumb 指令,为0时切换到 ARM 指令。
以下是猜测:由于指令对齐,所以带状态分支跳转地址一定为偶数,所以CPU在收到条件分支指令时不会将bit[0]当作地址的一部分,而是用来做bx的状态选择,并且由于地址为偶数的原因, adr r9, 1f 指令执行后,r9的bit[0]一定为0,所以需要加上1来进行状态选择,以达到跳转后开始执行Thumb指令集,并且,.thumb 也告知编译器,其下的汇编代码需要编译为Thumb指令。
至此分析完这五条指令。
next,如果开启了 CONFIG_ARM_VIRT_EXT 开关,则会跳转到 __hyp_stub_install 继续执行,具体代码如下:
#ifdef CONFIG_ARM_VIRT_EXTbl__hyp_stub_install#endif__hyp_stub_install 定义在 arch/arm/kernel/hyp-stub.S 中,根据源代码中的注释解释:
/** Hypervisor stub installation functions.** These must be called with the MMU and D-cache off.* They are not ABI compliant and are only intended to be called from the kernel* entry points in head.S.*/目前只知道此代码与arm虚拟化功能有关
next, linux 内核将会开始检测所运行的硬件是否支持此内核映像。由于 arm 内核种类多,并且互相之间有无法兼容的区别,所以当开发者对内核进行开发或者使用者编译内核时,都会在源码级别上给出对硬件的某些定义,就如下文所提到的 __proc_info 结构。内核在编译期即确定了自己所支持的硬件种类,例如在 s3c2440 平台下即会去指定支持 arm920t 架构。由于会有一些与硬件交互的操作无法互相兼容,所以内核会通过下文的一些操作来检查硬件的属性是否与编译到内核里对硬件的定义信息相互匹配,如果发现不匹配则会立刻让内核陷入死循环并告知启动失败。
检查 processor id
safe_svcmode_maskall r9 用来确保 CPU 在SVC模式下,并且所有中断都被屏蔽。其中 safe_svcmode_maskall 是一个宏,他定义在 arch/arm/include/asm/assembler.h 中,源码中的注释对其解释为:
/** Helper macro to enter SVC mode cleanly and mask interrupts. reg is* a scratch register for the macro to overwrite.** This macro is intended for forcing the CPU into SVC mode at boot time.* you cannot return to the original mode.*/mrc 指令将协处理器的寄存器中数值传送到ARM处理器的寄存器中。其中 mrc p15, 0, r9, c0, c0 用来获取处理器id(processor id)并将此值传递给r9
在指令 bl __lookup_processor_type 中,__lookup_processor_type 符号定义在 arch/arm/kernel/head-common.S 中:
在这段代码中涉及到 __lookup_processor_type_data 定义为:
/** Look in <asm/procinfo.h> for information about the __proc_info structure.*/.align2.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可以理解为在 __proc_info_begin 表示的内存地址与 __proc_info_end 表示的内存地址之间包含了一个或多个 __proc_info 结构体,这个结构体用C语言的定义如下:
struct proc_info_list {unsigned intcpu_val;unsigned intcpu_mask;unsigned long__cpu_mm_mmu_flags;/* used by head.S */unsigned long__cpu_io_mmu_flags;/* used by head.S */unsigned long__cpu_flush;/* used by head.S */const char*arch_name;const char*elf_name;unsigned intelf_hwcap;const char*cpu_name;struct processor*proc;struct cpu_tlb_fns*tlb;struct cpu_user_fns*user;struct cpu_cache_fns*cache;};这段代码会遍历结构体数组,并将结构内的 cpu 信息与通过 mrc 指令获取的处理器id进行对比,并通过 r5 寄存器返回结构的地址,或者返回 0 代表匹配失败。add r5, r5, #PROC_INFO_SZ 指令就是将指针指向下一个结构体,ldmia r5, {r3, r4} 指令就是将 cpu_val 与 cpu_mask 的值分别装载到 r3 r4 中,cmp r5, r6 指令来判断是否已经遍历到了数组的末尾,即已经没有未检查过的 __proc_info 结构体了,如果r5 != r6 则 blo 1b,若相等则给 r5 赋值为0并返回,指令 and r4, r4, r9 与 teq r3, r4 用于检查processor_id,如果通过检查则 beq 2f 并 通过指令 ret lr 返回,返回时寄存器 r5 存储的值为此相匹配的 __proc_info 结构的地址。
movs r10, r5 将 r5 的值转存到 r10 中,并且由于使用 movs 指令,会影响 CPSR 寄存器的值,若 r5 为0则会使得 Z 位置1,而 beq 指令通过判断 Z 位是否为1来决定是否跳转。最后,如果 r5 为0,即没有匹配的 __proc_info 则会导致 beq __error_p 执行,若执行此语句,最终便会跳转到 __error 代码段 最后系统会 halt up。其中 __error_p 与 __error 皆定义在 arch/arm/kernel/head-common.S 中。
通过查看链接脚本,我们可以清晰的看到 __proc_info 结构的存储位置:
#define PROC_INFO\. = ALIGN(4);\VMLINUX_SYMBOL(__proc_info_begin) = .;\*(.proc.info.init)\VMLINUX_SYMBOL(__proc_info_end) = .;及
.text : {/* Real text segment*/_stext = .;/* Text and read-only data*/IDMAP_TEXT__exception_text_start = .;*(.exception.text)__exception_text_end = .;IRQENTRY_TEXTTEXT_TEXTSCHED_TEXTLOCK_TEXTKPROBES_TEXT*(.gnu.warning)*(.glue_7)*(.glue_7t). = ALIGN(4);*(.got)/* Global offset table*/ARM_CPU_KEEP(PROC_INFO)}可以看到在 __proc_info_begin 与 __proc_info_end 之间的是 .proc.info.init 段,这样就可以通过检索 .proc.info.init 段的定义位置来找到这些 __proc_info 结构的定义位置了,通过检索发现 .proc.info.init 段的定义位置在 arch/arm/mm/ 目录下的多个 proc-x.S 中,下边举例:
在 arch/arm/mm/proc-arm9tdmi.S 中
在 arch/arm/mm/proc-arm920.S 中
.section ".proc.info.init", #alloc.type__arm920_proc_info,#object__arm920_proc_info:.long0x41009200.long0xff00fff0.long PMD_TYPE_SECT | \PMD_SECT_BUFFERABLE | \PMD_SECT_CACHEABLE | \PMD_BIT4 | \PMD_SECT_AP_WRITE | \PMD_SECT_AP_READ.long PMD_TYPE_SECT | \PMD_BIT4 | \PMD_SECT_AP_WRITE | \PMD_SECT_AP_READinitfn__arm920_setup, __arm920_proc_info.longcpu_arch_name.longcpu_elf_name.longHWCAP_SWP | HWCAP_HALF | HWCAP_THUMB.longcpu_arm920_name.longarm920_processor_functions.longv4wbi_tlb_fns.longv4wb_user_fns#ifndef CONFIG_CPU_DCACHE_WRITETHROUGH.longarm920_cache_fns#else.longv4wt_cache_fns#endif.size__arm920_proc_info, . - __arm920_proc_info可以看到这些结构都是通过汇编语言进行定义的,虽然在 linux 源代码中有对 __proc_info 结构的C语言定义(如上文),但是 linux 内核代码并没有使用这个C语言定义的结构。由于此时系统仍然处于底层初始化阶段,并没有进行C语言执行环境初始化工作(例如初始堆栈空间等),所以无法执行C代码,也就无法引用C语言定义的结构体,只能通过汇编语言直接定义特定内存位置的值来还原结构体的数据分布。
至此,linux 内核对 processor id 的检查流程分析完毕
下边将分析 linux 内核在内核管理方面的初始化工作,首先解释 linux 内核对内存地址相关定义的几个值:
1、PAGE_OFFSET
2、TEXT_OFFSET
3、KERNAL_RAM_VADDR
这些值出现在 arch/arm/kernel/head.S 中:
其中 TEXT_OFFSET 与 PAGE_OFFSET 在链接脚本中已经被用来定义当前虚拟地址
#ifdef CONFIG_XIP_KERNEL. = XIP_VIRT_ADDR(CONFIG_XIP_PHYS_ADDR);#else. = PAGE_OFFSET + TEXT_OFFSET;#endif.head.text : {_text = .;HEAD_TEXT}查看 arch/arm/kernel/head.S 所包含的头文件,发现了与内存管理相关的头文件 asm/memory.h ,进而在此头文件中找到了 PAGE_OFFSET 的定义:
/* PAGE_OFFSET - the virtual address of the start of the kernel image */#define PAGE_OFFSETUL(CONFIG_PAGE_OFFSET)并在 arm 架构的 defconfig 文件中找到了 CONFIG_PAGE_OFFSET 选项(以 arch\arm\configs\imx_alientek_emmc_defconfig 为例):
CONFIG_PAGE_OFFSET=0x80000000而展开 UL 宏:
/** Allow for constants defined here to be used from assembly code* by prepending the UL suffix only with actual C code compilation.*/#define UL(x) _AC(x, UL) //arch/arm/include/asm/memory.h而 TEXT_OFFSET 的定义位置比较隐蔽,通过查看 MAKEFILE 输出,我们可以看到(或者也可以查看 cmd 文件 arch/arm/kernel/.head.o.cmd,其中的变量 cmd_arch/arm/kernel/head.o := arm-linux-gnueabihf-gcc …):
arm-linux-gnueabihf-gcc -Wp -WD,arch/arm/kernel/.head.o.d -nostdinc -isystem /home/ace/kernels/alpha/toolchain/gcc-linaro-4.9.4 ... (省略无关紧要的部分) -D__ASSEMBLY__ -D__KERNEL__ -D__LINUX_ARM_ARCH__=6 -DTEXT_OFFSET=0x00008000 ... (省略无关紧要的部分)通过上述命令看到通过 gcc 的 -D 选项将 TEXT_OFFSET 宏注入到代码中,进而可以查到这个宏的值的定义位置在 arch/arm/Makefile 中:
TEXT_OFFSET := $(textofs-y)同在一个文件中,使用这可以通过 config 选择 textofs-y 的值:
# Text offset. This list is sorted numerically by address in order to# provide a means to avoid/resolve conflicts in multi-arch kernels.textofs-y:= 0x00008000textofs-$(CONFIG_ARCH_CLPS711X) := 0x00028000# We don't want the htc bootloader to corrupt kernel during resumetextofs-$(CONFIG_PM_H1940) := 0x00108000# SA1111 DMA bug: we don't want the kernel to live in precious DMA-able memoryifeq ($(CONFIG_ARCH_SA1100),y)textofs-$(CONFIG_SA1111) := 0x00208000endiftextofs-$(CONFIG_ARCH_MSM8X60) := 0x00208000textofs-$(CONFIG_ARCH_MSM8960) := 0x00208000textofs-$(CONFIG_ARCH_AXXIA) := 0x00308000若不在 config 中开启这些奇怪的选项,则 textofs-y 默认的值即为 0x00008000,并且通过上文查看到的 arm-linux-gcc 命令可以看到,我在此次编译时并没有开启这些选项,使用了 textofs-y 的默认值 0x00008000。
现在已经找到了他们的定义位置及其值,下边将分析这些值的含义
to be continue
当清楚了这些值的含义后,可以继续分析 linux 内核的启动代码
__create_page_tables
__create_page_tables 定义在 arch/arm/kernel/head.S 中:
其中 pgtbl 宏的定义为(在 arch/arm/kernel/head.S 中):
.macropgtbl, rd, physadd\rd, \phys, #TEXT_OFFSETsub\rd, \rd, #PG_DIR_SIZE.endmswapper_pg_dir 的定义为(在 arch/arm/kernel/head.S 中):
.globlswapper_pg_dir.equswapper_pg_dir, KERNEL_RAM_VADDR - PG_DIR_SIZEPG_DIR_SIZE 的定义为(在 arch/arm/kernel/head.S 中):
#ifdef CONFIG_ARM_LPAE/* LPAE requires an additional page for the PGD */#define PG_DIR_SIZE0x5000#define PMD_ORDER3#else#define PG_DIR_SIZE0x4000#define PMD_ORDER2#endif未完待续