linux内核启动过程——基于S3C2410

发布者:熙风细雨最新更新时间:2016-06-16 来源: eefocus关键字:linux内核  启动过程  S3C2410 手机看文章 扫描二维码
随时随地手机看文章
本文以流行的Samsung公司的S3C2410,openmoko平台和linux-2.6.24为例,介绍如何在ZIX嵌入式开发环境下探索linux内核启动过程。

Linux内核启动一般由外部的bootloader引导,也可以在内核头部嵌入一个loader,实际的应用中这两种方式都会经常遇到。所以要了 解内核启动最开始的过程,必须对bootloader如何引导内核有所熟悉。下面我们从u-boot加载linux内核的代码开始分析(关于u-boot 自身的启动流程,请参考u-boot 启动过程 —— 基于S3C2410)。

在u-boot的do_bootm_linux函数里,实现了处理器架构相关的linux内核加载代码,特别是tags传递。

linux内核启动过程——基于S3C2410

该函数中,在lib_arm/bootm.c的76行调用了getenv将bootargs环境变量保存在commandline

char *commandline = getenv ("bootargs");

然后解析uImage文件头,并且按照头中的定义分解和加载uImage。所以这部分代码的运行取决于uImage文件是如何生成的,本文不做过多叙述,可参考另文了解u-boot使用。接下来进行tags设置工作,分别调用了

  • setup_start_tag()
  • setup_memory_tag()
  • setup_commandline_tag()
  • setup_initrd_tag()
  • setup_end_tag()

然后对TLB、cache等进行ivalid操作,这是通过在lib_arm/bootm.c的156行调用cleanup_before_linux()实现,然后即可跳入从uImage中分解出来的内核Image或zImage入口

cleanup_before_linux (); theKernel (0, machid, bd->bi_boot_params); /* does not return */ return;

在s3c2410平台上,该入口theKernel一般是物理地址0x30008000。如果我们使用zImage自解压内核映像,对应的代码正是 自解压头,位置在内核源码linux-2.6.24-moko-linuxbj的arch/arm/boot/compressed/start.S第 114行的start符号

start: .type start,#function .rept 8 mov r0, r0 .endr b 1f .word 0x016f2818 @ Magic numbers to help the loader .word start @ absolute load/run zImage address .word _edata @ zImage end address 1: mov r7, r1 @ save architecture ID mov r8, r2 @ save atags pointer

这也标志着u-boot将系统完全的交给了OS,bootloader生命终止。之后代码在133行会读取cpsr并判断是否处理器处于supervisor模式——从u-boot进入kernel,系统已经处于SVC32模式;而利用angel进入则处于user模式,还需要额外两条指令。之后是再次确认中断关闭,并完成cpsr写入

mrs r2, cpsr @ get current mode tst r2, #3 @ not user? bne not_angel mov r0, #0x17 @ angel_SWIreason_EnterSVC swi 0x123456 @ angel_SWI_ARM not_angel: mrs r2, cpsr @ turn off interrupts to orr r2, r2, #0xc0 @ prevent angel from running msr cpsr_c, r2

 然后在LC0地址处将分段信息导入r0-r6、ip、sp等寄存器,并检查代码是否运行在与链接时相同的目标地址,以决定是否进行处理。由于现在很少有人不使用loader和tags,将zImage烧写到rom直接从0x0位置执行,所以这个处理是必须的(但是zImage的头现在也保留了不用loader也可启动的能力)。arm架构下自解压头一般是链接在0x0地址而被加载到0x30008000运行,所以要修正这个变化。涉及到

  • r5寄存器存放的zImage基地址
  • r6和r12(即ip寄存器)存放的got(global offset table)
  • r2和r3存放的bss段起止地址
  • sp栈指针地址

很简单,这些寄存器统统被加上一个你也能猜到的偏移地址 0x30008000。该地址是s3c2410相关的,其他的ARM处理器可以参考下表

  • PXA2xx是0xa0008000
  • IXP2x00和IXP4xx是0x00008000
  • Freescale i.MX31/37是0x80008000
  • TI davinci DM64xx是0x80008000
  • TI omap系列是0x80008000
  • AT91RM/SAM92xx系列是0x20008000
  • Cirrus EP93xx是0x00008000

