HateMath 发表于 2012-06-04 09:35

[原创]u-boot汇编代码探险指南.(另指出源码中的错误)

本帖最后由 HateMath 于 2012-06-26 16:23 编辑

u-boot汇编代码探险指南.

文章早写出来了,不过话说编辑这么长的帖子真辛苦,主要是给代码部分做标记,工作量大。今天心情好,搞一搞。


u-boot是一个典型的嵌入式bootloader,很多人都想一窥它源码真面目。在这个过程中,首先遇到的就是汇编代码的原始丛林,本文试图带领大家进入这片原始丛林,一睹传说中的神秘的“stage1”启动代码。

(一)准备工作

首先把环境交代一下。
1)u-boot-1.1.6(http://sourceforge.net/projects/u-boot/files/)
2)开发板为smdk2410(cpu为samsung s3c2410,arm920t的处理器核)

然后把涉及到的文件做个简写约定,以后方便叙述。cpu/arm920t/start.S             记为 startS
cpu/arm920t/interrupts.c      记为 intrptC
board/smdk2410/u-boot.lds       记为 ubootLDS
board/smdk2410/lowlevel_init.S记为 llinitS
lib_arm/board.c               记为 boardC 最后是用到的主要参考资料:
Uboot中start.S源码的指令级的详尽解析.作者:green-waste(at)163.com
s3c2410 datasheet

值得一提的是资料,写的很详细,具体解释了每一条汇编指令。如果说我的这篇文章的侧重点是带领大家在原始丛林中探路,让大家对这片丛林的猛兽、泥沼、陷阱有个明确的认识,那么资料的侧重点则是研究路上的每一株花花草草。这两篇文章是互补关系,所以严重推荐在看懂我这篇后再看一下资料,沾惹一下各种花花草草。

(二)代码分析
先说一下u-boot启动的大致流程吧,俯瞰一下这片原始丛林,然后就钻进去探路了。
一般来说,bootloader这种东西,其启动分为两个阶段,分别叫stage1和stage2。startS的汇编代码,基本上做的是stage1的事情,这是最底层、最基础也是最重要的事情。它指定了各种中断处理程序、初始化处理器、内存到合适的工作状态,并将自己拷贝到内存中,进行stage2的启动。
好,下面开始探路。友情提醒:跟紧我,别掉队,指南针在程序的世界无效。

1)第一站就挺险恶的,陷阱遍布。u-boot是从startS中的“_start: b reset”语句开始运行的,这可以从ubootLDS文件中的“ENTRY(_start)”这句看出来。而“_start: b reset”这句话和它下面的一堆代码其实是一个整体,它们是干什么的呢?

2)学过单片机的都知道,芯片上电的时候,大多从地址0开始取指令执行,而地址0附近往往有好多中断入口。ARM处理器也差不多,除了地址0处的复位中断外,还有未定义指令中断、软件中断、预取指令中断(有人管这些叫异常)等等,总之是各种倒霉的情况的处理集中营。程序员要告诉处理器出了这些倒霉情况时该怎么做。这就我们看到的:_start: b reset
ldr pc, _undefined_instruction
ldr pc, _software_interrupt
ldr pc, _prefetch_abort
ldr pc, _data_abort
ldr pc, _not_used
ldr pc, _irq
ldr pc, _fiq每一句的意思都是,如果出现某个中断,处理器就会将相应的处理程序的起始地址装入pc寄存器,执行相应的处理程序。至于处理器怎么知道哪个中断对应哪个处理程序,学过单片机的都知道,我就不唐僧了。我想起了早年的BASIC编程中,类似“出什么问题到预先设定的地方去解决”这种理念,就被称为陷阱技术,区别在于那是软件陷阱,我们正在谈的这个是正宗的硬件陷阱。

举例分析一下,比如出现了未定义指令中断,处理器就会执行“ldr pc, _undefined_instruction”这句,而_undefined_instruction代表哪个地址呢,在当前文件中已有定义:_undefined_instruction: .word undefined_instruction
_software_interrupt: .word software_interrupt
_prefetch_abort: .word prefetch_abort
_data_abort: .word data_abort
_not_used: .word not_used
_irq: .word irq
_fiq: .word fiq哦,原来是undefined_instruction这个东东的地址。在当前文件中搜索undefined_instruction:undefined_instruction:
get_bad_stack
bad_save_user_regs
bl do_undefined_instruction 原来这个东东是个子程序的标号。这个子程序有三句话,第一句“get_bad_stack”是个宏定义,当前文件中:.macro get_bad_stack
ldr r13, _armboot_start @ setup our mode stack
sub r13, r13, #(CONFIG_STACKSIZE+CFG_MALLOC_LEN)
sub r13, r13, #(CFG_GBL_DATA_SIZE+8) @ reserved a couple spots in abort stack

