不是每一行代码都必须读懂,我只是大概地过一下流程 毕竟这些都是比较成熟的代码,没必要去改的 是针对我自己的板子的,硬件配置如下 cpu是s3c2410 board type 是 smdk2410 16M Nor Flash 地址是 0x0---0xFFFFFF 64M SDRAM 地址是 0x30000000---0x33FFFFFF 软件是华恒版的 ppcboot 2.0 和 linux 2.4.18 仔细分析了一下启动的流程,能更好地理解硬件和软件的配合 方便移植。 我们在flash的开始处烧写了ppcboot.bin,这是可执行的二进制文件 注意和ELF可执行性文件是有区别的。 cpu上电后可以从直接从flash地址0处取指令来执行 开始的代码在ppcboot-2.0.0\cpu\arm920t\start.s中 这里需要提一下编译链接时用到的一个很重要的链接文件 ppcboot-2.0.0\board\smdk2410\ppcboot.lds 这个文件给出了代码中各标号的基地址,和各个段的链接顺序 ENTRY(_start) SECTIONS { . = 0x00000000;
. = ALIGN(4); .text : { cpu/arm920t/start.o (.text) *(.text) }
. = ALIGN(4); .rodata : { *(.rodata) }
. = ALIGN(4); .data : { *(.data) }
. = ALIGN(4); .got : { *(.got) }
armboot_end_data = .;
. = ALIGN(4); .bss : { *(.bss) }
armboot_end = .; } 可以看到程序的入口是_start标号指示的,而cpu/arm920t/start.o 则被安排在程序最开始的地方,这个标号就是在start.s中 但是还有一点是需要特别注意的,开始我也是因为这个地方而没有很好地理解程序 虽然lds中有 . = 0x00000000 这一句,指示链接基地址,不过其实这句是不起作用的, 真正的链接基地址在ppcboot-2.0.0\config.mk中指定的 LDFLAGS += -Bstatic -T $(LDSCRIPT) -Ttext $(TEXT_BASE) 其中-Ttext $(TEXT_BASE)就是指定链接地址为TEXT_BASE的值 因而是可变的,TEXT_BASE在ppcboot-2.0.0\board\smdk2410\config.mk中定义 TEXT_BASE = 0x33F00000 ppcboot-2.0.0\config.mk是包括到Makefile中的, 在Makefile中有$(LD) $(LDFLAGS) $(OBJS) $(LIBS) $(LIBS) -Map ppcboot.map -o ppcboot 所以说起来真正的链接地址是0x33F00000,其实这样在把ppcboot拷到Ram中就可以实现无缝跳转了 .globl _start _start: b reset 跳到renset reset: ldr r0, =pWTCON mov r1, #0x0 str r1, [r0] .......................... bl cpu_init_crit //bl跳转会回来 relocate: //下面开始要把ppcboot拷到Ram中 adr r0, _start /* r0 <- current position of code */ ldr r2, _armboot_start ldr r3, _armboot_end sub r2, r3, r2 /* r2 <- size of armboot */ ldr r1, _TEXT_BASE /* r1 <- destination address */ add r2, r0, r2 /* r2 <- source end address */ 以上代码需要注意的一点是 adr 和 ldr 的区别 adr取得是当前pc相关的偏移地址,在这里程序还是在flash中运行 所以取得地址是以0x0为基址的 而ldr取的是_armboot_start所指的值 .globl _armboot_start _armboot_start: .word _start 看到它的值也是_start的地址,不过我们这里取的是绝对地址,是在链接是确定的以 TEXT_BASE为基址的.由于_start的偏移是0,所以r0是0,r2就是TEXT_BASE copy_loop: ldmia r0!, {r3-r10} stmia r1!, {r3-r10} cmp r0, r2 ble copy_loop 循环copy ldr r0, _armboot_end /* set up the stack */ add r0, r0, #CONFIG_STACKSIZE sub sp, r0, #12 /* leave 3 words for abort-stack */
ldr pc, _start_armboot _start_armboot: .word start_armboot
//通过这一句跳转到ppcboot-2.0.0\lib_arm\board.c中的start_armboot函数去执行了 start_armboot的绝对地址也是以TEXT_BASE为基址的,所以可以顺利的实现无缝跳转了. 接着下来就是一系列初始化的工作了 首先定义了一个全局的数据结构 gd_t gd_data; DECLARE_GLOBAL_DATA_PTR 这个宏定义的是一个全局的gd_t类型的指针gd gd = &gd_data; 这样以后就可以用gd来访问gd_data这个数据结构了 for (init_fnc_ptr = init_sequence; *init_fnc_ptr; ++init_fnc_ptr) { if ((*init_fnc_ptr)() != 0) { hang (); } } init_fnc_ptr中是一系列初始化函数的指针 init_fnc_t *init_sequence[] = { cpu_init, /* basic cpu dependent setup */ board_init, /* basic board dependent setup */ interrupt_init, /* set up exceptions */ env_init, /* initialize environment */ init_baudrate, /* initialze baudrate settings */ serial_init, /* serial communications setup */ display_banner, dram_init, /* configure available RAM banks */ display_dram_config,
NULL, }; 基本上serial_init后我们就可以用printf函数来打印信息了. for (;;) { main_loop (); } 进入了主循环,在M:\ppcboot-2.0.0\common\main.c中 { char c = 'y'; unsigned long timedata; printf("start linux now(y/n):"); timedata = 0; for (;;) { while (!tstc()) { /* while no incoming data */ if (timedata++ > 3000 * 100 *3) goto bootm; /* timed out */ } c = getc(); } tstc()是测试串口是否有数据输入,显然没有的话就会等待time out跳出 bootm: if(c == 'y'||c == 'Y'){ strcpy(lastcommand , "bootm 30008000 30800000\r"); flag = 0; rc = run_command (lastcommand, flag); if (rc <= 0) { /* invalid command or not repeatable, forget it */ lastcommand[0] = 0; } } else{ printf("\n\n"); } } 这样如果串口没有输入或者输入时y Y 的话,就会去执行bootm 30008000 30800000\r这条命令 否则就会到ppcboot的命令行等待输入. 执行bootm 30008000 30800000\r这条命令会调用ppcboot-2.0.0\common\cmd_bootm.c中的 do_bootm函数,具体的命令怎样被分解,选择调用函数的机制我就不多说了,追着run_command去就是了 在do_bootm中调用了do_bootm_linux函数,这个函数在ppcboot-2.0.0\lib_arm\armlinux.c中 ret = memcpy((void *)0x30008000, (void *)0x40000, 0x100000); if (ret != (void *)0x30008000) printf("copy kernel failed\n"); else printf("copy kernel done\n");
ret = memcpy((void *)0x30800000, (void *)0x140000, 0x440000); if (ret != (void *)0x30800000) printf("haha failed\n"); else printf("copy ramdisk done\n");
首先把kernel和ramdisk都拷到Ram相应的地方去. setup_linux_param(0x30000000 + LINUX_PARAM_OFFSET); //也在armlinux.c中 #define LINUX_PARAM_OFFSET 0x100 建立要传给内核的参数,参数的地址都是固定的,所以内核也知道去这里取参数 参数格式比较复杂,我这里好像传得参数不多 void setup_linux_param(ulong param_base) { struct param_struct *params = (struct param_struct *)param_base; ............... } 只是通过一个param_struct的结构体来传参数的,不过现在一般都用另一种tag标记的传参方法 一个主要的参数时char linux_cmd[] = "initrd=0x30800000,0x440000 root=/dev/ram init=/linuxrc console=ttyS0"; if (linux_cmd == NULL) { printf("Wrong magic: could not found linux command line\n"); } else { memcpy(params->commandline, linux_cmd, strlen(linux_cmd) + 1); printf("linux command line is: \"%s\"\n", linux_cmd); } 是比较重要的.移植的时候常常需要修改 接着call_linux(0, 0xc1, 0x30008000); 看出来是准备调到linux去了 0xc1是machine type,这三个参数分别给了r0,r1,r2,这些都是调用内核的约定 void call_linux(long a0, long a1, long a2) { __asm__( "mov r0, %0\n" "mov r1, %1\n" "mov r2, %2\n" "mov ip, #0\n" "mcr p15, 0, ip, c13, c0, 0\n" /* zero PID */ "mcr p15, 0, ip, c7, c7, 0\n" /* invalidate I,D caches */ "mcr p15, 0, ip, c7, c10, 4\n" /* drain write buffer */ "mcr p15, 0, ip, c8, c7, 0\n" /* invalidate I,D TLBs */ "mrc p15, 0, ip, c1, c0, 0\n" /* get control register */ "bic ip, ip, #0x0001\n" /* disable MMU */ "mcr p15, 0, ip, c1, c0, 0\n" /* write control register */ "mov pc, r2\n" "nop\n" "nop\n" : /* no outpus */ : "r" (a0), "r" (a1), "r" (a2) ); } mov pc, r2 就是这句吧,调到了30008000去执行内核了
接下来就到内核了吧
arm linux 启动流程之 解压内核
Author-------Dansen-----xzd2734@163.com
从后往前看下编译生成zImage的过程,我们可以找到程序的入口还是那个很重要 链接文件,找到它,生成zImage所在的目录是kernel\arch\arm\boot\compressed\ Make过程为....ld -p -X -T vmlinux.lds head.o misc.o head-s3c2410.o piggy.o libgcc.o -o vmlinux 然后是用二进制工具objcopy把vmlinux制作成可执行的二进制映像文件zImage 这样在我们就去kernel\arch\arm\boot\compressed\目录下去找到vmlinux.lds文件 如果没有编译就不会有这个文件,因为它也是在编译过程生成的,由同一目录下的 vmlinux.lds.in生成,打开这个文件 ENTRY(_start) SECTIONS { . = LOAD_ADDR; _load_addr = .;
. = TEXT_START; _text = .;
.text : { _start = .; *(.start) *(.text) ........ 入口是_start,而且入口就直接定义在这个文件中了 入口直接接着.start段,所以程序开始是从.start段开始执行的 如果看看vmlinux.lds的生成过程就应该能找到LOAD_ADDR和TEXT_START的值 实际上这两个值是由其他两个变量赋给的 ZRELADDR 和 ZTEXTADDR 在kernel\arch\arm\boot\Makefile中我们可以找到这两个变量的值 ifeq ($(CONFIG_ARCH_S3C2410),y) ZTEXTADDR = 0x30008000 ZRELADDR = 0x30008000 endif 所以 LOAD_ADDR = 0x30008000 TEXT_START = 0x30008000 看一下vmlinux.lds吧 ENTRY(_start) SECTIONS { . = 0x30008000; _load_addr = .;
. = 0; _text = .; 显然LOAD_ADDR被赋值了0x30008000 看一下TEXT_START怎么成0了,我想这应该是一个偏移吧,偏移是0 所以它还是0x30008000 接着下来就从head.s来开始看代码吧 .section ".start", #alloc, #execinstr /* * sort out different calling conventions */ .align 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 这里一定就是程序的入口了,一般汇编程序的含义就看看英文注释就是了 有一个要注意的地方,不是一个汇编文件就是属于一个段的,不是说先执行完了 head.s再去执行head-s3c2410.s,还是要注意链接的段,显然head.s 不一会就开始了另一个段.text .text adr r0, LC0 ldmia r0, {r1, r2, r3, r4, r5, r6, ip, sp} subs r0, r0, r1 @ calculate the delta offset 而我们的head-s3c2410.s呢 .section ".start", #alloc, #execinstr __S3C2410_start: bic r2, pc, #0x1f add r3, r2, #0x4000 @ 16 kb is quite enough... 还是属于.start段的,所以顺序执行下来时先执行head-s3c2410.s,然后再去执行 .text段。head-s3c2410.s主要是cpu的一些初始化工作。接着下来我们会需要把内核 接压缩,先说说为什么吧。还是注意到上面生成zImage的文件中有一个piggy.o,往上 追寻可以看到是piggy.o由那个真正的内核vmlinux生成的,这个vmlinux才是启动后一直在 运行的内核,原本很大,压缩以后可以方便地放在flash中,当然其实不压缩跳到它的 入口也就可以运行了。解压的内核是准备从LOAD_ADDR = 0x30008000开始的4M空间,会覆盖 我们的当前运行的代码,那样就先把内核解压到我们这个zImage+分配堆栈0x10000的最后 cmp r4, r2 //r4 是LOAD_ADDR=0x30008000 bhs wont_overwrite //r2 是当前代码的最底部 这里当然不会跳转 add r0, r4, #4096*1024 @ 4MB largest kernel size cmp r0, r5 //r5 也是0x30008000 bls wont_overwrite //不会跳转
mov r5, r2 //r2是(user_stack+4096)在zImage的最后+0x10000 mov r0, r5 mov r3, r7 //machine type bl decompress_kernel 有了r5,r0,r7作为参数,就可以调用misc.c中的decompress_kernel函数进行解压缩了 这个函数调用的gunzip函数时gcc的库函数,所以在源码中找不到的 解压在r5开始的地方,函数返回的是r0解压得到的长度。这时候我们需要对代码经行调整 add r1, r5, r0 @ end of decompressed kernel adr r2, reloc_start ldr r3, LC1 //LC1: .word reloc_end - reloc_start add r3, r2, r3 1: ldmia r2!, {r8 - r13} @ copy relocation code stmia r1!, {r8 - r13} ldmia r2!, {r8 - r13} stmia r1!, {r8 - r13} cmp r2, r3 //这里就把从reloc_start到reloc_end这段我们需要的代码放到了 blo 1b //解压内核的最后,而在下面我们会将zImage都覆盖掉 bl cache_clean_flush add pc, r5, r0 //调到调整后的reloc_start,在decompressed kernel后 reloc_start: add r8, r5, r0 //r5解压内核开始的地方 r0解压内核的长度 debug_reloc_start mov r1, r4 //r4=0x30008000 1: .rept 4 ldmia r5!, {r0, r2, r3, r9 - r13} @ relocate kernel stmia r1!, {r0, r2, r3, r9 - r13} .endr
cmp r5, r8 blo 1b //这样就又把解压的真正内核移到了0x30008000处 call_kernel: bl cache_clean_flush bl cache_off mov r0, #0 mov r1, r7 @ restore architecture number mov pc, r4 @ call kernel 上面就是跳到0x30008000这里去执行真正的内核了吧
arm linux 启动流程之 进入内核
Author-------Dansen-----xzd2734@163.com
还是从编译链接生成vmlinux的过程来看吧,由一大堆.o文件链接而成,第一个就是 kernel\arch\arm\kernel\head-armv.o ,而且我们还看到了 lds链接文件kernel\arch\arm\vmlinux.lds,先把它分析一下 ENTRY(stext) //入口点是stext 应该就在head-armv.s中了 SECTIONS { . = 0xC0008000; //基址,是内核开始的虚拟地址 .init : { /* Init code and data */ _stext = .; __init_begin = .; *(.text.init) __proc_info_begin = .; *(.proc.info) __proc_info_end = .; __arch_info_begin = .; *(.arch.info) __arch_info_end = .; __tagtable_begin = .; *(.taglist) __tagtable_end = .; *(.data.init) . = ALIGN(16); __setup_start = .; *(.setup.init) __setup_end = .; __initcall_start = .; *(.initcall.init) __initcall_end = .; . = ALIGN(4096); __init_end = .; } 关于虚拟地址和物理地址的:使用MMU后,系统就会使用虚拟地址,通过MMU来指向 实际物理地址而在这里我们的0xC0008000实际物理地址就是0x30008000, 具体关于MMU的介绍参考《ARM体系结构与编程》。 到head-armv.s找到程序的入口 .section ".text.init",#alloc,#execinstr .type stext, #function ENTRY(stext) mov r12, r0 mov r0, #F_BIT | I_BIT | MODE_SVC @ make sure svc mode msr cpsr_c, r0 @ and all irqs disabled bl __lookup_processor_type teq r10, #0 @ invalid processor? moveq r0, #'p' @ yes, error 'p' beq __error bl __lookup_architecture_type teq r7, #0 @ invalid architecture? moveq r0, #'a' @ yes, error 'a' beq __error bl __create_page_tables adr lr, __ret @ return address add pc, r10, #12 @ initialise processor 来看看上一句跳到哪里去了 去追寻r10的值,是在__lookup_processor_type子函数中赋的 __lookup_processor_type: adr r5, 2f //r5 标号2的地址 基址是0x30008000 ldmia r5, {r7, r9, r10} //r7=__proc_info_end r9=__proc_info_begin sub r5, r5, r10 //r10 标号2的链接地址 基址是0xc0008000 add r7, r7, r5 @ to our address space add r10, r9, r5 //r10 变换为基址是0x30008000的__proc_info_begin 2: .long __proc_info_end .long __proc_info_begin .long 2b 这样r10中存放的是__proc_info_begin的地址,因为现在我们还没有打开MMU 所以还是需要把基址变换到0x30008000,接着我们就去找__proc_info_begin吧 注意到在上面的vmlinux.lds中有这个标号,下来链接的是.proc.info段, 在kernel\arch\arm\mm\proc-arm920.s的最后找到了这个段 .section ".proc.info", #alloc, #execinstr
.type __arm920_proc_info,#object __arm920_proc_info: .long 0x41009200 .long 0xff00fff0 .long 0x00000c1e @ mmuflags b __arm920_setup ok,这样我们就知道add pc, r10, #12跳到哪里去了,因为这个地址刚好放了条跳转语句 注意了b语句用的都是相对地址,所以不需要变换地址,反正是跳到__arm920_setup,而且 上一条语句是adr lr, __ret,设定了__arm920_setup的返回地址是__ret,所以执行完 __arm920_setup后回到head-armv.s的__ret标号继续执行. __ret: ldr lr, __switch_data mcr p15, 0, r0, c1, c0 //注意这里了,在这里打开了MMU mov r0, r0 mov r0, r0 mov r0, r0 mov pc, lr //跳到__mmap_switched,这里已经用了虚拟地址了吧 // 这条指令ldr lr, __switch_data加载的__mmap_switched地址就是虚拟地址啊 __switch_data: .long __mmap_switched 从__mmap_switched一路执行下来,就要调到C语言代码中去了 b SYMBOL_NAME(start_kernel) //在kernel\init\main.c中 这个程序不是特别复杂,细心看看还是能大概看懂,我也不能去一一注释 这里有一个流程图

到了C语言中就不是很难理解了 lock_kernel(); printk(linux_banner); setup_arch(&command_line); printk("Kernel command line: %s\n", saved_command_line); parse_options(command_line); trap_init(); init_IRQ(); sched_init(); softirq_init(); time_init(); 就是一大堆初始化工作,追着每个函数去看好了
start_kernel最后调用的一个函数 static void rest_init(void) { kernel_thread(init, NULL, CLONE_FS | CLONE_FILES | CLONE_SIGNAL); unlock_kernel(); current->need_resched = 1; cpu_idle(); } 用kernel_thread建立了一个init进程,执行的是main.c中的init函数 lock_kernel(); do_basic_setup(); 在do_basic_setup中调用了do_initcalls函数 各种驱动都是在do_initcalls(void)中完成的 static void __init do_initcalls(void) { initcall_t *call;
call = &__initcall_start; do { (*call)(); call++; } while (call < &__initcall_end);
flush_scheduled_tasks(); } __initcall_start也是在vmlinux.lds中赋值的,那就需要找到.initcall.ini这个段 在kernel\include\linux\init.h中可以找到 #define __init_call __attribute__ ((unused,__section__ (".initcall.init"))) typedef int (*initcall_t)(void); #define __initcall(fn) \ static initcall_t __initcall_##fn __init_call = fn 仔细研究下就发现这是把初始化函数的地址放到了.initcall.init段中 这样就可以不断调用驱动的初始化函数了 如果没有定义MODULE,那么#define module_init(x) __initcall(x); 所以如果要把驱动的编译进内核就很简单了吧 init的最后 if (execute_command) execve(execute_command,argv_init,envp_init); execute_command与ppcboot传的命令行参数是有关的哦,就是init=/linuxrc 这样就要去执行根目录下的linuxrc脚本,这个脚本会去执行busybox 而busybox又去执行/etc/init.d/rcS脚本,这个脚本又去执行/usr/etc/rc.local 完了
|