这些操作发生在代码172行开始的地方,下面只粘贴一部分

add r5, r5, r0 add r6, r6, r0 add ip, ip, r0

后面在211行进行bss段的清零工作

not_relocated: mov r0, #0 1: str r0, [r2], #4 @ clear bss str r0, [r2], #4 str r0, [r2], #4 str r0, [r2], #4 cmp r2, r3 blo 1b

 然后224行,打开cache,并为后面解压缩设置64KB的临时malloc空间

bl cache_on mov r1, sp @ malloc space above stack add r2, sp, #0x10000 @ 64k max

接下来238行进行检查,确定内核解压缩后的Image目标地址是否会覆盖到zImage头,如果是则准备将zImage头转移到解压出来的内核后面

cmp r4, r2 bhs wont_overwrite sub r3, sp, r5 @ > compressed kernel size add r0, r4, r3, lsl #2 @ allow for 4x expansion cmp r0, r5 bls wont_overwrite mov r5, r2 @ decompress after malloc space mov r0, r5 mov r3, r7 bl decompress_kernel

真实情况——在大多数的应用中,内核编译都会把压缩的zImage和非压缩的Image链接到同样的地址,s3c2410平台下即是0x30008000。这样做的好处是,人们不用关心内核是Image还是zImage,放到这个位置执行就OK,所以在解压缩后zImage头必须为真正的内核让路。

在250行解压完毕,内核长度返回值存放在r0寄存器里。在内核末尾空出128字节的栈空间用,并且使其长度128字节对齐。

add r0, r0, #127 + 128 @ alignment + stack bic r0, r0, #127 @ align the kernel length

算出搬移代码的参数:计算内核末尾地址并存放于r1寄存器,需要搬移代码原来地址放在r2,需要搬移的长度放在r3。然后执行搬移,并设置好sp指针指向新的栈(原来的栈也会被内核覆盖掉)

add r1, r5, r0 @ end of decompressed kernel adr r2, reloc_start ldr r3, LC1 add r3, r2, r3 1: ldmia r2!, {r9 - r14} @ copy relocation code stmia r1!, {r9 - r14} ldmia r2!, {r9 - r14} stmia r1!, {r9 - r14} cmp r2, r3 blo 1b add sp, r1, #128 @ relocate the stack

搬移完成后刷新cache,因为代码地址变化了不能让cache再命中被内核覆盖的老地址。然后跳转到新的地址继续执行

bl cache_clean_flush add pc, r5, r0 @ call relocation code

注意——zImage在解压后的搬移和跳转会给gdb调试内核带来麻烦。因为用来调试的符号表是在编译是生成的,并不知道以后会被搬移到何处去,只有在内核解压缩完成之后,根据计算出来的参数“告诉”调试器这个变化。以撰写本文时使用的zImage为例,内核自解压头重定向后,reloc_start地址由0x30008360变为0x30533e60。故我们要把vmlinux的符号表也相应的从0x30008000后移到0x30533b00开始,这样gdb就可以正确的对应源代码和机器指令。

随着头部代码移动到新的位置,不会再和内核的目标地址冲突,可以开始内核自身的搬移了。此时r0寄存器存放的是内核长度(严格的说是长度外加128Byte的栈),r4存放的是内核的目的地址0x30008000,r5是目前内核存放地址,r6是CPU ID,r7是machine ID,r8是atags地址。代码从501行开始

reloc_start: add r9, r5, r0 sub r9, r9, #128 @ do not copy the stack debug_reloc_start mov r1, r4 1: .rept 4 ldmia r5!, {r0, r2, r3, r10 - r14} @ relocate kernel stmia r1!, {r0, r2, r3, r10 - r14} .endr cmp r5, r9 blo 1b add sp, r1, #128 @ relocate the stack

接下来在516行清除并关闭cache,清零r0,将machine ID存入r1,atags指针存入r2,再跳入0x30008000执行真正的内核Image

call_kernel: bl cache_clean_flush bl cache_off mov r0, #0 @ must be zero mov r1, r7 @ restore architecture number mov r2, r8 @ restore atags pointer mov pc, r4 @ call kernel

 zImage自解压过程结束。

 

