2014-04-07 21:18:34 simanstar 阅读数 0
第一部分 Linux下ARM汇编语法
尽管在Linux下使用C或C++编写程序很方便,但汇编源程序用于系统最基本的初始化,如初始化堆栈指针、设置页表、操作ARM的协处理器等。初始化完成后就可以跳转到C代码执行。需要注意的是,GNU的汇编器遵循AT&T的汇编语法,可以从GNU的站点(www.gnu.org)上下载有关规范。

一. Linux汇编行结构
任何汇编行都是如下结构:
[:] [} @ comment
[:] [} @ 注释
Linux ARM 汇编中,任何以冒号结尾的标识符都被认为是一个标号,而不一定非要在一行的开始。
【例1】定义一个"add"的函数,返回两个参数的和。
.section .text, “x”
.global add @ give the symbol add external linkage
add:
ADD r0, r0, r1  @ add input arguments
MOV pc, lr @ return from subroutine
@ end of program


二. Linux 汇编程序中的标号
标号只能由a~z,A~Z,0~9,“.”,_等字符组成。当标号为0~9的数字时为局部标号,局部标号可以重复出现,使用方法如下:
 标号f: 在引用的地方向前的标号
 标号b: 在引用的地方向后的标号
【例2】使用局部符号的例子,一段循环程序
1:
    subs r0,r0,#1        @每次循环使r0=r0-1
    bne 1f         @跳转到1标号去执行
局部标号代表它所在的地址,因此也可以当作变量或者函数来使用。


三. Linux汇编程序中的分段
(1).section伪操作
用户可以通过.section伪操作来自定义一个段,格式如下:
 .section section_name [, "flags"[, %type[,flag_specific_arguments]]]
每一个段以段名为开始, 以下一个段名或者文件结尾为结束。这些段都有缺省的标志(flags),连接器可以识别这些标志。(与armasm中的AREA相同)。

下面是ELF格式允许的段标志
<标志> 含义
a 允许段
w 可写段
x 执行段

【例3】定义段
 .section .mysection @自定义数据段,段名为 “.mysection”
 .align  2
 strtemp:
 .ascii  "Temp string \n\0"


(2)汇编系统预定义的段名
.text  @代码段
.data  @初始化数据段
.bss  @未初始化数据段
.sdata @
.sbss  @
需要注意的是,源程序中.bss段应该在.text之前。
四. 定义入口点
汇编程序的缺省入口是 start标号,用户也可以在连接脚本文件中用ENTRY标志指明其它入口点。
【例4】定义入口点
.section.data
< initialized data here>
.section .bss
< uninitialized data here>
.section .text
.globl _start
_start:
<instruction code goes here>


五. Linux汇编程序中的宏定义
格式如下:
 .macro 宏名 参数名列表   @伪指令.macro定义一个宏
   宏体
 .endm  @.endm表示宏结束
如果宏使用参数,那么在宏体中使用该参数时添加前缀“\”。宏定义时的参数还可以使用默认值。
可以使用.exitm伪指令来退出宏。
【例5】宏定义
.macro SHIFTLEFT a, b
.if \b < 0
MOV \a, \a, ASR #-\b
.exitm
.endif
MOV \a, \a, LSL #\b
.endm


六. Linux汇编程序中的常数
(1)十进制数以非0数字开头,如:123和9876;
(2)二进制数以0b开头,其中字母也可以为大写;
(3)八进制数以0开始,如:0456,0123;
(4)十六进制数以0x开头,如:0xabcd,0X123f;
(5)字符串常量需要用引号括起来,中间也可以使用转义字符,如: “You are welcome!\n”;
(6)当前地址以“.”表示,在汇编程序中可以使用这个符号代表当前指令的地址;
(7)表达式:在汇编程序中的表达式可以使用常数或者数值, “-”表示取负数, “~”表示取补,“<>”表示不相等,其他的符号如:+、-、*、 /、%、<、<<、>、>>、|、&、^、!、==、>=、<=、&&、|| 跟C语言中的用法相似。


七. Linux下ARM汇编的常用伪操作
在前面已经提到过了一些为操作,还有下面一些为操作:
>> 数据定义伪操作: .byte,.short,.long,.quad,.float,.string/.asciz/.ascii,重复定义伪操作.rept,赋值语句.equ/.set ;
>> 函数的定义 ;
>> 对齐方式伪操作 .align;
>> 源文件结束伪操作.end;
>> .include伪操作;
>> if伪操作;
>> .global/ .globl 伪操作 ;
>> .type伪操作 ;
>> 列表控制语句 ;
>> 区别于gas汇编的通用伪操作,下面是ARM特有的伪操作 :.reg ,.unreq ,.code ,.thumb ,.thumb_func ,.thumb_set, .ltorg ,.pool
1. 数据定义伪操作
(1) .byte:单字节定义,如:.byte 1,2,0b01,0x34,072,'s' ;
(2) .short:定义双字节数据,如:.short 0x1234,60000 ;
(3) .long:定义4字节数据,如:.long 0x12345678,23876565
(4) .quad:定义8字节,如:.quad 0x1234567890abcd
(5) .float:定义浮点数,如:
  .float 0f-314159265358979323846264338327\
    95028841971.693993751E-40                 @ - pi
(6) .string/.asciz/.ascii:定义多个字符串,如:
   .string "abcd", "efgh", "hello!"
   .asciz "qwer", "sun", "world!"
   .ascii "welcome\0"
需要注意的是:.ascii伪操作定义的字符串需要自行添加结尾字符'\0'。
(7) .rept:重复定义伪操作, 格式如下:
               .rept 重复次数
               数据定义
               .endr  @结束重复定义
     例如:
                .rept 3
                .byte 0x23
                .endr
(8) .equ/.set: 赋值语句, 格式如下:
                .equ(.set) 变量名,表达式
     例如:
                .equ abc 3  @让abc=3

2.函数的定义伪操作
(1)函数的定义,格式如下:
         函数名:
         函数体
         返回语句
一般的,函数如果需要在其他文件中调用, 需要用到.global伪操作将函数声明为全局函数。为了不至于在其他程序在调用某个C函数时发生混乱,对寄存器的使用我们需要遵循APCS准则。函数编译器将处理为函数代码为一段.global的汇编码。
(2)函数的编写应当遵循如下规则:
>> a1-a4寄存器(参数、结果或暂存寄存器,r0到r3 的同义字)以及浮点寄存器f0-f3(如果存在浮点协处理器)在函数中是不必保存的;
>> 如果函数返回一个不大于一个字大小的值,则在函数结束时应该把这个值送到 r0 中;
>> 如果函数返回一个浮点数,则在函数结束时把它放入浮点寄存器f0中;
>> 如果函数的过程改动了sp(堆栈指针,r13)、fp(框架指针,r11)、sl(堆栈限制,r10)、lr(连接寄存器,r14)、v1-v8(变量寄存器,r4 到 r11)和 f4-f7,那么函数结束时这些寄存器应当被恢复为包含在进入函数时它所持有的值。

3. .align .end .include .incbin伪操作
(1).align:用来指定数据的对齐方式,格式如下:
                .align [absexpr1, absexpr2]
     以某种对齐方式,在未使用的存储区域填充值. 第一个值表示对齐方式,4, 8,16或     32. 第二个表达式值表示填充的值。
(2).end:表明源文件的结束。
(3).include:可以将指定的文件在使用.include 的地方展开,一般是头文件,例如:
                .include “myarmasm.h”
(4).incbin伪操作可以将原封不动的一个二进制文件编译到当前文件中,使用方法如下:
            .incbin "file"[,skip[,count]]
     skip表明是从文件开始跳过skip个字节开始读取文件,count是读取的字数.

4. .if伪操作
根据一个表达式的值来决定是否要编译下面的代码, 用.endif伪操作来表示条件判断的结束, 中间可以使用.else来决定.if的条件不满足的情况下应该编译哪一部分代码。
.if有多个变种:
 .ifdef symbol           @判断symbol是否定义
 .ifc string1,string2   @字符串string1和string2是否相等,字符串可以用单引号括起来
 .ifeq expression       @判断expression的值是否为0