str lr, @ save caller lr / spsr
mrs lr, spsr
str lr,

mov r13, #MODE_SVC @ prepare SVC-Mode
@ msr spsr_c, r13
msr spsr, r13
mov lr, pc
movs pc, lr
.endm第二句“bad_save_user_regs”也是个宏定义,当前文件中:.macro bad_save_user_regs
sub sp, sp, #S_FRAME_SIZE
stmia sp, {r0 - r12} @ Calling r0-r12
ldr r2, _armboot_start
sub r2, r2, #(CONFIG_STACKSIZE+CFG_MALLOC_LEN)
sub r2, r2, #(CFG_GBL_DATA_SIZE+8) @ set base 2 words into abort stack
ldmia r2, {r2 - r3} @ get pc, cpsr
add r0, sp, #S_FRAME_SIZE @ restore sp_SVC
add r5, sp, #S_SP
mov r1, lr
stmia r5, {r0 - r3} @ save sp_SVC, lr_SVC, pc, cpsr
mov r0, sp
.endm第三句“do_undefined_instruction”,在intrptC文件中:void do_undefined_instruction (struct pt_regs *pt_regs)
{
printf ("undefined instruction\n");
show_regs (pt_regs);
bad_mode ();
}
void bad_mode (void)
{
panic ("Resetting CPU ...\n");
reset_cpu (0);
}上述流程就是大概意思就是:处理器在执行程序时出问题了,赶紧给现场拍照并通知程序员,最后试图重启。


3)按照上述分析,处理器上电复位时,会走到“b reset”这句。找”reset”:reset:
/*
* set the cpu to SVC32 mode
*/
mrs r0,cpsr
bic r0,r0,#0x1f
orr r0,r0,#0xd3
msr cpsr,r0有人说:哎,这段代码前面还有一大段代码你没分析呢。 别想太多,Follow me,你要是迷路了,只有一种可能,就是没有紧跟我。
这段代码的功能是设置处理器运行在SVC32模式。对照s3c2410的datasheet就知道那些乱七八糟的值是怎么得来的了,很简单的。至于为什么要设置为SVC32模式,文献有详细解释,简要的说:1)SVC32模式有各种访问资源的特权,方便干活 ;2)如果要启动linux系统,linux要求CPU必须处于SVC32模式。

好,继续往下走:

4)关闭看门狗/* turn off the watchdog */
#if defined(CONFIG_S3C2400)
# define pWTCON 0x15300000
# define INTMSK 0x14400008 /* Interupt-Controller base addresses */
# define CLKDIVN 0x14800014 /* clock divisor register */
#elif defined(CONFIG_S3C2410)
# define pWTCON 0x53000000
# define INTMSK 0x4A000008 /* Interupt-Controller base addresses */
# define INTSUBMSK 0x4A00001C
# define CLKDIVN 0x4C000014 /* clock divisor register */
#endif

#if defined(CONFIG_S3C2400) || defined(CONFIG_S3C2410)
ldr r0, =pWTCON
mov r1, #0x0
str r1, 分析:这段代码的功能是关闭看门狗。看门狗是处理器内部的一个硬件模块,是个好东西,但是这里用不着,要让丫闭嘴,不然会捣乱,比如咬人什么的。一开始那一堆密集的预编译,用来指定看门狗狗窝的住址,因为不同的家庭(处理器),狗窝的位置不一定相同,要分别指定一下,避免访问狗窝时误入了猪圈。将看门狗赋值为0(见datasheet),它就不叫唤了。

看,原始丛林也没想象中那么险恶嘛。继续:

5)设置中断和时钟/*
* mask all IRQs by setting all bits in the INTMR - default
*/
mov r1, #0xffffffff
ldr r0, =INTMSK
str r1,
# if defined(CONFIG_S3C2410)
ldr r1, =0x3ff
ldr r0, =INTSUBMSK
str r1,
# endif
/* FCLK:HCLK:PCLK = 1:2:4 */
/* default FCLK is 120 MHz ! */
ldr r0, =CLKDIVN
mov r1, #3
str r1,
#endif /* CONFIG_S3C2400 || CONFIG_S3C2410 */分析:这段代码的功能是禁止所有中断。跟关闭看门狗类似,都是按照datasheet的说明给特定寄存器赋特定的值罢了。
但是,这个里面我想说说自己的看法。我觉得”ldr r1, =0x3ff”这句话不妥,因为INTSUBMSK的bit10是INT_ADC,既然要禁止所有中断,为什么要单单留这个INT_ADC?难道开发者和它有一腿:P ? 我认为正确写法应该是”ldr r1, =0x7ff”。
6)处理器杂项初始化、板载内存初始化/*
* we do sys-critical inits only at reboot,
* not when booting from ram!
*/
#ifndef CONFIG_SKIP_LOWLEVEL_INIT
bl cpu_init_crit
#endif分析:这段的注释文字,我当初阅读源码的时候,曾花了一段时间去揣摩,主要是我一开始接受不了u-boot作为一个bootloader,居然还会被别的程序load。这段话的言下之意是,u-boot不总是从flash、usb、sd卡之类的设备中启动,有时候,u-boot会被别的程序加载到ram,从ram中开始运行(奇怪吧,自己是bootloader,却要别人去加载。但这种情况是存在的,README文件中有句话提到这种情况),这个时候,内存刷新参数之类的工作,肯定是别人已经做好了,u-boot就不要多此一举,而且最好别多此一举。
对于大部分情况,cpu_init_crit还是要被执行的。这个子程序在就在startS文件中,且看其内容:#ifndef CONFIG_SKIP_LOWLEVEL_INIT
cpu_init_crit:
/*
* flush v4 I/D caches
*/
mov r0, #0
mcr p15, 0, r0, c7, c7, 0 /* flush v3/v4 cache */
mcr p15, 0, r0, c8, c7, 0 /* flush v4 TLB */

/*
* disable MMU stuff and caches
*/
mrc p15, 0, r0, c1, c0, 0
bic r0, r0, #0x00002300 @ clear bits 13, 9:8 (--V- --RS)
bic r0, r0, #0x00000087 @ clear bits 7, 2:0 (B--- -CAM)
orr r0, r0, #0x00000002 @ set bit 2 (A) Align
orr r0, r0, #0x00001000 @ set bit 12 (I) I-Cache
mcr p15, 0, r0, c1, c0, 0

/*
* before relocating, we have to setup RAM timing
* because memory timing is board-dependend, you will
* find a lowlevel_init.S in your board directory.
*/
mov ip, lr
bl lowlevel_init
mov lr, ip
mov pc, lr
#endif /* CONFIG_SKIP_LOWLEVEL_INIT */上面的代码可以分为三个部分,第一部分flush(我想不出确切意思的中文翻译) 指令和数据缓存;第二部分是禁用内存管理单元MMU;第三部分设置内存参数,其中调用了文件llinitS 中的lowlevel_init子程序。内存设置完全是和板子相关的,放在 board/smdk2410/ 目录里正合适。

7)走到现在还能跟上吧?快结束了,不过,关键的地方到了。下面这段代码可谓承上启下、承前启后,很重要。#ifndef CONFIG_SKIP_RELOCATE_UBOOT
relocate: /* relocate U-Boot to RAM */
adr r0, _start /* r0 - current position of code */
ldr r1, _TEXT_BASE /* test if we run from flash or RAM */
cmp r0, r1 /* don't reloc during debug */
beq stack_setup

ldr r2, _armboot_start
ldr r3, _bss_start
sub r2, r3, r2 /* r2 - size of armboot */
add r2, r0, r2 /* r2 - source end address */

copy_loop:
ldmia r0!, {r3-r10} /* copy from source address */
stmia r1!, {r3-r10} /* copy to target address */
cmp r0, r2 /* until source end addreee */
ble copy_loop
#endif /* CONFIG_SKIP_RELOCATE_UBOOT */上面的代码中有不少没有见过的符号,还记得开始的时候我们跳过的那段代码吗?现在说明一下,那是(全局)变量的定义。_TEXT_BASE:
.word TEXT_BASE /* _TEXT_BASE的值被定义为TEXT_BASE,而TEXT_BASE的定义在ubootLDS文件中,其默认值为0x33f80000 */

.globl _armboot_start
_armboot_start:
.word _start /* 你可以把_armboot_start和_start理解为一回事 */

/*
* These are defined in the board-specific linker script.
*/
.globl _bss_start
_bss_start:
.word __bss_start

.globl _bss_end
_bss_end:
.word _endbss段放的是未初始化的全局变量。正如英文注释所说的,__bss_start和_end是链接的时候才确定的值,咱就甭操心了。只需要知道它们分别是bss段的起始和结束地址即可。#ifdef CONFIG_USE_IRQ
/* IRQ stack memory (calculated at run-time) */
.globl IRQ_STACK_START
IRQ_STACK_START:
.word 0x0badc0de