从zImage头跳转进来,此时的状态

  • MMU为off
  • D-cache为off
  • I-cache为dont care,on或off没有关系
  • r0为0
  • r1为machine ID
  • r2为atags指针

内核代码入口在linux-2.6.24-moko-linuxbj/arch/arm/kernel/head.S文件的83行。首先进入SVC32模式,并查询CPU ID,检查合法性

msr cpsr_c, #PSR_F_BIT | PSR_I_BIT | SVC_MODE @ ensure svc mode @ and irqs disabled mrc p15, 0, r9, c0, c0 @ get processor id bl __lookup_processor_type @ r5=procinfo r9=cpuid movs r10, r5 @ invalid processor (r5=0)? beq __error_p @ yes, error 'p'

接着在87行进一步查询machine ID并检查合法性

bl __lookup_machine_type @ r5=machinfo movs r8, r5 @ invalid machine (r5=0)? beq __error_a @ yes, error 'a'

其中__lookup_processor_type在linux-2.6.24-moko-linuxbj/arch/arm/kernel/head-common.S文件的149行,该函数首将标号3的实际地址加载到r3,然后将编译时生成的__proc_info_begin虚拟地址载入到r5,__proc_info_end虚拟地址载入到r6,标号3的虚拟地址载入到r7。由于adr伪指令和标号3的使用,以及__proc_info_begin等符号在linux-2.6.24-moko-linuxbj/arch/arm/kernel/vmlinux.lds而不是代码中被定义,此处代码不是非常直观,想弄清楚代码缘由的读者请耐心阅读这两个文件和adr伪指令的说明。

r3和r7分别存储的是同一位置标号3的物理地址(由于没有启用mmu,所以当前肯定是物理地址)和虚拟地址,所以儿者相减即得到虚拟地址和物理地址之间的offset。利用此offset,将r5和r6中保存的虚拟地址转变为物理地址

__lookup_processor_type: adr r3, 3f ldmda r3, {r5 - r7} sub r3, r3, r7 @ get offset between virt&phys add r5, r5, r3 @ convert virt addresses to add r6, r6, r3 @ physical address space

然后从proc_info中读出内核编译时写入的processor ID和之前从cpsr中读到的processor ID对比,查看代码和CPU硬件是否匹配(想在arm920t上运行为cortex-a8编译的内核?不让!)。如果编译了多种处理器支持,如versatile板,则会循环每种type依次检验,如果硬件读出的ID在内核中找不到匹配,则r5置0返回

1: ldmia r5, {r3, r4} @ value, mask and r4, r4, r9 @ mask wanted bits teq r3, r4 beq 2f add r5, r5, #PROC_INFO_SZ @ sizeof(proc_info_list) cmp r5, r6 blo 1b mov r5, #0 @ unknown processor 2: mov pc, lr

 __lookup_machine_type在linux-2.6.24-moko-linuxbj/arch/arm/kernel/head-common.S文件的197行,编码方法与检查processor ID完全一样,请参考前段