.ifeqs string1,string2  @判断string1和string2是否相等,字符 串必须用双引号括起来
.ifge expression         @判断expression的值是否大于等于0
.ifgt absolute expression  @判断expression的值是否大于0
.ifle expression         @判断expression的值是否小于等于0
.iflt absolute expression  @判断expression的值是否小于0
.ifnc string1,string2     @判断string1和string2是否不相等, 其用法跟.ifc恰好相反。
.ifndef symbol, .ifnotdef symbol  @判断是否没有定义symbol,  跟.ifdef恰好相反
.ifne expression          @如果expression的值不是0, 那么编译器将编译下面的代码
.ifnes string1,string2    @如果字符串string1和string2不相 等, 那么编译器将编译下面的代码.

5. .global  .type   .title   .list
(1).global/ .globl :用来定义一个全局的符号,格式如下:
       .global symbol  或者  .globl symbol
(2).type:用来指定一个符号的类型是函数类型或者是对象类型, 对象类型一般是数据, 格式如下:
            .type 符号, 类型描述
【例6】
.globl a
.data
.align 4
.type a, @object
.size a, 4
a:
.long 10
【例7】
.section .text
.type asmfunc, @function
.globl asmfunc
asmfunc:

mov pc, lr

(3)列表控制语句:
.title:用来指定汇编列表的标题,例如:
            .title “my program”
.list:用来输出列表文件.

6. ARM特有的伪操作
(1) .reg: 用来给寄存器赋予别名,格式如下:
                   别名 .req 寄存器名
(2) .unreq: 用来取消一个寄存器的别名,格式如下:
       .unreq 寄存器别名
  注意被取消的别名必须事先定义过,否则编译器就会报错,这个伪操作也可以用来取消系统预制的别名, 例如r0, 但如果没有必要的话不推荐那样做。
(3) .code伪操作用来选择ARM或者Thumb指令集,格式如下:
           .code 表达式
  如果表达式的值为16则表明下面的指令为Thumb指令,如果表达式的值为32则表明下面的指令为ARM指令.
(4) .thumb伪操作等同于.code 16, 表明使用Thumb指令, 类似的.arm等同于.code 32
(5) .force_thumb伪操作用来强制目标处理器选择thumb的指令集而不管处理器是否支持
(6) .thumb_func伪操作用来指明一个函数是thumb指令集的函数
(7) .thumb_set伪操作的作用类似于.set, 可以用来给一个标志起一个别名, 比.set功能增加的一点是可以把一个标志标记为thumb函数的入口, 这点功能等同于.thumb_func
(8) .ltorg用于声明一个数据缓冲池(literal pool)的开始,它可以分配很大的空间。
(9) .pool的作用等同.ltorg
(9).space <number_of_bytes> {,<fill_byte>}
分配number_of_bytes字节的数据空间,并填充其值为fill_byte,若未指定该值,缺省填充0。(与armasm中的SPACE功能相同)
(10).word <word1> {,<word2>} …
插入一个32-bit的数据队列。(与armasm中的DCD功能相同)
可以使用.word把标识符作为常量使用
 例如:
  Start:
  valueOfStart:
   .word Start
 这样程序的开头Start便被存入了内存变量valueOfStart中。
(11).hword <short1> {,<short2>} …
插入一个16-bit的数据队列。(与armasm中的DCW相同)

八. GNU ARM汇编特殊字符和语法
代码行中的注释符号: ‘@’
整行注释符号: ‘#’
语句分离符号: ‘;’
直接操作数前缀: ‘#’ 或 ‘$’


第二部分 GNU的编译器和调试工具

一. 编译工具
1.编辑工具介绍
GNU提供的编译工具包括汇编器as、C编译器gcc、C++编译器g++、连接器ld和二进制转换工具objcopy。基于ARM平台的工具分别为arm-linux-as、arm-linux-gcc、arm-linux-g++、arm- linux-ld和arm-linux- objcopy。GNU的编译器功能非常强大,共有上百个操作选项,这也是这类工具让初学者头痛的原因。不过,实际开发中只需要用到有限的几个,大部分可以采用缺省选项。GNU工具的开发流程如下:编写C、C++语言或汇编源程序,用gcc或g++生成目标文件,编写连接脚本文件,用连接器生成最终目标文件(elf格式),用二进制转换工具生成可下载的二进制代码。
(1)编写C、C++语言或汇编源程序
通常汇编源程序用于系统最基本的初始化,如初始化堆栈指针、设置页表、操作ARM的协处理器等。初始化完成后就可以跳转到C代码执行。需要注意的是,GNU的汇编器遵循AT&T的汇编语法,读者可以从GNU的站点(www.gnu.org)上下载有关规范。汇编程序的缺省入口是 start标号,用户也可以在连接脚本文件中用ENTRY标志指明其它入口点(见下文关于连接脚本的说明)。

(2)用gcc或g++生成目标文件
如果应用程序包括多个文件,就需要进行分别编译,最后用连接器连接起来。如笔者的引导程序包括3个文件:init.s(汇编代码、初始化硬件)xmrecever.c(通信模块,采用Xmode协议)和flash.c(Flash擦写模块)。
分别用如下命令生成目标文件: arm-linux-gcc-c-O2-oinit.oinit.s arm-linux-gcc-c-O2-oxmrecever.oxmrecever.c arm-linux-gcc-c-O2-oflash.oflash.c 其中-c命令表示只生成目标代码,不进行连接;-o命令指明目标文件的名称;-O2表示采用二级优化,采用优化后可使生成的代码更短,运行速度更快。如果项目包含很多文件,则需要编写makefile文件。关于makefile的内容,请感兴趣的读者参考相关资料。
(3)编写连接脚本文件
gcc 等编译器内置有缺省的连接脚本。如果采用缺省脚本,则生成的目标代码需要操作系统才能加载运行。为了能在嵌入式系统上直接运行,需要编写自己的连接脚本文件。编写连接脚本,首先要对目标文件的格式有一定了解。GNU编译器生成的目标文件缺省为elf格式。elf文件由若干段(section)组成,如不特殊指明,由C源程序生成的目标代码中包含如下段:.text(正文段)包含程序的指令代码;.data(数据段)包含固定的数据,如常量、字符串;.bss(未初始化数据段)包含未初始化的变量、数组等。C++源程序生成的目标代码中还包括.fini(析构函数代码)和. init(构造函数代码)等。连接器的任务就是将多个目标文件的.text、.data和.bss等段连接在一起,而连接脚本文件是告诉连接器从什么地址开始放置这些段。例如连接文件link.lds为:
ENTRY(begin)
SECTION
{
.=0x30000000;
.text:{*(.text)}
.data:{*(.data)}
.bss:{*(.bss)}
}
其中,ENTRY(begin)指明程序的入口点为begin标号;.=0x00300000指明目标代码的起始地址为0x30000000,这一段地址为 MX1的片内RAM;.text:{*(.text)}表示从0x30000000开始放置所有目标文件的代码段,随后的.data:{* (.data)}表示数据段从代码段的末尾开始,再后是.bss段。
(4)用连接器生成最终目标文件
有了连接脚本文件,如下命令可生成最终的目标文件:
arm-linux-ld –no stadlib –o bootstrap.elf -Tlink.lds init.o xmrecever.o flash.o
其中,ostadlib表示不连接系统的运行库,而是直接从begin入口;-o指明目标文件的名称;-T指明采用的连接脚本文件(也可以使用-Ttext address,address表示执行区地址);最后是需要连接的目标文件列表。
(5)生成二进制代码
连接生成的elf文件还不能直接下载执行,通过objcopy工具可生成最终的二进制文件:
arm-linux-objcopy –O binary bootstrap.elf bootstrap.bin
其中-O binary指定生成为二进制格式文件。Objcopy还可以生成S格式的文件,只需将参数换成-O srec。还可以使用-S选项,移除所有的符号信息及重定位信息。如果想将生成的目标代码反汇编,还可以用objdump工具:
 arm-linux-objdump -D bootstrap.elf