/* IRQ stack memory (calculated at run-time) */
.globl FIQ_STACK_START
FIQ_STACK_START:
.word 0x0badc0de
#endif正如英文注释所说的,IRQ_STACK_START和FIQ_STACK_START是运行时确定的值,咱也甭操心了。0x0badc0de这个数字是哪块地址?别揣摩了,那只是一个单词……
CONFIG_SKIP_RELOCATE_UBOOT这个宏,在我们这种情况下,是没有被定义的,也就是说,宏里面的代码会被执行。啥时候会被定义呢,比如u-boot已经被人加载到内存的时候,这段代码就不需要了。嗯,反应快的人已经猜到了,这段代码是将u-boot拷贝到内存的。

这段代码因为涉及到编译链接时的地址重定位问题,分析源码比较绕,让我们突破迷雾,直击本质,看看最后生成的u-boot文件中的汇编代码吧。命令:arm-linux-objdump -d u-boot | head -n 100找到 relocate这段:33f80094 relocate:
33f80094: e24f009c sub r0, pc, #156 ; 0x9c
33f80098: e51f1060 ldr r1, ; 33f80040 _TEXT_BASE
33f8009c: e1500001 cmp r0, r1
33f800a0: 0a000007 beq 33f800c4 stack_setup
33f800a4: e51f2068 ldr r2, ; 33f80044 _armboot_start
33f800a8: e51f3068 ldr r3, ; 33f80048 _bss_start
33f800ac: e0432002 sub r2, r3, r2
33f800b0: e0802002 add r2, r0, r2

33f800b4copy_loop:
33f800b4: e8b007f8 ldm r0!, {r3, r4, r5, r6, r7, r8, r9, sl}
33f800b8: e8a107f8 stmia r1!, {r3, r4, r5, r6, r7, r8, r9, sl}
33f800bc: e1500002 cmp r0, r2
33f800c0: dafffffb ble 33f800b4 copy_loop第一句,r0=pc-#156=(0×94+8)-#156=0;
第二句,r1==[(0x98+8)-#96]=,而地址处定义的值为0x33f80000(arm-linux-objdump -d cpu/arm920t/start.o可知).即r1=0x33f80000.

没看懂?觉得pc的值是33f800xx才对?我来解释一下。
首先要说明,链接器是尽职尽责的,程序员在configMK中定义了TEXT_BASE = 0x33F80000,即指定了代码段基址从0x33F80000开始,连接器认为该程序会被加载到0x33F80000处执行,于是很认真的对指令进行了地址重定位,所以u-boot文件中的指令地址都是大于0x33F80000的(有兴趣的arm-linux-objdump一下编译生成的”u-boot”文件),但,这不意味着pc寄存器就一定是0x33F8xxxx,这取决于程序实际从哪里启动。
按照预期,既然开发人员定义了TEXT_BASE,按理说应该加载到定义的地址后去运行,但是开发人员可能不按套路出牌,实际加载到哪个地址运行随具体情况而定。在我们的分析中,很不幸的,程序被烧写(加载)到norflash中并从这里开始运行,而norflash的起始地址为0×0,所以,程序刚开始是从0×0地址开始运行的。因此,运行到我们分析的这片代码时,pc的实际值是个很小的数,就像我分析的那样。

第三句,比较r0和r1是否相同,相同的话说明u-boot已经被加载到TEXT_BASE指定的地址了,否则,就把u-boot加载(拷贝)到TEXT_BASE指定的地址。

开始拷贝?不急。拷贝前还有些准备工作要做。拷贝有3要素:源地址、目标地址、要拷贝的数据长度。源地址r2====,而地址处定义的值为0×0.即r2=0×0.目标地址r1=0x33f80000,刚才分析过的。拷贝哪些东西呢?text段和bss段之间的内容,所以数据长度=BSS段起始地址-CS段起始地址=0x33f96938-0x33f80000=0×16938=大概90KB的样子。

现在可以开始了。从 copy_loop起的那段就是拷贝的代码,一个循环搬运的过程,没什么好说的。

拷贝完成后,痛苦的汇编代码即将结束,因为程序要飞到c语言编写的代码去啦,以后就好分析多了。

8)收尾工作往往令人厌倦,比如吃完饭后洗碗、打完球后刷鞋之类的。但收尾工作还是要做的,因为它也是下一阶段工作的开始。首尾工作分为两块,一个是为c语言中函数调用设置栈空间;另一个是清零bss段。/* Set up the stack */
stack_setup:
ldr r0, _TEXT_BASE /* upper 128 KiB: relocated u-boot */
sub r0, r0, #CFG_MALLOC_LEN /* malloc area */
sub r0, r0, #CFG_GBL_DATA_SIZE /* bdinfo */
#ifdef CONFIG_USE_IRQ
sub r0, r0, #(CONFIG_STACKSIZE_IRQ+CONFIG_STACKSIZE_FIQ)
#endif
sub sp, r0, #12 /* leave 3 words for abort-stack */说起来也简单,就是告诉处理器哪块内存可以当栈用而已。通过计算把栈的起始位置定下来,然后把这个地址赋值给sp寄存器。
首先r0=0x33f80000,然后去掉堆占用的空间,再去掉开发板所用数据结构占用的空间,如果使用IRQ,还要把IRQ用得空间也去掉。最后赋值给sp寄存器。显然,这个栈是向下增长的。
那么,CFG_MALLOC_LEN、CFG_GBL_DATA_SIZE、CONFIG_STACKSIZE、CONFIG_STACKSIZE_FIQ都是哪里定义的?答案尽在smdk2410H中。