__lookup_machine_type: adr r3, 3b ldmia r3, {r4, r5, r6} sub r3, r3, r4 @ get offset between virt&phys add r5, r5, r3 @ convert virt addresses to add r6, r6, r3 @ physical address space 1: ldr r3, [r5, #MACHINFO_TYPE] @ get machine type teq r3, r1 @ matches loader number? beq 2f @ found add r5, r5, #SIZEOF_MACHINE_DESC @ next machine_desc cmp r5, r6 blo 1b mov r5, #0 @ unknown machine 2: mov pc, lr

代码回到head.S第92行,检查atags合法性,然后创建初始页表

bl __vet_atags bl __create_page_tables

 创建页表的代码在218行,首先将内核起始地址-0x4000到内核起始地址之间的16K存储器清0

__create_page_tables: pgtbl r4 @ page table address /* * Clear the 16K level 1 swapper page table */ mov r0, r4 mov r3, #0 add r6, r0, #0x4000 1: str r3, [r0], #4 str r3, [r0], #4 str r3, [r0], #4 str r3, [r0], #4 teq r0, r6 bne 1b

 然后在234行将proc_info中的mmu_flags加载到r7

ldr r7, [r10, #PROCINFO_MM_MMUFLAGS] @ mm_mmuflags

在242行将PC指针右移20位,得到内核第一个1MB空间的段地址存入r6,在s3c2410平台该值是0x300。接着根据此值存入映射标识

mov r6, pc, lsr #20 @ start of kernel section orr r3, r7, r6, lsl #20 @ flags + kernel base str r3, [r4, r6, lsl #2] @ identity mapping

完成页表设置后回到102行,为打开虚拟地址映射作准备。设置sp指针,函数返回地址lr指向__enable_mmu,并跳转到linux-2.6.24-moko-linuxbj/arch/arm/mm/proc-arm920.S的386行,清除I-cache、D-cache、write buffer和TLB

__arm920_setup: mov r0, #0 mcr p15, 0, r0, c7, c7 @ invalidate I,D caches on v4 mcr p15, 0, r0, c7, c10, 4 @ drain write buffer on v4 #ifdef CONFIG_MMU mcr p15, 0, r0, c8, c7 @ invalidate I,D TLBs on v4 #endif

然后返回head.S的158行,加载domain和页表,跳转到__turn_mmu_on

__enable_mmu: #ifdef CONFIG_ALIGNMENT_TRAP orr r0, r0, #CR_A #else bic r0, r0, #CR_A #endif #ifdef CONFIG_CPU_DCACHE_DISABLE bic r0, r0, #CR_C #endif #ifdef CONFIG_CPU_BPREDICT_DISABLE bic r0, r0, #CR_Z #endif #ifdef CONFIG_CPU_ICACHE_DISABLE bic r0, r0, #CR_I #endif mov r5, #(domain_val(DOMAIN_USER, DOMAIN_MANAGER) | / domain_val(DOMAIN_KERNEL, DOMAIN_MANAGER) | / domain_val(DOMAIN_TABLE, DOMAIN_MANAGER) | / domain_val(DOMAIN_IO, DOMAIN_CLIENT)) mcr p15, 0, r5, c3, c0, 0 @ load domain access register mcr p15, 0, r4, c2, c0, 0 @ load page table pointer b __turn_mmu_on

在194行把mmu使能位写入mmu,激活虚拟地址。然后将原来保存在sp中的地址载入pc,跳转到head-common.S的__mmap_switched,至此代码进入虚拟地址的世界

mov r0, r0 mcr p15, 0, r0, c1, c0, 0 @ write control reg mrc p15, 0, r3, c0, c0, 0 @ read id reg mov r3, r3 mov r3, r3 mov pc, r13

在head-common.S的37行开始清除内核bss段,processor ID保存在r9,machine ID报存在r1,atags地址保存在r2,并将控制寄存器保存到r7定义的内存地址。接下来跳入linux-2.6.24-moko-linuxbj/init/main.c的507行,start_kernel函数。这里只粘贴部分代码

__mmap_switched: adr r3, __switch_data + 4 ldmia r3!, {r4, r5, r6, r7} cmp r4, r5 @ Copy data segment if needed 1: cmpne r5, r6 ldrne fp, [r4], #4 strne fp, [r5], #4 bne 1b

在main.c第507行,是硬件无关的C初始化代码

asmlinkage void __init start_kernel(void) { char * command_line; extern struct kernel_param __start___param[], __stop___param[]; smp_setup_processor_id();

s3c2410平台linux-2.6.24内核早期的汇编初始化到这里就结束了

调试技巧:

利用gdb调试内核Image启动流程是一种很好的分析手段,要使用好这种手段有一个问题需要解决——内核地址映射问题

  1. 代码执行的早期是处于mmu关闭的状态下,软件直接使用硬件相关的物理地址(s3c2410的ram从0x30000000开始);
  2. 后来启用mmu并建立映射之后,软件使用虚拟地址。在3G用户空间配置下(大多数32位嵌入式系统都是此配置),内核使用的PAGE_OFFSET为0xc0000000,与硬件无关,核心虚拟地址变为从0xc0000000开始;

调试器无法自动接受这样的地址转变,需要使用上文介绍的诀窍,手工“告诉”调试器该怎么做。

对内核编译产生的vmlinux文件使用objdump工具

$ /usr/local/linuxbj/eabi-glibc/arm/bin/arm-linuxbj-linux-gnueabi-objdump -t vmlinux|more vmlinux: file format elf32-littlearm SYMBOL TABLE: c0008000 l d .text.head 00000000 .text.head c0008240 l d .init 00000000 .init c0027000 l d .text 00000000 .text c03377ec l d .notes 00000000 .notes c0338000 l d __ksymtab 00000000 __ksymtab c033ca40 l d __ksymtab_gpl 00000000 __ksymtab_gpl c033e2d0 l d __ksymtab_gpl_future 00000000 __ksymtab_gpl_future c033e2e8 l d __ksymtab_strings 00000000 __ksymtab_strings c034c9ec l d __param 00000000 __param c034e000 l d .data 00000000 .data c0373e20 l d .bss 00000000 .bss

可以看到内核符号表的.text链接虚拟地址是0xc0027000,所以在mmu处于关闭的阶段中,应该将内核符号表在调试器里加载到0x30027000地址。使得head.S入口.text.head正好是0x30008000,与实际的内存一致。

关键字:linux内核  启动过程  S3C2410 引用地址:linux内核启动过程——基于S3C2410

上一篇:ARM Linux 的启动过程
下一篇:ARM linux的启动部分源代码简略分析

推荐阅读最新更新时间:2024-03-16 14:57

关于s3c2410 中断异常处理
s3c2410的中断异常处理模块总共由以下寄存器构成 SRCPND(SOURCE PENDING REGISTER) INTMOD(INTERRUPT MODE REGISTER) INTMSK(INTERRUPT MASK REGISTER) PRIORITY( PRIORITY REGISTER) INTPND(INTERRUPT PENDING REGISTER) INTOFFSET(INTERRUPT OFFSET REGISTER) SUBSRCPND (INTERRUPT SUB SOURCE PENDING) INTSUBMSK (INTERRUPT SUB MASK REGISTER) 下面我将讲解每个寄存器在一
[单片机]
浅谈STM32的启动过程
浅谈STM32的启动过程 分享这篇文章,谈一下STM32启动流程。如果读者朋友已经有过汇编相关基础,能够更好理解本文内容。汇编语言是比C语言更接近机器底层的编程语言,能让我们更好的理解和操纵硬件底层。 STM32三种启动模式 下好程序后,重启芯片时,SYSCLK的第4个上升沿,BOOT引脚的值将被锁存,这就是所谓的启动过程。 STM32上电或者复位后,代码区始终从0x00000000开始,其实就是将存储空间的地址映射到0x00000000中。三种启动模式如下: 从 主闪存存储器 启动,将主Flash地址0x08000000映射到0x00000000,这样代码启动之后就相当于从0x08000000开始。主闪存存储器是STM32内
[单片机]
同步电动机异步全压启动过程的转矩分析
  同步电动机广泛应用在工农业生产恒速系统中,具有自由调节功率因数、转速恒定、负载特性硬等优点。但是,长期以来,同步电动机启动困难是限制它广泛应用的一个重要原因。全压异步直接启动方式因其操作最简单、方便,而在工程实践中得到了广泛的应用,是当前同步电动机普遍采用的一种启动方法。但是,由于励磁绕组在启动过程中产生的单轴转矩在半同步转速(简称半速)附近出现较大的起制动作用的转矩,使得合成的启动转矩曲线出现较大的下凹,在大于半速附近形成最小转矩,影响电动机带重载时的正常启动,使得启动时间延长,甚至会使得电动机卡在半速状态,使启动失败。本文详细分析了单轴转矩随转速变化的特性及对同步电动机启动过程的影响。 异步全压直接启动过程分析   同步电动
[嵌入式]
s3c2410板上移植2.6 kernel
最近这几天的晚上我在移植2.6的kernel。因为水平很菜,所以不太顺利。 我用的是kernel是2.6.14.2, 这个内核直接支持S3C2410的板子,不必打任何补丁。从 www.kernel.org 下载了内核。然后找来了3.4.1的GCC交叉编译器(经典的2.95.3编译2.6内核会出错)。解开内核开始修改。 (该部分转贴自: http://superlp.blogchina.com/blog/1391393.html 感谢superlp) 1. 增加nand分区信息 打开arch/arm/mach-s3c2410/devs.c 增加头文件 #include linux/mtd/partitions.h
[单片机]
嵌入式Linux内核调试技术
近年处理器技术发展速度加快,嵌入式领域发生了翻天覆地的变化。特别是网络的普及,消费电子异军突起,嵌入式与互联网成为最热门的技术。在所有操作系统中,Linux是发展很快、应用很广泛的一种操作系统。Linux的开放性以及其他优秀特性使其成为嵌入式系统开发的首选。 嵌入式系统开发所面临的问题 嵌入式软件开发有别于桌面软件系统开发的一个显著的特点是,一般需要一个交叉编译和调试环境,即编辑和编译软件在主机上进行,编译好的软件需要下载到目标机上运行 ,主机和目标机之间建立起通讯连接,并传输调试命令和数据。由于主机和目标机往往运行着不同的操作系统,而且处理器的体系结构也彼此不同,这就提高了嵌入式开发的复杂性。 总的来说,嵌入式开发所面临的问题
[嵌入式]
linux内核中的copy_to_user和copy_from_user(二)
linux内核中的copy_to_user和copy_from_user(二) 图解__arch_copy_from_user Kernel version:2.6.14 CPU architecture:ARM920T Author:ce123(http://blog.csdn.net/ce123) __arch_copy_from_user函数实现数据的拷贝,当地址没有按4字节对齐,拷贝数据时需要进行字节组合,拷贝大量数据时速度会非常慢。该函数的过程如下图所示。下图有点模糊,大家可以先下载( http://download.csdn.net/detail/ce123/4973958 )到自己电脑再看。
[单片机]
<font color='red'>linux内核</font>中的copy_to_user和copy_from_user(二)
基于S3C2410的辅助倒车数字图象系统设计
1 、引言   据统计,由于车后盲区所造成的交通事故在中国约占30%,美国20%。前两代倒车辅助产品,一种是倒车喇叭,一种是倒车雷达。前者只能提醒路人自行躲闪,而司机却一无所知,固定的障碍物更是无法探测,起到的作用微乎其微,后者虽能把固定的障碍物通过报警的形式告知司机,但司机还是无法判断障碍物的确切位置,更不能探测地坑或低矮障碍物。   目前,国内外的研究趋势是在倒车雷达的基础上采用数字图像处理技术,利用强大的嵌入式处理器,开发用于检测车后物距和监视车后图像的优点相结合的车载可视倒车装置。   因此本文提出一种基于S3C2410的辅助倒车系统设计,该系统不但使驾驶员可以在车内观察到汽车车尾的真实场景,而且可以通过系统所带的
[汽车电子]
基于<font color='red'>S3C2410</font>的辅助倒车数字图象系统设计
你知道嵌入式Linux内核?文件系统的制作也是有密切关联的
嵌入式Linux系统由 Linux内核 + 根文件系统 两部分组成 一个完整的嵌入式Linux系统组成:bootloader + boot parameters + kernel + root filesystem 嵌入式Linux系统使用的是Linux内核,制作方法基本和X86平台的Linux内核一致,下面介绍制作运行于micro2440开发板的内核和根文件系统。 嵌入式Linux内核的制作: 1,到www.kernel.org下载源代码,解压,进入内核源代码目录。 2,清除原有的配置与中间文件 x86: make distclean arm: make distclean 3,配置内核: x86: make me
[单片机]
小广播
添点儿料...
无论热点新闻、行业分析、技术干货……
设计资源 培训 开发板 精华推荐

最新单片机文章
何立民专栏 单片机及嵌入式宝典

北京航空航天大学教授,20余年来致力于单片机与嵌入式系统推广工作。

换一换 更多 相关热搜器件
电子工程世界版权所有 京B2-20211791 京ICP备10001474号-1 电信业务审批[2006]字第258号函 京公网安备 11010802033920号 Copyright © 2005-2024 EEWORLD.com.cn, Inc. All rights reserved