至此,所生成的目标文件就可以直接写入Flash中运行了。

2.Makefile实例
example: head.s  main.c
 arm-linux-gcc -c -o head.o head.s
 arm-linux-gcc -c -o main.o main.c
 arm-linux-ld -Tlink.lds head.o ain.o -o example.elf
 arm-linux-objcopy -O binary -S example_tmp.o example
 arm-linux-objdump -D -b binary -m arm  example >ttt.s


二. 调试工具
Linux下的GNU调试工具主要是gdb、gdbserver和kgdb。其中gdb和gdbserver可完成对目标板上Linux下应用程序的远程调试。gdbserver是一个很小的应用程序,运行于目标板上,可监控被调试进程的运行,并通过串口与上位机上的gdb通信。开发者可以通过上位机的gdb输入命令,控制目标板上进程的运行,查看内存和寄存器的内容。gdb5.1.1以后的版本加入了对ARM处理器的支持,在初始化时加入- target==arm参数可直接生成基于ARM平台的gdbserver。gdb工具可以从ftp: //ftp.gnu.org/pub/gnu/gdb/上下载。
对于Linux内核的调试,可以采用kgdb工具,同样需要通过串口与上位机上的gdb通信,对目标板的Linux内核进行调试。可以从http://oss.sgi.com/projects/kgdb/上了解具体的使用方法。


参考资料:
1. Richard Blum,Professional Assembly Language
2. GNU ARM 汇编快速入门,http://blog.chinaunix.net/u/31996/showart.php?id=326146
3. ARM GNU 汇编伪指令简介,http://www.cppblog.com/jb8164/archive/2008/01/22/41661.aspx
4. GNU汇编使用经验,http://blog.chinaunix.net/u1/37614/showart_390095.html
5. GNU的编译器和开发工具,http://blog.ccidnet.com/blog-htm-do-showone-uid-34335-itemid-81387-type-blog.html
6. 用GNU工具开发基于ARM的嵌入式系统,http://blog.163.com/liren0@126/blog/static/32897598200821211144696/
7. objcopy命令介绍,http://blog.csdn.net/junhua198310/archive/2007/06/27/1669545.aspx


=================================================================================

(补充)转:http://hi.baidu.com/760159/blog/item/122980def7c9e11948540361.html/cmtid/959835c57b6a74a48226aca4

ARM的ADS汇编器与GCC汇编器

   

一:ads下的一段汇编程序:
__main
EXPORT BootReset
BootReset
B resetvec_reqset
IMPORT BootEntry
IMPORT |Image$$RO$$Limit|