有不少人思考问题很严谨,于是有人要问了,刚才的代码说是为c语言中函数调用设置栈空间,那么本篇开始时,有个c函数do_undefined_instruction的调用为什么不需要设置栈空间?
问得好。我们看看objdump后的uboot汇编代码:33f80020 _undefined_instruction:
33f80020: 33f80140 .word 0x33f80140
...
33f80140 undefined_instruction:
33f80140: e51fd104 ldr sp, ; 33f80044 _armboot_start
33f80144: e24dd805 sub sp, sp, #327680 ; 0x50000
33f80148: e24dd088 sub sp, sp, #136 ; 0x88
...栈的本质作用是为了保护现场,把待会要用到的数据放在一块内存里–当然,这块内存不能存在有用的数据,否则就被抹掉了,会发生不可预料的后果。可见,上面的代码里其实也是指定了栈的位置的,确切的说,是随便指定了一个位置,位置在-0x33f80000-0×50000-0×88。考虑到uboot启动时,系统处于混沌状态,这样的临时设置,破坏有用数据的可能性基本上没有,除非u-boot是被别的程序加载到内存中的,而那个加载u-boot的程序恰好用了那块内存,那真是躺着也中枪了…
因为bss段放的是未初始化的全局变量,所以,把这块内存初始化为0, 其实就是循环赋值0。clear_bss:
ldr r0, _bss_start /* find start of bss segment */
ldr r1, _bss_end /* stop here */
mov r2, #0x00000000 /* clear */

clbss_l:str r2, /* clear loop... */
add r0, r0, #4
cmp r0, r1
ble clbss_l9)看到丛林的出口啦! ldr pc, _start_armboot 程序跳转到_start_armboot。查找_start_armboot,在当前文件中发现其定义:_start_armboot: .word start_armboot,搜索start_armboot,在boardC文件中:void start_armboot (void)。C语言的函数!一阵感动哇。。。从汇编的暗黑世界里挣扎出来,发现c语言太可爱了。

至此,汇编语言部分(也就是传说中的stage1)分析完毕,c语言的大门就在眼前,我脑中突然出现了游戏《暗黑2》联网时的过场:一道充满光芒的门徐徐打开……

(全文完)作者:HateMath
原创首发于博客 “a HateMath Driver”
转载请注明出处:(http://hatemath.sinaapp.com/)

HateMath 发表于 2012-06-26 16:22

终于把格式编辑完了。文章有点长,不知有没有人能看完。

fayewangfans 发表于 2012-07-01 08:42

昨天才把这个程序看两遍,也看了些分析资料,问一下对于具体的板子lowlevel_init做了些什么工作

mulegame 发表于 2012-08-31 00:04

好文章,楼主辛苦了。呵呵。恭喜暗黑2通关。
我们还在不停砍怪,积累EXP中,感谢给出秘籍。
吃BOSS的方法已经给出,不过总是操作不好,容易被秒杀。

希望楼主有时间继续跟进暗黑3的通关方法。谢谢了!

哈哈 只能说嵌入式很神奇,要说抽大烟的上瘾,还真就别说,没有嵌入式这“海洛因”厉害!后劲儿大啊!!:lol
页: [1]
查看完整版本: [原创]u-boot汇编代码探险指南.(另指出源码中的错误)