AREA BOOTROM, CODE, READONLY
LDR r0, =|Image$$RO$$Limit|
BEQ %1
ldr pc, [pc,#-&F20]
转换到gcc下的汇编程序为:

__main
.global BootReset
BootReset:
B resetvec_reqset

.extern BootEntry

.extern Image_RO_Limit

# AREA BOOTROM, CODE, READONLY
LDR r0, =Image_RO_Limit

BEQ FUNC1
ldr pc, [pc,#-0xF20]


二:将ARM SDT下的汇编码移植到GCC for ARM编译器时,经常要做如下修改:
1、注释行以“@”或""代替“;”
2、伪操作符替换:
INCLUDE 替换成 .INCLUDE
TCLK2 EQU PB25 替换成 .equ TCLK2, PB25
EXPORT 替换成 .global
IMPORT 替换成 .extern
DCD 替换成 .long
IF :DEF: 替换成 .IFDEF
ELSE 替换成 .ELSE
ENDIF 替换成 .ENDIF
:OR: 替换成 |
:SHL: 替换成 <<
END 替换成 .end


符号定义后加":"号
AREA Word, CODE, READONLY --> .text
AREA Block, DATA, READWRITE --> .data
CODE32 --> .arm
CODE16 --> .thumb
LTORG --> .ltorg



3、操作数及运算符号替换
ldr pc, [pc, #&18] 替换成 ldr pc, [pc, #+0x18]

“&”以“+0x”号替换


ARM-Linux汇编到ADS汇编转换需要注意的问题

  

最近那些课比较麻烦,好长时间没做ARM了,今天拿出一段nand准备在ADS下搭建调试环境,发现两家的汇编代码有但不兼容,需要修改一下.现在罗列一下我主要修改的地方,其他很多可能没遇到,大家还是自己查一查文档吧。

 

1、修改头

arm-linux汇编头:

.text

.global_start

_start:

 

修改为ads版本:

AREA        nand1,        CODE,READONLY

ENTRY

 

注意AREAENTRY前面都tab空格,否则编译出错。

 

2、修改段标志

去掉arm-linux汇编中的即可在ads中使用。

 

3ads需要加上ENTRYEND指令表示程序入口和结束标志

 

4ADS中的C语言混编

arm-linux汇编不同,ads下的汇编调用C语言的函数时需要指定IMPORT

切记注意在IMPORT前面加tab键空格。否则可能出现下面的错误(崩溃啊...):

 

arm汇编的条件执行码,这个比较常用,页贴上来供自己参考:

                                    转自:http://blog.chinaunix.net/uid-21457204-id-1826253.html

                                               http://www.cnblogs.com/yixiaoyang/archive/2010/11/18/1881273.html


2014-12-29 18:16:54 u013256622 阅读数 0
%BXX前XXb, %FXX后XXf

1、
前阵子看cpu从sleep模式唤醒时,对tst bne和tst beq有些模糊。先记录:
摘抄如下:

TST     R0, #0X8
BNE    SuspendUp ;BNE指令是“不相等(或不为0)跳转指令”:

LDR   R1,#0x00000000

先进行and运算,如果R0的第四位不为1,则结果为零,则设置zero=1(继续下面的LDR指令);

否则,zero=0(跳到SuspendUp处执行)。

还有:

有点模糊,在此记下。

tst r0,#02

bne sleep

ldr  r1,#0

解释:位比较,先进行and运算,如果r0第2位不为1,则与的结果为0,设置标志位zero=1,继续下面的ldr指令。反之,zero=0,跳转到sleep执行。

bne指令: 非零则跳转

个人总结:tst 和bne连用: 先是用tst进行位与运算,然后将位与的结果与0比较,如果不为0,则跳到bne紧跟着的标记(如bne sleep,则跳到sleep处)。

tst 和beq连用: 先是用tst进行位与运算,然后将位与的结果与0比较,如果为0,则跳到beq紧跟着的标记(如bne AAAA,则跳到AAAA处)。

2、

昨天在看arm汇编,其中有这样的一段语句

0

         ldr    r3, [r0], #4

         str    r3, [r1], #4

         cmp r2, r0

         bne %B0

bne:不等于则调转

但%B0 ,网上搜了一遍,还是未果。从最后的汇编语言来看,%B 代表,往前搜 lable为0的行,换句话说,就是指本条语句前,lable为0的地址。整条语句的意思就是,如果不相等则跳转到lable为0的行。

 

同样,有了bne %B0,也就有了bne %F1,这是向后搜索lable为1的行。参考代码:

; check if EIN0 button is pressed

 

       ldr       r0,=GPFCON

         ldr    r1,=0x0

         str    r1,[r0]

         ldr    r0,=GPFUP

         ldr    r1,=0xff

         str    r1,[r0]

 

         ldr    r1,=GPFDAT

         ldr    r0,[r1]

       bic      r0,r0,#(0x1e<<1) ; bit clear

         tst    r0,#0x1

         bne %F1

 

(省略一些语句)

;Clear SDRAM End

1

                ;Initialize stacks

         bl      InitStacks

%B, %F可以这样理解: B表示before,向...之前。     F表示after,向...之后
    b,f                                b是back向后,                     f是forward向前
哪个对呢??  原文地址也找不到了...
2016-12-01 19:46:02 silent123go 阅读数 2123

转载地址:http://blog.csdn.net/luckyapple1028/article/details/44726131

本文整理了ARM Linxu启动流程的第一阶段——内核自解压,内核版本为3.12.35。我以手上的树莓派b(ARM11)为平台示例来分析uboot跳转到Linux内核运行后做了哪些初始化动作,以及如何转入真正的内核开始运行。

内核版本:Linux-3.12.35

分析文件:linux/arch/arm/boot/compressed/head.S

单板:树莓派b

在内核启动前,bootloader(我使用的是uboot)做如下准备工作:

  1. CPU寄存器:R0 = 0、R1 = 机器码(linux/arch/tools/mach-types)、R2 = tags在RAM中的物理地址
  2. CPU和MMU:SVC模式,禁止中断,MMU关闭,数据Cache关闭。

首先给出内核自解压部分的总流程如下:
这里写图片描述
内核自解压程序的入口:参见arch/arm/boot/compressed/vmlinux.lds(由arch/arm/boot/compressed/vmlinux.lds.in生成):

    SECTIONS  
    {  
    ......  
        *(.data)  
      }  

      . = 0;  
      _text = .;  

      .text : {  
        _start = .;  
        *(.start)  
        *(.text)  
        *(.text.*)  
        *(.fixup)  
        *(.gnu.warning)  
        *(.glue_7t)  
        *(.glue_7)  
      }  
    ......  
    }  

程序的入口点在linux/arch/arm/boot/compressed/head.S中,下面来进行详细分析:

    start:  
            .type   start,#function  
            .rept   7  
            mov r0, r0  
            .endr  
       ARM(     mov r0, r0      )  
       ARM(     b   1f      )  
     THUMB(     adr r12, BSYM(1f)   )  
     THUMB(     bx  r12     )  

使用.type标号来指明start的符号类型是函数类型,然后重复执行.rept到.endr之间的指令7次,这里一共执行了7次mov r0, r0指令,共占用了4*7 = 28个字节,这是用来存放ARM的异常向量表的。向前跳转到标号为1处执行:

    1:  
            mrs r9, cpsr  
    #ifdef CONFIG_ARM_VIRT_EXT  
            bl  __hyp_stub_install  @ get into SVC mode, reversibly  
    #endif  
            mov r7, r1          @ save architecture ID  
            mov r8, r2          @ save atags pointer  

这里将CPU的工作模式保存到r9寄存器中,将uboot通过r1传入的机器码保存到r7寄存器中,将启动参数tags的地址保存到r8寄存器中。

 /* 
     * Booting from Angel - need to enter SVC mode and disable 
     * FIQs/IRQs (numeric definitions from angel arm.h source). 
     * We only do this if we were in user mode on entry. 
     */  
    mrs r2, cpsr        @ get current mode  
    tst r2, #3          @ not user?  
    bne not_angel  
    mov r0, #0x17       @ angel_SWIreason_EnterSVC  
ARM(        swi 0x123456    )   @ angel_SWI_ARM  
THUMB(      svc 0xab        )   @ angel_SWI_THUMB 

这里将CPU的工作模式保存到r2寄存器中,然后判断是否是SVC模式,如果是USER模式就会通过swi指令产生软中断异常的方式来自动进入SVC模式。由于我这里在uboot中已经将CPU的模式设置为SVC模式了,所以就直接跳到not_angel符号处执行。

    not_angel:  
            safe_svcmode_maskall r0  
            msr spsr_cxsf, r9       @ Save the CPU boot mode in  
                            @ SPSR  

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. 
     */  
    .macro safe_svcmode_maskall reg:req  
    #if __LINUX_ARM_ARCH__ >= 6  
        mrs \reg , cpsr  
        eor \reg, \reg, #HYP_MODE  
        tst \reg, #MODE_MASK  
        bic \reg , \reg , #MODE_MASK  
        orr \reg , \reg , #PSR_I_BIT | PSR_F_BIT | SVC_MODE  
    THUMB(  orr \reg , \reg , #PSR_T_BIT    )  
        bne 1f  
        orr \reg, \reg, #PSR_A_BIT  
        adr lr, BSYM(2f)  
        msr spsr_cxsf, \reg  
        __MSR_ELR_HYP(14)  
        __ERET  
    1:  msr cpsr_c, \reg  
    2:  
    #else  
    /* 
     * workaround for possibly broken pre-v6 hardware 
     * (akita, Sharp Zaurus C-1000, PXA270-based) 
     */  
        setmode PSR_F_BIT | PSR_I_BIT | SVC_MODE, \reg  
    #endif  
    .endm  

这里的注释已经说明了,这里是强制将CPU的工作模式切换到SVC模式,并且关闭IRQ和FIQ中断。然后将r9中保存的原始CPU配置保存到SPSR中。

    #ifdef CONFIG_AUTO_ZRELADDR  
            @ determine final kernel image address  
            mov r4, pc  
            and r4, r4, #0xf8000000  
            add r4, r4, #TEXT_OFFSET  
    #else  
            ldr r4, =zreladdr  
    #endif  

内核配置项AUTO_ZRELDDR表示自动计算内核解压地址(Auto calculation of the decompressed kernelimage address),这里没有选择这个配置项,所以保存到r4中的内核解压地址就是zreladdr,这个参数在linux/arch/arm/boot/compressed/Makefile中:

    #ifdef CONFIG_AUTO_ZRELADDR  
            @ determine final kernel image address  
            mov r4, pc  
            and r4, r4, #0xf8000000  
            add r4, r4, #TEXT_OFFSET  
    #else  
            ldr r4, =zreladdr  
    #endif  

内核配置项AUTO_ZRELDDR表示自动计算内核解压地址(Auto calculation of the decompressed kernelimage address),这里没有选择这个配置项,所以保存到r4中的内核解压地址就是zreladdr,这个参数在linux/arch/arm/boot/compressed/Makefile中:

ifneq ($(CONFIG_AUTO_ZRELADDR),y)  
LDFLAGS_vmlinux += --defsym zreladdr=$(ZRELADDR)  
endif 

内核配置项AUTO_ZRELDDR表示自动计算内核解压地址(Auto calculation of the decompressed kernelimage address),这里没有选择这个配置项,所以保存到r4中的内核解压地址就是zreladdr,这个参数在linux/arch/arm/boot/compressed/Makefile中:

    ifneq ($(CONFIG_AUTO_ZRELADDR),y)  
    LDFLAGS_vmlinux += --defsym zreladdr=$(ZRELADDR)  
    endif  

而ZRELADDR定义在arch/arm/boot/Makefile中:

    ifneq ($(MACHINE),)  
    include $(srctree)/$(MACHINE)/Makefile.boot  
    endif  

    # Note: the following conditions must always be true:  
    #   ZRELADDR == virt_to_phys(PAGE_OFFSET + TEXT_OFFSET)  
    #   PARAMS_PHYS must be within 4MB of ZRELADDR  
    #   INITRD_PHYS must be in RAM  
    ZRELADDR    := $(zreladdr-y)  
    PARAMS_PHYS := $(params_phys-y)  
    INITRD_PHYS := $(initrd_phys-y)  

既然看到了内核解压地址zreladdr,也顺便来看一下params_phys和initrd_phys的值,他们最终由arch/arm/mach-$(SOC)/Makefile.boot决定,我这里使用的soc是bcm2807(bcm2835),他的Makefile.boot内容如下:
zreladdr-y := 0x00008000
params_phys-y := 0x00000100
initrd_phys-y :=0x00800000
这里的params_phys-y和initrd_phys-y是内核参数的物理地址和initrd文件系统的物理地址。其实除了zreladdr外这些地址uboot都会传入的。

    /* 
     * Set up a page table only if it won't overwrite ourself. 
     * That means r4 < pc && r4 - 16k page directory > &_end. 
     * Given that r4 > &_end is most unfrequent, we add a rough 
     * additional 1MB of room for a possible appended DTB. 
     */  
    mov r0, pc  
    cmp r0, r4  
    ldrcc   r0, LC0+32  
    addcc   r0, r0, pc  
    cmpcc   r4, r0  
    orrcc   r4, r4, #1      @ remember we skipped cache_on  
    blcs    cache_on  

这里将比较当前PC地址和内核解压地址,只有在不会自覆盖的情况下才会创建一个页表,如果当前运行地址PC < 解压地址r4,则读取LC0+32地址处的内容加载到r0中,否则跳转到cache_on处执行缓存初始化和MMU初始化。LC0是地址表,定义在525行:

            .align  2  
            .type   LC0, #object  
    LC0:        .word   LC0         @ r1  
            .word   __bss_start     @ r2  
            .word   _end            @ r3  
            .word   _edata          @ r6  
            .word   input_data_end - 4  @ r10 (inflated size location)  
            .word   _got_start      @ r11  
            .word   _got_end        @ ip  
            .word   .L_user_stack_end   @ sp  
            .word   _end - restart + 16384 + 1024*1024  
            .size   LC0, . - LC0  

LC0+32地址处的内容为:_end -restart + 16384 + 1024*1024,所指的就是程序长度+16k的页表长+1M的DTB空间。继续比较解压地址r4(0x00008000)和当前运行程序的(结束地址+16384 + 1024*1024),如果小于则不进行缓存初始化并置位r4最低位进行标识。

这里稍稍有点绕,分情况总结一下:

(1) PC >= r4:直接进行缓存初始化

(2) PC < r4 && _end + 16384+ 1024*1024 > r4:不进行缓存初始化

(3) PC < r4 && _end + 16384+ 1024*1024 <= r4:执行缓存初始化

这里先暂时不分析cache_on(已补充在文中最后分析),继续沿主线往下分析:

    restart:    adr r0, LC0  
            ldmia   r0, {r1, r2, r3, r6, r10, r11, r12}  
            ldr sp, [r0, #28]  

通过前面LC0地址表的内容可见,这里r0中的内容就是编译时决定的LC0的实际运行地址(特别注意不是链接地址),然后调用ldmia命令依次将LC0地址表处定义的各个地址加载到r1、r2、r3、r6、r10、r11、r12和SP寄存器中去。执行之后各个寄存器中保存内容的意义如下:

(1) r0:LC0标签处的运行地址

(2) r1:LC0标签处的链接地址

(3) r2:__bss_start处的链接地址

(4) r3:_ednd处的链接地址(即程序结束位置)

(5) r6:_edata处的链接地址(即数据段结束位置)

(6) r10:压缩后内核数据大小位置

(7) r11:GOT表的启示链接地址

(8) r12:GOT表的结束链接地址

(9) sp:栈空间结束地址
195~196行这段代码通过反汇编来看着部分代码会更加清晰(反汇编arch/arm/boot /compressed/vmlinux):

    000000c0 <restart>:  
          c0:   e28f0e13    add r0, pc, #304    ; 0x130  
          c4:   e8901c4e    ldm r0, {r1, r2, r3, r6, sl, fp, ip}  
          c8:   e590d01c    ldr sp, [r0, #28]  

由于我的环境中实际的运行在物理地址为0x00008000处,所以r0 = pc + 0x130 = 0x00008000 + 0xc0 + 0x8 + 0x130 = 0x00008000 +0x1F8,而0x000081F8物理地址处的内容就是LC0:

    000001f8 <LC0>:  
         1f8:   000001f8    strdeq  r0, [r0], -r8  
         1fc:   006f2a80    rsbeq   r2, pc, r0, lsl #21  
         200:   006f2a98    mlseq   pc, r8, sl, r2  ; <UNPREDICTABLE>  
         204:   006f2a80    rsbeq   r2, pc, r0, lsl #21  
         208:   006f2a45    rsbeq   r2, pc, r5, asr #20  
         20c:   006f2a58    rsbeq   r2, pc, r8, asr sl  ; <UNPREDICTABLE>  
         210:   006f2a7c    rsbeq   r2, pc, ip, ror sl  ; <UNPREDICTABLE>  
         214:   006f3a98    mlseq   pc, r8, sl, r3  ; <UNPREDICTABLE>  
         218:   007f69d8    ldrsbteq    r6, [pc], #-152  
         21c:   e320f000    nop {0}  

在获取了LC0的链接地址和运行地址后,就可以通过计算这两者之间的差值来判断当前运行的地址是否就是编译时的链接地址。

    /* 
     * We might be running at a different address.  We need 
     * to fix up various pointers. 
     */  
    sub r0, r0, r1      @ calculate the delta offset  
    add r6, r6, r0      @ _edata  
    add r10, r10, r0        @ inflated kernel size location  

将运行地址和链接地址的偏移保存到r0寄存器中,然后更新r6和r10中的地址,将其转换为实际的运行地址。

    /* 
     * The kernel build system appends the size of the 
     * decompressed kernel at the end of the compressed data 
     * in little-endian form. 
     */  
    ldrb    r9, [r10, #0]  
    ldrb    lr, [r10, #1]  
    orr r9, r9, lr, lsl #8  
    ldrb    lr, [r10, #2]  
    ldrb    r10, [r10, #3]  
    orr r9, r9, lr, lsl #16  
    orr r9, r9, r10, lsl #24  

注释中说明了,内核编译系统在压缩内核时会在末尾处以小端模式附上未压缩的内核大小,这部分代码的作用就是将该值计算出来并保存到r9寄存器中去。

    #ifndef CONFIG_ZBOOT_ROM  
            /* malloc space is above the relocated stack (64k max) */  
            add sp, sp, r0  
            add r10, sp, #0x10000  
    #else  
            /* 
             * With ZBOOT_ROM the bss/stack is non relocatable, 
             * but someone could still run this code from RAM, 
             * in which case our reference is _edata. 
             */  
            mov r10, r6  
    #endif  

这里将镜像的结束地址保存到r10中去,我这里并没有定义ZBOOT_ROM(如果定义了ZBOOT_ROM则bss和stack是非可重定位的),这里将r10设置为sp结束地址上64kb处(这64kB空间是用来作为堆空间的)。
接下来内核如果配置为支持设备树(DTB)会做一些特别的工作,我这里没有配置(#ifdef CONFIG_ARM_APPENDED_DTB),所以先跳过。

    /* 
     * Check to see if we will overwrite ourselves. 
     *   r4  = final kernel address (possibly with LSB set) 
     *   r9  = size of decompressed image 
     *   r10 = end of this image, including  bss/stack/malloc space if non XIP 
     * We basically want: 
     *   r4 - 16k page directory >= r10 -> OK 
     *   r4 + image length <= address of wont_overwrite -> OK 
     * Note: the possible LSB in r4 is harmless here. 
     */  
            add r10, r10, #16384  
            cmp r4, r10  
            bhs wont_overwrite  
            add r10, r4, r9  
            adr r9, wont_overwrite  
            cmp r10, r9  
            bls wont_overwrite  

这里r4、r9和r10中的内容见注释。这部分代码用来分析当前代码是否会和最后的解压部分重叠,如果有重叠则需要执行代码搬移。首先比较内核解压地址r4-16Kb(这里是0x00004000,包括16KB的内核页表存放位置)和r10,如果r4 – 16kB >= r10,则无需搬移,否则继续计算解压后的内核末尾地址是否在当前运行地址之前,如果是则同样无需搬移,不然的话就需要进行搬移了。

总结一下可能的3种情况:

(1) 内核起始地址– 16kB >= 当前镜像结束地址:无需搬移

(2) 内核结束地址 <= wont_overwrite运行地址:无需搬移

(3) 内核起始地址– 16kB < 当前镜像结束地址 && 内核结束地址 > wont_overwrite运行地址:需要搬移
仔细分析一下,这里内核真正运行的地址是0x00004000,而现在代码的运行地址显然已经在该地址之后了反汇编发现wont_overwrite的运行地址是0x00008000+0x00000168),而且内核解压后的空间必然会覆盖掉这里(内核解压后的大小大于0x00000168),所以这里会执行代码搬移。

    /* 
     * Relocate ourselves past the end of the decompressed kernel. 
     *   r6  = _edata 
     *   r10 = end of the decompressed kernel 
     * Because we always copy ahead, we need to do it from the end and go 
     * backward in case the source and destination overlap. 
     */  
            /* 
             * Bump to the next 256-byte boundary with the size of 
             * the relocation code added. This avoids overwriting 
             * ourself when the offset is small. 
             */  
            add r10, r10, #((reloc_code_end - restart + 256) & ~255)  
            bic r10, r10, #255  

            /* Get start of code we want to copy and align it down. */  
            adr r5, restart  
            bic r5, r5, #31  

从这里开始会将镜像搬移到解压的内核地址之后,首先将解压后的内核结束地址进行扩展,扩展大小为代码段的大小(reloc_code_end定义在head.s的最后)保存到r10中,即搬运目的起始地址,然后r5保存了restart的起始地址,并进行对齐,即搬运的原起始地址。反汇编查看这里扩展的大小为0x800:

    11c:    e28aab02    add sl, sl, #2048   ; 0x800  
    120:    e3caa0ff    bic sl, sl, #255    ; 0xff  
    124:    e24f506c    sub r5, pc, #108    ; 0x6c  
    128:    e3c5501f    bic r5, r5, #31  
            sub r9, r6, r5      @ size to copy  
            add r9, r9, #31     @ rounded up to a multiple  
            bic r9, r9, #31     @ ... of 32 bytes  
            add r6, r9, r5  
            add r9, r9, r10  

    1:      ldmdb   r6!, {r0 - r3, r10 - r12, lr}  
            cmp r6, r5  
            stmdb   r9!, {r0 - r3, r10 - r12, lr}  
            bhi 1b  

            /* Preserve offset to relocated code. */  
            sub r6, r9, r6  

    #ifndef CONFIG_ZBOOT_ROM  
            /* cache_clean_flush may use the stack, so relocate it */  
            add sp, sp, r6  
    #endif  

这里首先计算出需要搬运的大小保存到r9中,搬运的原结束地址到r6中,搬运的目的结束地址到r9中。注意这里只搬运代码段和数据段,并不包含bss、栈和堆空间。
接下来开始执行代码搬移,这里是从后往前搬移,一直到r6 == r5结束,然后r6中保存了搬移前后的偏移,并重定向栈指针(cache_clean_flush可能会使用到栈)。

    bl  cache_clean_flush  

    adr r0, BSYM(restart)  
    add r0, r0, r6  
    mov pc, r0  

首先调用cache_clean_flush清楚缓存,然后将PC的值设置为搬运后restart的新地址,然后重新从restart开始执行。这次由于进行了代码搬移,所以会在检查自覆盖时进入wont_overwrite处执行。

    wont_overwrite:  
    /* 
     * If delta is zero, we are running at the address we were linked at. 
     *   r0  = delta 
     *   r2  = BSS start 
     *   r3  = BSS end 
     *   r4  = kernel execution address (possibly with LSB set) 
     *   r5  = appended dtb size (0 if not present) 
     *   r7  = architecture ID 
     *   r8  = atags pointer 
     *   r11 = GOT start 
     *   r12 = GOT end 
     *   sp  = stack pointer 
     */  
            orrs    r1, r0, r5  
            beq not_relocated  

这里的注释列出了现有所有寄存器值得含义,如果r0为0则说明当前运行的地址就是链接地址,无需进行重定位,跳转到not_relocated执行,但是这里运行的地址已经被移动到内核解压地址之后,显然不会是链接地址0x00000168(反汇编代码中得到),所以这里需要重新修改GOT表中的变量地址来实现重定位。



            add r11, r11, r0  
            add r12, r12, r0  

    #ifndef CONFIG_ZBOOT_ROM  
            /* 
             * If we're running fully PIC === CONFIG_ZBOOT_ROM = n, 
             * we need to fix up pointers into the BSS region. 
             * Note that the stack pointer has already been fixed up. 
             */  
            add r2, r2, r0  
            add r3, r3, r0  

这里更新GOT表的运行起始地址到r11和结束地址到r12中去,然后同样更新BSS段的运行地址(需要修正BSS段的指针)。接下来开始执行重定位:

            /* 
             * Relocate all entries in the GOT table. 
             * Bump bss entries to _edata + dtb size 
             */  
    1:      ldr r1, [r11, #0]       @ relocate entries in the GOT  
            add r1, r1, r0      @ This fixes up C references  
            cmp r1, r2          @ if entry >= bss_start &&  
            cmphs   r3, r1          @       bss_end > entry  
            addhi   r1, r1, r5      @    entry += dtb size  
            str r1, [r11], #4       @ next entry  
            cmp r11, r12  
            blo 1b  

            /* bump our bss pointers too */  
            add r2, r2, r5  
            add r3, r3, r5  

通过r1获取GOT表中的一项,然后对这一项的地址进行修正,如果修正后的地址 < BSS段的起始地址,或者在BSS段之中则再加上DTB的大小(如果不支持DTB则r5的值为0),然后再将值写回GOT表中去。如此循环执行直到遍历完GOT表。来看一下反汇编出来的GOT表,加深理解:

    006f2a58 <.got>:  
      6f2a58:   006f2a49    rsbeq   r2, pc, r9, asr #20  
      6f2a5c:   006f2a94    mlseq   pc, r4, sl, r2  ; <UNPREDICTABLE>  
      6f2a60:   0000466c    andeq   r4, r0, ip, ror #12  
      6f2a64:   006f2a90    mlseq   pc, r0, sl, r2  ; <UNPREDICTABLE>  
      6f2a68:   006f2a80    rsbeq   r2, pc, r0, lsl #21  
      6f2a6c:   0000090c    andeq   r0, r0, ip, lsl #18  
      6f2a70:   006f2a88    rsbeq   r2, pc, r8, lsl #21  
      6f2a74:   006f2a8c    rsbeq   r2, pc, ip, lsl #21  
      6f2a78:   006f2a84    rsbeq   r2, pc, r4, lsl #21  

以这里的6f2a60: 0000466c为例,0000466c为input_data符号的链接地址,定义在arch/arm/boot/compressed/piggy.gzip.S中,可以算作是全局变量,这里在执行重定位时会将6f2a60地址处的值加上偏移,可得到input_data符号的运行地址,以此完成重定位工作,以后在内核的C代码中如果用到input_data符号就从这里的地址加载。

    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  

在重定位完成后,继续执行not_relocated部分代码,这里循环清零BSS段。

    /* 
     * Did we skip the cache setup earlier? 
     * That is indicated by the LSB in r4. 
     * Do it now if so. 
     */  
    tst r4, #1  
    bic r4, r4, #1  
    blne    cache_on  

这里检测r4中的最低位,如果已经置位则说明在前面执行restart前并没有执行cache_on来打开缓存(见前文),这里补执行。

    /* 
     * The C runtime environment should now be setup sufficiently. 
     * Set up some pointers, and start decompressing. 
     *   r4  = kernel execution address 
     *   r7  = architecture ID 
     *   r8  = atags pointer 
     */  
            mov r0, r4  
            mov r1, sp          @ malloc space above stack  
            add r2, sp, #0x10000    @ 64k max  
            mov r3, r7  
            bl  decompress_kernel  

到此为止,C语言的执行环境已经准备就绪,设置一些指针就可以开始解压内核了(这里的内核解压部分是使用C代码写的)。

这里r0~r3的4个寄存器是decompress_kernel()函数传参用的,r0传入内核解压后的目的地址,r1传入堆空间的起始地址,r2传入堆空间的结束地址,r3传入机器码,然后就开始调用decompress_clean_flush()函数执行内核解压操作:

    void  
    decompress_kernel(unsigned long output_start, unsigned long free_mem_ptr_p,  
            unsigned long free_mem_ptr_end_p,  
            int arch_id)  
    {  
        int ret;  

        output_data     = (unsigned char *)output_start;  
        free_mem_ptr        = free_mem_ptr_p;  
        free_mem_end_ptr    = free_mem_ptr_end_p;  
        __machine_arch_type = arch_id;  

        arch_decomp_setup();  

        putstr("Uncompressing Linux...");  
        ret = do_decompress(input_data, input_data_end - input_data,  
                    output_data, error);  
        if (ret)  
            error("decompressor returned an error");  
        else  
            putstr(" done, booting the kernel.\n");  
    }  

真正执行内核解压的函数是do_decompress()->decompress()(该函数会根据不同的压缩方式调用不同的解压函数),在解压前会在终端上打印“Uncompressing Linux…”,结束后会打印出“done, booting the kernel.\n”。(这里有一点疑问:这里会在终端输出,那串口的驱动是在什么时候初始化的?)

    bl  cache_clean_flush  
    bl  cache_off  
    mov r1, r7          @ restore architecture number  
    mov r2, r8          @ restore atags pointer  

解压完成后就刷新缓存,然后将缓存(包括MMU关闭),这里之所以要打开缓存和MMU是为了加速内核解压。

然后将机器码和内启动参数atags恢复到r1和r2寄存器中,为跳转到解压后的内核代码做准备。

b   __enter_kernel  
    __enter_kernel:  
            mov r0, #0          @ must be 0  
     ARM(       mov pc, r4  )       @ call kernel  
     THUMB(     bx  r4  )       @ entry point is always ARM  

补充:缓存和MMU初始化cache_on的执行流程

    /* 
     * Turn on the cache.  We need to setup some page tables so that we 
     * can have both the I and D caches on. 
     * 
     * We place the page tables 16k down from the kernel execution address, 
     * and we hope that nothing else is using it.  If we're using it, we 
     * will go pop! 
     * 
     * On entry, 
     *  r4 = kernel execution address 
     *  r7 = architecture number 
     *  r8 = atags pointer 
     * On exit, 
     *  r0, r1, r2, r3, r9, r10, r12 corrupted 
     * This routine must preserve: 
     *  r4, r7, r8 
     */  
            .align  5  
    cache_on:   mov r3, #8          @ cache_on function  
            b   call_cache_fn  

注释中说明了,为了开启I Cache和D Cache,需要建立页表(开启MMU),而页表使用的就是内核运行地址以下的16KB空间(对于我的环境来说地址就等于0x00004000~0x00008000)。同时在运行的过程中r0~r3以及r9、r10和r12寄存器会被使用。

这里首先在r3中保存打开缓存函数表项在cache操作表中的地址偏移(这里为8,cache操作表见后文),然后跳转到call_cache_fn中。

    /* 
     * Here follow the relocatable cache support functions for the 
     * various processors.  This is a generic hook for locating an 
     * entry and jumping to an instruction at the specified offset 
     * from the start of the block.  Please note this is all position 
     * independent code. 
     * 
     *  r1  = corrupted 
     *  r2  = corrupted 
     *  r3  = block offset 
     *  r9  = corrupted 
     *  r12 = corrupted 
     */  

    call_cache_fn:  adr r12, proc_types  
    #ifdef CONFIG_CPU_CP15  
            mrc p15, 0, r9, c0, c0  @ get processor ID  
    #else  
            ldr r9, =CONFIG_PROCESSOR_ID  
    #endif  
    1:      ldr r1, [r12, #0]       @ get value  
            ldr r2, [r12, #4]       @ get mask  
            eor r1, r1, r9      @ (real ^ match)  
            tst r1, r2          @       & mask  
     ARM(       addeq   pc, r12, r3     ) @ call cache function  
     THUMB(     addeq   r12, r3         )  
     THUMB(     moveq   pc, r12         ) @ call cache function  
            add r12, r12, #PROC_ENTRY_SIZE  
            b   1b  

首先保存cache操作表的运行地址到r12寄存器中,proc_types定义在head.s中的825行:

    /* 
     * Table for cache operations.  This is basically: 
     *   - CPU ID match 
     *   - CPU ID mask 
     *   - 'cache on' method instruction 
     *   - 'cache off' method instruction 
     *   - 'cache flush' method instruction 
     * 
     * We match an entry using: ((real_id ^ match) & mask) == 0 
     * 
     * Writethrough caches generally only need 'on' and 'off' 
     * methods.  Writeback caches _must_ have the flush method 
     * defined. 
     */  
            .align  2  
            .type   proc_types,#object  

表中的每一类处理器都包含以下5项(如果不存在缓存操作函数则使用“mov pc, lr”占位):

(1) CPU ID

(2) CPU ID 位掩码(用于匹配CPU类型用)

(3) 打开缓存“cache on”函数入口

(4) 关闭缓存“cache off”函数入口

(5) 刷新缓存“cache flush”函数入口

其中我环境中使用到的ARMv6的cache操作表如下:

    .word   0x0007b000      @ ARMv6  
    .word   0x000ff000  
    W(b)    __armv6_mmu_cache_on  
    W(b)    __armv4_mmu_cache_off  
    W(b)    __armv6_mmu_cache_flush  

我的环境中由于配置了CPU_CP15条件编译项,所以这里将从CP15中获取CPU型号而不是从内核配置项中获取。

然后逐条对cache操作表中的CPU类型进行匹配,如果匹配上了就跳转到相应的函数入口执行。

这里通过反汇编代码来理解一下:

    0000044c <call_cache_fn>:  
         44c:   e28fc01c    add ip, pc, #28  
         450:   ee109f10    mrc 15, 0, r9, cr0, cr0, {0}  
         454:   e59c1000    ldr r1, [ip]  
         458:   e59c2004    ldr r2, [ip, #4]  
         45c:   e0211009    eor r1, r1, r9  
         460:   e1110002    tst r1, r2  
         464:   008cf003    addeq   pc, ip, r3  
         468:   e28cc014    add ip, ip, #20  
         46c:   eafffff8    b   454 <call_cache_fn+0x8>  

这里首先在r12中获取了proc_types的运行地址:pc + 0x8 + 0x1c:

00000470 <proc_types>:  

然后逐条匹配,最后会匹配到ARMv6部分,cache table中ARMv6部分的代码如下:

    5b0:    0007b000    andeq   fp, r7, r0  
    5b4:    000ff000    andeq   pc, pc, r0  
    5b8:    eaffff5c    b   330 <__armv6_mmu_cache_on>  
    5bc:    ea00001f    b   640 <__armv4_mmu_cache_off>  
    5c0:    ea00004f    b   704 <__armv6_mmu_cache_flush>  

于是PC寄存器就执行上面5b8地址处的内容,这条机器码被翻译为相对跳转到__armv6_mmu_cache_on处执行:

00000330 <__armv6_mmu_cache_on>:  

这样__armv6_mmu_cache_on就被调用执行了。

__armv6_mmu_cache_on中会通过写寄存器来开启MMU、I Cache和D Cache,这里具体就不仔细分析了,其中为MMU建立页表有必要分析一下:

前文已经分析过在内核最终运行地址r4下面有16KB的空间(我环境中是0x00004000~0x00008000),这就是用来存放页表的,但是现在要建立的页表在内核真正启动后会被销毁,只是用于零时存放。同时这里将要建立的页表映射关系是1:1映射(即虚拟地址 == 物理地址)。

    __setup_mmu:    sub r3, r4, #16384      @ Page directory size  
            bic r3, r3, #0xff       @ Align the pointer  
            bic r3, r3, #0x3f00  

首先在r3中保存的是页目录表的基地址,需要将低14位清零进行16KB对齐。

    /* 
     * Initialise the page tables, turning on the cacheable and bufferable 
     * bits for the RAM area only. 
     */  
            mov r0, r3  
            mov r9, r0, lsr #18  
            mov r9, r9, lsl #18     @ start of RAM  
            add r10, r9, #0x10000000    @ a reasonable RAM size  

这里建立页表,只对物理RAM空间建立cache和buffer。然后通过将r0(r3)中的地址值右移18位再左移18位(即清零r3中地址的低18位),得到物理RAM空间的“初始地址”(其实是估计值)并保存到r9(0x00000000)中去,然后将该地址加上256MB的大小作为物理RAM的“结束地址”(也是估计值)并保存到r10(0x10000000)中去。这里的另一个隐含意思也就是最多映射256MB大小的空间。

    mov r1, #0x12       @ XN|U + section mapping  
    orr r1, r1, #3 << 10  @ AP=11  
    add r2, r3, #16384  
            cmp r1, r9          @ if virt > start of RAM  
    cmphs   r10, r1         @   && end of RAM > virt  
    bic r1, r1, #0x1c       @ clear XN|U + C + B  
    orrlo   r1, r1, #0x10       @ Set XN|U for non-RAM  
    orrhs   r1, r1, r6      @ set RAM section settings  
    str r1, [r0], #4        @ 1:1 mapping  
    add r1, r1, #1048576  
    teq r0, r2  
    bne 1b  

ARM11的 MMU支持两级分页机制,用户通过配置TTBCR可选映射方式,这里只采用一级映射方式,每页的大小为1MB,4GB线性空间占4096个表项。ARM手册中列出了映射的关系如下:
这里写图片描述
其中Translation table base的值就是页表存放的起始地址值0x00004000。这里虚拟地址转换到物理地址的方式如下:首先将虚拟地址的高12位(页表中的索引)左移2位(正好为14位长度,补齐Translation table base中低14位的空白)得到在页表中的偏移地址,该偏移值加上页表基地址就得到了对应表项的物理地址,然后取出表项中的高12位值作为物理地址的基地址,最后以虚拟地址的低20位作为偏移地址值就得到了最终映射后的物理地址。

这里First-level descriptor中的内容就是这里程序中要填充的值,其中最低2位为0b10表示表项中保存的是Section base address,而不是2级页表的基地址。

来继续分析代码,这里首先r1 =0b 1100 0001 0010 = 0xC12(用于设置MMU区域表项的低12位状态位),r2为页表空间的结束地址,然后开始循环建立页表项。

接着r1比较r9和r10以设置MMU区域表项状态位:(其中r6中的值在前面__armv6_mmu_cache_on中赋值为0x1E)
(1)r1 > r9 && r1 <r10(r1的值在物理RAM地址范围内):
设置RAM表项的C+B 位来开启cache和buffer,同时清除XN表示可执行code
(2) r1 < r9 || r1 > r10(r1的值在物理RAM地址范围外):

设置RAM表项的XN位并清除C+B位来关闭cache和buffer,不可执行code
在设置完状态为后就要写入页表的相应地址中去了,然后将页表的地址+4(指向下一个表项),物理地址空间+1M设置下一项(下一个需要映射物理地址的基地址),直到填完所有的4096表项。设置完后页表项与映射关系如下:
这里写图片描述

    /* 
     * If ever we are running from Flash, then we surely want the cache 
     * to be enabled also for our execution instance...  We map 2MB of it 
     * so there is no map overlap problem for up to 1 MB compressed kernel. 
     * If the execution is in RAM then we would only be duplicating the above. 
     */  
            orr r1, r6, #0x04       @ ensure B is set for this  
            orr r1, r1, #3 << 10  
            mov r2, pc  
            mov r2, r2, lsr #20  
            orr r1, r1, r2, lsl #20  
            add r0, r3, r2, lsl #2  
            str r1, [r0], #4  
            add r1, r1, #1048576  
            str r1, [r0]  
            mov pc, lr  
    ENDPROC(__setup_mmu)  

如果代码不是运行在RAM中而是运行在FLASH中的,则映射2MB代码,如果运行在RAM中,则这部分代码重复前面的工作。

同前面一样,设置r1区域表项的低12位,这里首先初始化r1 = 0xC1E,然后计算出当前PC运行地址的基地址到r2和r1中(我的环境中r2 = r1 = 0)。然后计算r2 << 2得到映射基地址在表中的偏移(因为是1:1映射,所以可以这样计算出来),再加上页表的起始地址就得到了该页表项的地址了,然后将这2MB空间的地址写入这两个表项中即完成了整个MMU页表的建立,最后mov pc lr返回。

参考文献:《ARM Linux内核源码剖析》

2011-06-22 16:53:00 penglijiang 阅读数 37606

1、
前阵子看cpu从sleep模式唤醒时,对tst bne和tst beq有些模糊。先记录:
摘抄如下:

TST     R0, #0X8
BNE    SuspendUp ;BNE指令 是“不相等(或不为0)跳转指令 ”:

LDR   R1,#0x00000000

先进行and运算,如果R0的第四位不为1,则结果为零,则设置zero=1(继续下面的LDR指令);

否则,zero=0(跳到SuspendUp处执行)。

还有:

有点模糊,在此记下。

tst r0,#02

bne sleep

ldr  r1,#0

解释:位比较,先进行and运算,如果r0第2位不为1,则与的结果为0,设置标志位zero=1,继续下面的ldr指令。反之,zero=0,跳转到sleep执行。

bne指令: 非零则跳转

个人总结:tst 和bne连用: 先是用tst进行位与运算,然后将位与的结果与0比较,如果不为0,则跳到bne紧跟着的标记(如bne sleep,则跳到sleep处)。

tst 和beq连用: 先是用tst进行位与运算,然后将位与的结果与0比较,如果为0,则跳到beq紧跟着的标记(如bne AAAA,则跳到AAAA处)。

2、

昨天在看arm汇编,其中有这样的一段语句

0

          ldr     r3, [r0], #4

          str     r3, [r1], #4

          cmp  r2, r0

          bne  %B0

 

bne:不等于则调转

但%B0 ,网上搜了一遍,还是未果。从最后的汇编语言来看,%B 代表,往前搜  lable为0的行,换句话说,就是指本条语句前,lable为0的地址。整条语句的意思就是,如果不相等则跳转到lable为0的行。

 

同样,有了bne %B0,也就有了bne %F1,这是向后搜索 lable 为1的行。参考代码:

; check if EIN0 button is pressed

 

        ldr        r0,=GPFCON

          ldr     r1,=0x0

          str     r1,[r0]

          ldr     r0,=GPFUP

          ldr     r1,=0xff

          str     r1,[r0]

 

          ldr     r1,=GPFDAT

          ldr     r0,[r1]

        bic       r0,r0,#(0x1e<<1)  ; bit clear

          tst     r0,#0x1

          bne %F1

 

(省略一些语句)

;Clear SDRAM End

1

                 ;Initialize stacks

          bl       InitStacks

%B, %F可以这样理解: B表示before,向前。F表示after,向后

2016-04-13 16:02:09 stephenbruce 阅读数 2517

bne里的1b是向后跳转到局部标签1处执行,b表示backward,例如:

对应的还有bne 1f(向前跳到局部标签1处执行)

1: ;A

cmp r0, #0

beq 1f ; r0==0那么向前跳转到B处执行

bne 1b ; 否则向后跳转到A处执行

1: ;B

1b,1f里的b和f表示backward和forward,1表示局部标签1

TST指令是数据处理指令,用于把一个寄存器的内容和另一个寄存器的内容或立即数进行按位的与运算,并根据运算结果更新CPSR中条件标志位的值。 例如:TST R1,#%1用于测试在寄存器R1中是否设置了最低位。

BEQ指定是跳转指令,但是跳转要满足一定的条件,例:CMP R1,#0 BEQ Label 即当R1和0相等的时候程序跳到标号Label处执行

ARM汇编指令总结

阅读数 786