我的github: https://github.com/WHJWNAVY

Uboot中start.S源码的指令级的详尽解析

嵌入式编程 WHJWNAVY 994℃


 

本文转自: Uboot中start.S源码的指令级的详尽解析

版权归原作者所有!感谢原作者的开源精神!

Uboot中start.S源码的指令级的详尽解析

版本:v1.9

Crifan Li

摘要

本文对Uboot中的Start.S的源码的几乎每一行,都进行了详细的解析

  1. 本文提供多种格式供:
  2. http://http://www.crifan.com/files/doc/docbook/uboot_starts_analysis/release/html/uboot_starts_analysis.html
  3. 有任何意见,建议,提交bug等,都欢迎去讨论组发帖讨论:
  4. http://http://www.crifan.com/bbs/categories/uboot_starts_analysis/

在线阅读

HTML

1

HTMLs

2

PDF

3

CHM

4

TXT

5

RTF

6

WEBHELP

7

下载(7zip压缩包)

HTML

8

HTMLs

9

PDF

10

CHM

11

TXT

12

RTF

13

WEBHELP

14

修订历史

修订 1.9

2013-09-04 

crl 

  1. 通过Docbook发布
    1. 修正了一些琐碎的笔误,同时增添了些琐碎内容
    2. 修正了0xdeadbeef的解释
    3. 更新了所有的xml:id

修订 1.6

2011-05-01 

crl 

  1. 添加汇编学习记录
    1. 添加了如何查看C或汇编的源代码所对应的真正的汇编代码
      1. 添加Start.S的总结Start.S的各个部分的总结
      2. Uboot中的内存的layout
    2. 更加详细地解释了为何ARM9中PC=PC+8
    3. 添加了一些其他的细节的内容
    4. 修正一些拼写错误

修订 1.0

2011-04-17 

crl 

  1. 详细解释了uboot的start.s中的每行代码
    1. 添加了相关知识点的详细解释

Uboot中start.S源码的指令级的详尽解析:

Crifan Li

版本:v1.9

出版日期 2013-09-04

版权 © 2013 Crifan, http://crifan.com


 

本文章遵从:署名-非商业性使用 2.5 中国大陆(CC BY-NC 2.5)15

 


 

目录

正文之前      #


 

1. 本文内容      #

2. 本文目标      #

3. 代码来源      #

4. 阅读此文所要具有的前提知识      #

5. 声明      #

1. start.S详解      #


 

1.1. 设置CPU模式      #


 

1.1.1. globl      #

1.1.2. _start      #

1.1.3. ldr      #

1.1.4. .word      #

1.1.5. .balignl      #

1.1.6. _TEXT_BASE _armboot_start      #

1.1.7. _bss_start _bss_end      #

1.1.8. FREE_RAM_END FREE_RAM_SIZE      #

1.1.9. IRQ_STACK_START FIQ_STACK_START      #

1.1.10. cpsr      #

1.1.11. bic      #

1.1.12. orr      #

1.1.13. msr      #

1.2. 关闭看门狗      #


 

1.2.1. pWTCON INTMOD INTMSK INTSUBMSK CLKDIVN      #

1.2.2. ldr pWTCON      #

1.2.3. mov      #

1.2.4. str      #

1.3. 关闭中断      #


 

1.3.1. set INTMSK      #

1.3.2. set INTSUBMSK      #

1.3.3. set CLKDIVN      #

1.3.4. bl      #

1.4. 设置堆栈sp指针      #

 

1.4.1. stack_setup      #

1.4.2. calc stack      #

1.4.3. bl clock_init      #

1.4.4. adr      #

1.4.5. clear_bss      #

1.4.6. cal armboot size from _armboot_start      #

1.4.7. cal armboot size from CopyCode2Ram      #

1.5. 清除bss段      #


 

1.5.1. clear_bss      #

1.5.2. clear css loop      #

1.5.3. ldr pc      #

1.5.4. cpu_init_crit      #

1.5.5. disable MMU      #

1.5.6. clear bits      #

1.5.7. bl lowlevel_init      #

1.6. 异常中断处理      #


 

1.6.1. macros stmia      #

1.6.2. cal reg value and store      #

1.6.3. irq_save_user_regs irq_restore_user_regs      #

1.6.4. exception handlers      #

1.6.5. Launch      #

1.6.6. int_return      #

2. start.S的总结      #


 

2.1. start.S各个部分的总结      #

2.2. Uboot中的内存的Layout      #

3. 相关知识点详解      #


 

3.1. 如何查看C或汇编的源代码所对应的真正的汇编代码      #

3.2. uboot初始化中,为何要设置CPU为SVC模式而不是设置为其他模式      #

3.3. 什么是watchdog + 为何在要系统初始化的时候关闭watchdog      #


 

3.3.1. 什么是watchdog      #

3.3.2. 为何在要系统初始化的时候关闭watchdog      #

3.4. 为何ARM7中PC=PC+8      #


 

3.4.1. 为何ARM9和ARM7一样,也是PC=PC+8      #

3.5. AMR寄存器的别名 + APCS      #


 

3.5.1. ARM中的寄存器的别名      #

3.5.2. 什么是APCS      #

3.6. 为何C语言(的函数调用)需要堆栈,而汇编语言却不需要堆栈      #


 

3.6.1. 保存现场/上下文      #


 

3.6.1.1. 什么叫做上下文context      #

3.6.2. 传递参数      #

3.6.3. 举例分析C语言函数调用是如何使用堆栈的      #

3.7. 关于为何不直接用mov指令,而非要用adr伪指令      #

3.8. mov指令的操作数的取值范围到底是多少      #

3.9. 汇编学习总结记录      #


 

3.9.1. 汇编中的标号=C中的标号      #

3.9.2. 汇编中的跳转指令=C中的goto      #

3.9.3. 汇编中的.globl=C语言中的extern      #

3.9.4. 汇编中用bl指令和mov pc,lr来实现子函数调用和返回      #

3.9.5. 汇编中的对应位置有存储值的标号 = C语言中的指针变量      #

3.9.6. 汇编中的ldr+标号,来实现C中的函数调用      #

3.9.7. 汇编中设置某个寄存器的值或给某个地址赋值      #

参考书目      #

 


 

插图清单

1.1. LDR指令的语法      #

1.2. CPSR/SPSR的位域结构      #

1.3. pWTCON      #

1.4. INTMOD      #

1.5. INTMSK      #

1.6. INTSUBMSK      #

1.7. CLKDIVN      #

1.8. WTCON寄存器的位域      #

1.9. INTMSK寄存器的位域      #

1.10. INTSUBMSK寄存器的位域      #

1.11. INTSUBMSK寄存器的位域      #

1.12. macro的语法      #

1.13. LDM/STM的语法      #

1.14. 条件码的含义      #

2.1. Uboot中的内存的Layout      #

3.1. AMR7三级流水线      #

3.2. ARM7三级流水线状态      #

3.3. ARM7三级流水线示例      #

3.4. ARM7三级流水线 vs ARM9五级流水线      #

3.5. ARM7三级流水线到ARM9五级流水线的映射      #

3.6. ARM9的五级流水线示例      #

3.7. ARM9的五级流水线中为何PC=PC+8      #

3.8. ARM Application Procedure Call Standard (AAPCS)      #

3.9. 数据处理指令的指令格式      #

 


 

表格清单

1.1. global的语法      #

1.2. .word的语法      #

1.3. balignl的语法      #

1.4. CPSR Bitfield      #

1.5. CPSR=0xD3的位域及含义      #

1.6. 控制寄存器1的位域含义      #

1.7. 时钟模式      #

1.8. 关于访问控制位在域访问控制寄存器中的含义      #

1.9. 关于访问允许(AP)位的含义      #

3.1. ARM中CPU的模式      #

3.2. ARM寄存器的别名      #

3.3. mov指令0xe3a00453的位域含义解析      #

 


 

范例清单

3.1. 汇编中的ldr加标号实现函数调用 示例      #

3.2.      #

3.3.      #

 


 

正文之前


 

1. 本文内容

此文主要内容就是分析start.S这个汇编文件的内容,即ARM上电后的最开始那一段的启动过程。


 

2. 本文目标

本文的目标是,希望看完此文的读者,可以达到:

这样的目的,是为了读者看完本文后,再去看其他类似的启动相关的源码,能明白需要做什么事情,然后再看别的系统是如何实现相关的内容的,达到一定程度的触类旁通。

总体说就是,要做哪些,为何要这么做,如何实现的,即英语中常说的:

此三方面都清楚理解了,那么也才能算真正懂了。


 

3. 代码来源

所用代码来自TQ2440官网,天嵌的bbs上下载下来的uboot中的源码:

u-boot-1.1.6_20100601\ opt\ EmbedSky\ u-boot-1.1.6\ cpu\ arm920t\ start.S

下载地址为:2010年6月 最新TQ2440光盘下载 (Linux内核,WinCE的eboot,uboot均有更新)1


 

4. 阅读此文所要具有的前提知识

阅读此文之前,你至少要对TQ2440的板子有个基本的了解,

以及要了解开发板初始化的大概要做的事情,比如设置输入频率,设置堆栈等等。

另外,至少要有一定的C语言的基础,这样更利于理解汇编代码。


 

5. 声明

由于水平有限,难免有误,欢迎指正:admin (at) crifan.com

欢迎转载,但请注明作者。

 


 

 1  start.S详解

下面将详细解释uboot中的start.S中的每一行代码。详细到,每个指令的语法和含义,都进行详细讲解,使得此文读者可以真正搞懂具体的含义,即what。

以及对于一些相关的问题,深入探究为何要这么做,即why。

对于uboot的start.S,主要做的事情就是系统的各个方面的初始化。

从大的方面分,可以分成这几个部分:

下面来对start.S进行详细分析,看看每一个部分,是如何实现的。


 

1.1. 设置CPU模式


 

1.1.1. globl



/*
* armboot – Startup Code for ARM920 CPU-core
*
* Copyright (c) 2001 Marius Gr鰃er <mag@sysgo.de>
* Copyright (c) 2002 Alex Z黳ke <azu@sysgo.de>
* Copyright (c) 2002 Gary Jennejohn <gj@denx.de>
*
* See file CREDITS for list of people who contributed to this
* project.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation; either version 2 of
* the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston,
* MA 02111-1307 USA
*/

#include <config.h>
#include <version.h>


/*
*************************************************************************
*
* Jump vector table as in table 3.1 in [1]
*
*************************************************************************
*/


.globl
_start

Directive 

Description 

Syntax 

Example 

.global

Makes symbol visible to the linker

.global symbol 

.global MyAsmFunc

.globl

Same as .global

.globl symbol 

.globl MyOtherAsmFunc

 

1.1.2. _start



_start
: b reset

    1. A label is written as a symbol immediately followed by a colon `:. The symbol then represents the current value of the active location counter, and is, for example, a suitable instruction operand. You are warned if you use the same symbol to represent two different locations: the first definition overrides any other definitions.
  1. 而_start标号后面的:


 

1.1.3. ldr



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

    1. LDR指令的格式为:
    2. LDR{ 条件} 目的寄存器,<存储器地址>
    3. LDR指令用于从存储器中将一个32位的字数据传送到目的寄存器中。该指令通常用于从存储器中读取32位的字数据到通用寄存器,然后对数据进行处理。当程序计数器PC作为目的寄存器时,指令从存储器中读取的字数据被当作目的地址,从而可以实现程序流程的跳转。该指令在程序设计中比较常用,且寻址方式灵活多样,请读者认真掌握。
    4. 指令示例:
    5. LDR R0,[R1] ;将存储器地址为R1的字数据读入寄存器R0。
    6. LDR R0,[R1,R2] ;将存储器地址为R1+R2的字数据读入寄存器R0。
    7. LDR R0,[R1,#8] ;将存储器地址为R1+8的字数据读入寄存器R0。
    8. LDR R0,[R1,R2]! ;将存储器地址为R1+R2的字数据读入寄存器R0,并将新地址R1+R2写入R1。
    9. LDR R0,[R1,#8]! ;将存储器地址为R1+8的字数据读入寄存器R0,并将新地址R1+8写入R1。
    10. LDR R0,[R1],R2 ;将存储器地址为R1的字数据读入寄存器R0,并将新地址R1+R2写入R1。
    11. LDR R0,[R1,R2,LSL#2]! ;将存储器地址为R1+R2×4的字数据读入寄存器R0,并将新地址R1+R2×4写入R1。
    12. LDRR0,[R1],R2,LSL#2 ;将存储器地址为R1的字数据读入寄存器R0,并将新地址R1+R2×4写入R1。”
    13. http://http://www.pczpg.com/a/2010/0607/11062.html
    14. ARM是RISC结构,数据从内存到CPU之间的移动只能通过L/S指令来完成,也就是ldr/str指令。
    15. 比如想把数据从内存中某处读取到寄存器中,只能使用ldr
    16. 比如:
    17. ldr r0, 0x12345678
    18. 就是把0x12345678这个地址中的值存放到r0中。
  1. 上面那些ldr的作用,以第一个_undefined_instruction为例,就是将地址为_undefined_instruction中的一个word的值,赋值给pc。


 

1.1.4. .word



_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

  1. .word .word expr { ,expr} … 分配一段字内存单元,并用expr初始化字内存单元(32bit)

Directive 

Description 

Syntax 

Example 

.word

Define word expr (32bit numbers)

.word expr { , …}

.word 144511, 0x11223


 

1.1.5. .balignl



.balignl
16,0xdeadbeef

Directive 

Description 

Syntax 

Example 

.balignl

Word align the following code to alignment byte boundary (default=4). Fill skipped words with fill (default=0 or NOP). If the number of bytes skipped is greater than max, then dont align (default=alignment ).

.balignl { alignment} { , fill} { , max}  

.balignl

 

1.1.6. _TEXT_BASE _armboot_start



/*
*************************************************************************
*
* Startup Code (reset vector)
*
* do important init only if we dont start from memory!
* relocate armboot to ram
* setup stack
* jump to second stage
*
*************************************************************************
*/

_TEXT_BASE
:
.word TEXT_BASE

.globl _armboot_start
_armboot_start:
.word _start

 

1.1.7. _bss_start _bss_end



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

.globl _bss_end
_bss_end:
.word _end


 

1.1.8. FREE_RAM_END FREE_RAM_SIZE



.globl FREE_RAM_END
FREE_RAM_END:
.word 0x0badc0de

.globl FREE_RAM_SIZE
FREE_RAM_SIZE:
.word 0x0badc0de


 

1.1.9. IRQ_STACK_START FIQ_STACK_START



#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


 

1.1.10. cpsr



/*
* the actual reset code
*/

reset:
/*
* set the cpu to SVC32 mode
*/
mrs
r0,cpsr

    1. http://http://www.csie.nctu.edu.tw/~wjtsai/EmbeddedSystemDesign/Ch2-bootloader.pdf

31 

30 

29 

28 

 

7 

6 

 

4 

3 

2 

1 

0 

说明

N 

Z 

C 

V 

  

I 

F 

  

M4 

M3 

M2 

M1 

M0 

  

  

0 

0 

0 

0 

0 

User26 模式

  

0 

0 

0 

0 

1 

FIQ26 模式

  

0 

0 

0 

1 

0 

IRQ26 模式

  

0 

0 

0 

1 

1 

SVC26 模式

  

1 

0 

0 

0 

0 

User 模式

  

1 

0 

0 

0 

1 

FIQ 模式

  

1 

0 

0 

1 

0 

IRQ 模式

  

1 

0 

0 

1 

1 

SVC 模式

  

1 

0 

1 

1 

1 

ABT 模式

  

1 

1 

0 

1 

1 

UND 模式


 

1.1.11. bic



bic
r0,r0,#0x1f


 

1.1.12. orr



orr
r0,r0,#0xd3


 

1.1.13. msr



msr
cpsr,r0

所以,上面四行汇编代码的含义就很清楚了。

先是把CPSR的值放到r0寄存器中,然后清除bit[4:0],然后再或上

0xd3=11 0 10111b


 

 1.5. CPSR=0xD3的位域及含义

CPSR位域

7 

6 

5 

4 

3 

2 

1 

0 

位域含义

I 

F 

  

M4 

M3 

M2 

M1 

M0 

0xD3 

1 

1 

0 

1

0 

0 

1 

1 

对应含义

关闭中断IRQ

关闭快速中断FIQ

  

设置CPU为SVC模式,这和上面代码注释中的”set the cpu to SVC32 mode”,也是一致的。

关于为何设置CPU为SVC模式,而不是设置为其他模式,请参见本文档后面的章节: 3.2 节 “uboot初始化中,为何要设置CPU为SVC模式而不是设置为其他模式”


 

1.2. 关闭看门狗


 

1.2.1. pWTCON INTMOD INTMSK INTSUBMSK CLKDIVN



/* 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) || defined(CONFIG_S3C2440)

# define pWTCON 0x53000000
# define INTMOD 0X4A000004
# define INTMSK 0x4A000008 /* Interupt-Controller base addresses */
# define INTSUBMSK 0x4A00001C
# define CLKDIVN 0x4C000014 /* clock divisor register */
#endif

  1. 其中有S3C2440的CPU的datasheet:
  2. 其中有对应的寄存器定义:


 

1.2.2. ldr pWTCON



#if defined(CONFIG_S3C2400) || defined(CONFIG_S3C2410) || defined(CONFIG_S3C2440)
ldr
r0, =pWTCON

      1. 另外还有一个就是ldr伪指令,虽然ldr伪指令和ARM的ldr指令很像,但是作用不太一样。ldr伪指令可以在立即数前加上=,以表示把一个地址写到某寄存器中,比如:
      2. ldr r0, =0x12345678
      3. 这样,就把0x12345678这个地址写到r0中了。所以,ldr伪指令和mov是比较相似的。
    1. 只不过mov指令后面的立即数是有限制的,这个立即数,能够必须由一个8位的二进制数,即属于0x00-0xFF内的某个值,经过偶数次右移后得到,这样才是合法数据,而ldr伪指令没有这个限制。
    2. 那为何ldr伪指令的操作数没有限制呢,那是因为其是伪指令,写出来的伪指令,最终会被编译器解释成为真正的,合法的指令的,一般都是对应的mov指令。
    3. 这样的话,写汇编程序的时候,使用MOV指令是比较麻烦的,因为有些简单的数据比较容易看出来,有些数据即不容易看出来是否是合法数据。所以,对此,ldr伪指令的出现,就是为了解决这个问题的,你只管放心用ldr伪指令,不用关心操作数,而写出的ldr伪指令,编译器会帮你翻译成对应的真正的汇编指令的。
    4. 而关于编译器是如何将这些ldr伪指令翻译成为真正的汇编指令的,我的理解是,其自动会去算出来对应的操作数,是否是合法的mov 的操作数,如果是,就将该ldr伪指令翻译成mov指令,否则就用别的方式处理,我所观察到的,其中一种方式就是,单独申请一个4字节的空间用于存放操作数,然后用ldr指令实现。
    5. 在uboot中,最后make完毕之后,会生产u-boot,
    6. 通过:


 

1.2.3. mov



mov
r1, #0x0


 

1.2.4. str



str
r1, [r0]

所以,上面几行代码意思也很清楚:

先是用r0寄存器存pWTCON的值,然后r1=0,再将r1中的0写入到pWTCON中,其实就是

pWTCON = 0;

而pWTCON寄存器的具体含义是什么呢?下面就来了解其详细含义:


 

 1.8. WTCON寄存器的位域


注意到bit[0]是Reset Enable/Disable,而设置为0的话,那就是关闭Watchdog的reset了,所以其他位的配置选项,就更不需要看了。

我们只需要了解,在此处禁止了看门狗WatchDog(的复位功能),即可。

关于看门狗的作用,以及为何要在系统初始化的时候关闭看门狗,请参见本文档后面的章节: 3.3 节 “什么是watchdog + 为何在要系统初始化的时候关闭watchdog”


 

1.3. 关闭中断


 

1.3.1. set INTMSK



/*
* mask all IRQs by setting all bits in the INTMR – default
*/
mov r1, #0xffffffff
ldr r0, =INTMSK
str r1, [r0])



 

1.3.2. set INTSUBMSK



# if defined(CONFIG_S3C2410)
ldr r1, =0x3ff

ldr r0, =INTSUBMSK
str r1, [r0]
# elif defined(CONFIG_S3C2440)
ldr r1, =0x7fff
ldr r0, =INTSUBMSK
str r1, [r0]
# endif

  1. 后经HateMath3的提醒后,去查证,的确此处设置的0x3ff,是不严谨的。
  2. 因为,根据2410的datasheet中关于INTSUBMSK的解释,bit[10:0]共11位,虽然默认reset的每一位都是1,但是此处对应的mask值,应该是11位全为1=0x7ff。
  3. 即写成0x3ff,虽然是可以正常工作的,但是却不够严谨的。
  4. 此处CPU是是S3C2440,所以用到0x7fff这段代码。
  5. 其意思也很容易看懂,就是将INTSUBMSK寄存器的值设置为0x7fff。
  6. 先贴出对应每一位的含义:


 

1.3.3. set CLKDIVN



#if 0
/* FCLK:HCLK:PCLK = 1:2:4 */
/* default FCLK is 120 MHz ! */
ldr r0, =CLKDIVN

mov r1, #3
str r1, [r0]
#endif
#endif /* CONFIG_S3C2400 || CONFIG_S3C2410 || CONFIG_S3C2440 */

  1. 此处是结束上面的#ifdef


 

1.3.4. bl



/*
* we do sys-critical inits only at reboot,
* not when booting from ram!
*/
#ifndef CONFIG_SKIP_LOWLEVEL_INIT
bl
cpu_init_crit
#endif


 

1.4. 设置堆栈sp指针


 

1.4.1. stack_setup



/* Set up the stack */
stack_setup:
ldr r0, _TEXT_BASE /* upper 128 KiB: relocated uboot */

sub r0, r0, #CFG_MALLOC_LEN /* malloc area */
sub r0, r0, #CFG_GBL_DATA_SIZE /* bdinfo */


 

1.4.2. calc stack



#ifdef CONFIG_USE_IRQ
sub r0, r0, #(CONFIG_STACKSIZE_IRQ+CONFIG_STACKSIZE_FIQ)

#endif
sub sp, r0, #12 /* leave 3 words for abort-stack */


 

1.4.3. bl clock_init



bl clock_init


  1. 而对于C语言,为何就需要堆栈,而汇编却不需要堆栈的原因,请参见本文后面的内容: 3.6 节 “为何C语言(的函数调用)需要堆栈,而汇编语言却不需要堆栈”


 

1.4.4. adr



#ifndef CONFIG_SKIP_RELOCATE_UBOOT
relocate: /* relocate U-Boot to RAM */
adr
r0, _start /* r0 <- current position of code */

    1. 1、ADR伪指令— 小范围的地址读取
    2. ADR伪指令将基于PC相对偏移的地址值或基于寄存器相对偏移的地址值读取到寄存器中。
    3. 在汇编编译器编译源程序时,ADR伪指令被编译器替换成一条合适的指令。通常,编译器用一条ADD指令或SUB指令来实现该ADR伪指令的功能,若不能用一条指令实现,则产生错误,编译失败。
    4. ADR伪指令格式 :ADR{ cond} register, expr
    5. 地址表达式expr的取值范围:
    6. 当地址值是字节对齐时,其取指范围为: +255 ~ 255B;
    7. 当地址值是字对齐时,其取指范围为: -1020 ~ 1020B;
  1. 所以,上述:
  2. 对于为何是PC=PC+8,请参见后面的内容: 3.4 节 “为何ARM7中PC=PC+8″
  3. 对于此处为何不直接用mov指令,却使用adr指令,请参见后面内容: 3.7 节 “关于为何不直接用mov指令,而非要用adr伪指令”
  4. 对于mov指令的操作数的取值范围,请参见后面内容: 3.8 节 “mov指令的操作数的取值范围到底是多少”

adr r0, _start

的伪代码,被翻译成实际汇编代码为:

33d000a4: e24f00ac sub r0, pc, #172 ; 0xac

其含义就是,通过计算PC+8-172 _start的地址,

而_start的地址,即相对代码段的0地址,是这个地址在运行时刻的值,而当ARM920T加电启动后,,此处是从Nor Flash启动,对应的代码,也是在Nor Flash中,对应的物理地址是0x0,所以,此时_start的值就是0,而不是0x33d00000。

所以,此时:

r0 = 0x0


 

1.4.5. clear_bss



ldr r1, _TEXT_BASE
/* test if we run from flash or RAM */

cmp r0, r1 /* dont reloc during debug */

beq clear_bss

 

1.4.6. cal armboot size from _armboot_start




ldr r2, _armboot_start
ldr r3, _bss_start

sub r2, r3, r2 /* r2 <- size of armboot */


 

1.4.7. cal armboot size from CopyCode2Ram



#if 1

bl CopyCode2Ram /* r0: source, r1: dest, r2: size */
#else
add r2, r0, r2 /* r2 <- source end address */

copy_loop:
ldmia r0!, { r3-r10} /* copy from source address [r0] */
stmia r1!, { r3-r10} /* copy to target address [r1] */
cmp r0, r2 /* until source end addreee [r2] */
ble copy_loop
#endif
#endif /* CONFIG_SKIP_RELOCATE_UBOOT */


 

1.5. 清除bss段


 

1.5.1. clear_bss



clear_bss:

ldr r0, _bss_start /* find start of bss segment */
ldr r1, _bss_end /* stop here */
mov r2, #0x00000000 /* clear */


 

1.5.2. clear css loop



clbss_l:str r2, [r0] /* clear loop… */
add r0, r0, #4
cmp r0, r1
ble clbss_l


 

1.5.3. ldr pc



#if 0

/* try doing this stuff after the relocation */
ldr r0, =pWTCON
mov r1, #0x0
str r1, [r0]

/*
* mask all IRQs by setting all bits in the INTMR – default
*/
mov r1, #0xffffffff
ldr r0, =INTMR
str r1, [r0]

/* FCLK:HCLK:PCLK = 1:2:4 */
/* default FCLK is 120 MHz ! */
ldr r0, =CLKDIVN
mov r1, #3
str r1, [r0]
/* END stuff after relocation */
#endif

ldr pc, _start_armboot

_start_armboot: .word start_armboot


 

1.5.4. cpu_init_crit



/*
*************************************************************************
*
* CPU_init_critical registers
*
* setup important registers
* setup memory timing
*
*************************************************************************
*/
#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 */

    1. ARM 微处理器可支持多达 16 个协处理器,用于各种协处理操作,在程序执行的过程中,每个协处理器只执行针对自身的协处理指令,忽略 ARM 处理器和其他协处理器的指令。ARM 的协处理器指令主要用于 ARM 处理器初始化 ARM 协处理器的数据处理操作,以及在ARM 处理器的寄存器和协处理器的寄存器之间传送数据,和在 ARM 协处理器的寄存器和存储器之间传送数据。 ARM 协处理器指令包括以下 5 条:
      1. CDP 协处理器数操作指令
      2. LDC 协处理器数据加载指令
      3. STC 协处理器数据存储指令
      4. MCR ARM 处理器寄存器到协处理器寄存器的数据传送指令
      5. MRC 协处理器寄存器到ARM 处理器寄存器的数据传送指令

      ……

      CP15系统控制协处理器

      CP15 —系统控制协处理器 (the system control coprocessor)他通过协处理器指令MCR和MRC提供具体的寄存器来配置和控制caches、MMU、保护系统、配置时钟模式(在bootloader时钟初始化用到)

      CP15的寄存器只能被MRC和MCR(Move to Coprocessor from ARM Register )指令访问

  1. 一些要说明的内容,见下::
    1. you can only access CP15 registers with MRC and MCR instructions in a privileged mode. The assembler for these instructions is:
    2. MCR/MRC{ cond} P15,opcode_1,Rd,CRn,CRm,opcode_2
    3. The CRn field of MRC and MCR
    4. instructions specifies the coprocessor register to access. The CRm field and opcode_2 fields specify a particular action when addressing registers. The L bit distinguishes between an MRC (L=1) and an MCR (L=0).
    5. Note:
    6. Attempting to read from a nonreadable register, or to write to a nonwritable register causes unpredictable results.
    7. The opcode_1, opcode_2, and CRm fields should be zero, except when the values specified are used to select the desired operations, in all instructions that access CP15.
    8. Using other values results in unpredictable behavior
  2. CP15有很多个寄存器,分别叫做寄存器0(Register 0),到寄存器15(Register 15),
  3. 每个寄存器分别控制不同的功能,而且有的是只读,有的是只写,有的是可读写。
  4. 而且这些寄存器的含义,随着版本ARM内核版本变化而不断扩展,详情请参考:Processor setup via co-processor 15 and about co-processors4
  5. 其中,根据我们此处关心的内容,摘录部分内容如下:
    1. ARM 710
      1. Register 7 – IDC flush (write only)
      2. Any data written to this location will cause the IDC (Instruction/Data cache) to be flushed.

      ……

      StrongARM SA110

      ……

      1. Register 7 – Cache control (write only)
      2. Any data written to this location will cause the selected cache to be flushed.
      3. The OPC_2 and CRm co-processor fields select which cache
      4. operation should occur:
      5. Function OPC_2 CRm Data

      <CRm>和<opcode_2>两者组合决定对协处理器寄存器进行所需要的操作,如果没有指定,则将为<CRm>为C0,opcode_2为0

  6. 当opcode_2为0,CRm为0111=7,就是我们要找的,其作用是”Flush I + D“,即清空指令缓存I Cache和数据缓存D Cache。
  7. 根据该表,同理,如果是opcode_2=0,而CRm=0101b=5,那么对应的就是去”Flush I“,即只清除指令缓存I Cache了。
  8. 而对应的指令也就是

Function 

Rd 

Instruction 

Invalidate ICache and DCache 

SBZ 

MCR p15,0,Rd,c7,c7,0 

Function 

Rd 

Instruction 

Invalidate TLB(s) 

SBZ 

MCR p15,0,Rd,c8,c7,0 


 

1.5.5. disable MMU



/*
* disable MMU stuff and caches
*/
mrc p15, 0, r0, c1, c0, 0


    1. http://http://www.heyrick.co.uk/assembler/coprocmnd.html
    2. StrongARM SA110
      1. Register 1 – Control (read/write)
      2. All values set to 0 at power-up.
        1. Bit 0 – On-chip MMU turned off (0) or on (1)
        2. Bit 1 – Address alignment fault disabled (0) or enabled (1)
        3. Bit 2 – Data cache turned off (0) or on (1)
        4. Bit 3 – Write buffer turned off (0) or on (1)
        5. Bit 7 – Little-endian operation if 0, big-endian if 1
        6. Bit 8 – System bit – controls the MMU permission system
        7. Bit 9 – ROM bit – controls the MMU permission system
        8. Bit 12 – Instruction cache turned off (0) or on (1)”
  1. 所以,对应内容就是,向bit[CRm]中写入opcode_2,即向bit[0]写入0,对应的作用为”On-chip MMU turned off“,即关闭MMU。


 

1.5.6. clear bits




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

    1. Domain access control
    2.  1.9 “关于访问允许(AP)位的含义”shows how to interpret the Access Permission (AP) bits and how their interpretation is dependent on the S and R bits (control register bits 8 and 9)

Register bits 

Name 

Function 

Value 

31 

iA bit 

Asynchronous clock select 

See Table 2.115

30 

nF bit 

notFastBus select 

See Table 2.116

29:15 

 

Reserved 

Read = Unpredictable
Write = Should be zero

14 

RR bit 

Round robin replacement 

0 = Random replacement
1 = Round-robin replacement

13 

V bit 

Base location of exception registers 

0 = Low addresses = 0x00000000
1 = High addresses = 0xFFFF0000

12 

I bit 

ICache enable 

0 = ICache disabled
1 = ICache enabled

11:10 

 

Reserved 

Read = 00
Write = 00

9 

R bit 

ROM protection 

This bit modifies the MMU protection system. See Domain access control7

8 

S bit 

System protection 

This bit modifies the MMU protection system. See Domain access control8

7 

B bit 

Endianness 

0 = Little-endian operation
1 = Big-endian operation

6:3 

 

Reserved 

Read = 1111
Write = 1111

2 

C bit 

DCache enable 

0 = DCache disabled
1 = DCache enabled

1 

A bit 

Alignment fault enable 

Data address alignment fault checking

0 = Fault checking disabled
1 = Fault checking enabled

0 

M bit 

MMU enable 

0 = MMU disabled
1 = MMU enabled

Clocking mode 

iA 

nF 

FastBus mode 

0 

0 

Synchronous 

0 

1 

Reserved 

1 

0 

Asynchronous 

1 

1 

Value 

Meaning 

Description 

00 

No access 

Any access generates a domain fault 

01 

Client 

Accesses are checked against the access permission bits in the section or page descriptor 

10 

Reserved 

Reserved. Currently behaves like the no access mode 

11 

Manager 

Accesses are not checked against the access permission bits so a permission fault cannot be generated 

AP 

S 

R 

Supervisor permissions 

User permissions 

Description 

00 

0 

0 

No access 

No access 

Any access generates a permission fault 

00 

1 

0 

Read-only 

No access 

Only Supervisor read permitted 

00 

0 

1 

Read-only 

Read-only 

Any write generates a permission fault 

00 

1 

1 

Reserved 

 

 

01 

x 

x 

Read/write 

No access 

Access allowed only in Supervisor mode 

10 

x 

x 

Read/write 

Read-only 

Writes in User mode cause permission fault 

11 

x 

x 

Read/write 

Read/write 

All access types permitted in both modes 

xx 

1 

1 

Reserved 

 

  


 

1.5.7. bl lowlevel_init



/*
* 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 */


 

1.6. 异常中断处理


 

1.6.1. macros stmia



/*
*************************************************************************
*
* Interrupt handling
*
*************************************************************************
*/

@
@ IRQ stack frame.
@
#define S_FRAME_SIZE 72

#define S_OLD_R0 68
#define S_PSR 64
#define S_PC 60
#define S_LR 56
#define S_SP 52

#define S_IP 48
#define S_FP 44
#define S_R10 40
#define S_R9 36
#define S_R8 32
#define S_R7 28
#define S_R6 24
#define S_R5 20
#define S_R4 16
#define S_R3 12
#define S_R2 8
#define S_R1 4
#define S_R0 0

#define MODE_SVC 0x13
#define I_BIT 0x80

/*
* use bad_save_user_regs for abort/prefetch/undef/swi …
* use irq_save_user_regs / irq_restore_user_regs for IRQ/FIQ handling
*/
.macro bad_save_user_regs

sub sp, sp, #S_FRAME_SIZE
stmia sp, { r0 – r12} @ Calling r0-r12
ldr r2, _armboot_start



 

1.6.2. cal reg value and store



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

此处虽然每行代码基本看懂了,但是到底此bad_save_user_regs函数是做什么的,还是不太清楚,有待以后慢慢深入理解。

 

1.6.3. irq_save_user_regs irq_restore_user_regs



.macro irq_save_user_regs
sub sp, sp, #S_FRAME_SIZE
stmia sp, { r0 – r12} @ Calling r0-r12
add r8, sp, #S_PC
stmdb r8, { sp, lr} ^ @ Calling SP, LR
str lr, [r8, #0] @ Save calling PC
mrs r6, spsr
str r6, [r8, #4] @ Save CPSR
str r0, [r8, #8] @ Save OLD_R0
mov r0, sp
.endm

.macro irq_restore_user_regs
ldmia sp, { r0 – lr} ^ @ Calling r0 – lr
mov r0, r0
ldr lr, [sp, #S_PC] @ Get PC
add sp, sp, #S_FRAME_SIZE
subs pc, lr, #4 @ return & move spsr_svc into cpsr
.endm

.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, [r13] @ save caller lr / spsr
mrs lr, spsr
str lr, [r13, #4]
mov r13, #MODE_SVC @ prepare SVC-Mode
@ msr spsr_c, r13
msr spsr, r13
mov lr, pc
movs pc, lr
.endm

.macro get_irq_stack @ setup IRQ stack
ldr sp, IRQ_STACK_START
.endm

.macro get_fiq_stack @ setup FIQ stack
ldr sp, FIQ_STACK_START
.endm


  1. 而此处,就是用到了,前面已经在cpu_init()中重新计算正确的值了。
  2. 即算出IRQ堆栈的起始地址,其算法很简单,就是:


 

1.6.4. exception handlers



/*
* exception handlers
*/
.align 5
undefined_instruction:

get_bad_stack
bad_save_user_regs
bl do_undefined_instruction
.align 5



software_interrupt:
get_bad_stack
bad_save_user_regs
bl do_software_interrupt

.align 5
prefetch_abort:
get_bad_stack
bad_save_user_regs
bl do_prefetch_abort

.align 5
data_abort:
get_bad_stack
bad_save_user_regs
bl do_data_abort

.align 5
not_used:
get_bad_stack
bad_save_user_regs
bl do_not_used



 

1.6.5. Launch



@ HJ
.globl Launch

.align 4
Launch:
mov r7, r0
@ diable interrupt
@ disable watch dog timer
mov r1, #0x53000000
mov r2, #0x0
str r2, [r1]

ldr r1,=INTMSK
ldr r2,=0xffffffff @ all interrupt disable
str r2,[r1]

ldr r1,=INTSUBMSK
ldr r2,=0x7ff @ all sub interrupt disable
str r2,[r1]

ldr r1, = INTMOD
mov r2, #0x0 @ set all interrupt as IRQ (not FIQ)
str r2, [r1]

@
mov ip, #0
mcr p15, 0, ip, c13, c0, 0 @ /* zero PID */
mcr p15, 0, ip, c7, c7, 0 @ /* invalidate I,D caches */
mcr p15, 0, ip, c7, c10, 4 @ /* drain write buffer */
mcr p15, 0, ip, c8, c7, 0 @ /* invalidate I,D TLBs */
mrc p15, 0, ip, c1, c0, 0 @ /* get control register */
bic ip, ip, #0x0001 @ /* disable MMU */
mcr p15, 0, ip, c1, c0, 0 @ /* write control register */

@ MMU_EnableICache
@mrc p15,0,r1,c1,c0,0
@orr r1,r1,#(1<<12)
@mcr p15,0,r1,c1,c0,0

#ifdef CONFIG_SURPORT_WINCE
bl Wince_Port_Init
#endif

@ clear SDRAM: the end of free mem(has wince on it now) to the end of SDRAM
ldr r3, FREE_RAM_END
ldr r4, =PHYS_SDRAM_1+PHYS_SDRAM_1_SIZE @ must clear all the memory unused to zero
mov r5, #0

ldr r1, _armboot_start
ldr r2, =On_Steppingstone
sub r2, r2, r1
mov pc, r2
On_Steppingstone:
2: stmia r3!, { r5}
cmp r3, r4
bne 2b

@ set sp = 0 on sys mode
mov sp, #0

@ add by HJ, switch to SVC mode
msr cpsr_c, #0xdf @ set the I-bit = 1, diable the IRQ interrupt
msr cpsr_c, #0xd3 @ set the I-bit = 1, diable the IRQ interrupt
ldr sp, =0x31ff5800


nop
nop
nop
nop

mov pc, r7 @ Jump to PhysicalAddress
nop
mov pc, lr


 

1.6.6. int_return




#ifdef CONFIG_USE_IRQ
.align 5
irq:
/* add by http://www.embedsky.net to use IRQ for USB and DMA */
sub lr, lr, #4 @ the return address
ldr sp, IRQ_STACK_START @ the stack for irq
stmdb sp!, { r0-r12,lr } @ save registers


ldr lr, =int_return @ set the return addr
ldr pc, =IRQ_Handle @ call the isr
int_return:
ldmia sp!, { r0-r12,pc } ^ @ return from interrupt
.align 5
fiq:
get_fiq_stack
/* someone ought to write a more effiction fiq_save_user_regs */
irq_save_user_regs
bl do_fiq
irq_restore_user_regs
#else

.align 5
irq:
get_bad_stack
bad_save_user_regs
bl do_irq

.align 5
fiq:
get_bad_stack
bad_save_user_regs
bl do_fiq

#endif

 


 

 2  start.S的总结


 

2.1. start.S各个部分的总结

其实关于start.S这个汇编文件,主要做的事情就是系统的各个方面的初始化。

关于每个部分,上面具体的代码实现,也都一行行的解释过了,此处不再赘述。

此处,只是简单总结一下,其实现的方式,或者其他需要注意的地方。


 

2.2. Uboot中的内存的Layout

总结了start.S做的事情之后,另外想在此总结一下,uboot中,初始化部分的代码执行后,对应的内存空间,都是如何规划,什么地方放置了什么内容。此部分内容,虽然和start.S没有直接的关系,但是start.S中,堆栈sp的计算等,也和这部分内容有关。

下面这部分的uboot的内存的layout,主要是根据:

分析而得出的。

uboot的内存的layout,用图表表示就是:


 

 2.1. Uboot中的内存的Layout


 


 

 3  相关知识点详解


 

3.1. 如何查看C或汇编的源代码所对应的真正的汇编代码

首先解释一下,由于汇编代码中会存在一些伪指令等内容,所以,写出来的汇编代码,并不一定是真正可以执行的代码,这些类似于伪指令的汇编代码,经过汇编器,转换或翻译成真正的可以执行的汇编指令。所以,上面才会有将”汇编源代码”转换为”真正的汇编代码”这一说。

然后,此处对于有些人不是很熟悉的,如何查看源代码真正对应的汇编代码。

此处,对于汇编代码,有两种:

总的来说,两者区别很小,后者主要是更新了外部函数的地址等等,对于汇编代码本身,至少对于我们一般所去查看源代码所对应的汇编来说,两者可以视为没区别。

在查看源代码所对应的真正的汇编代码之前,先要做一些相关准备工作:

对于C语言的源码,也是同样的方法,用对应的dump工具,去从该C语言的.o文件中,dump出来汇编代码。


 

3.2. uboot初始化中,为何要设置CPU为SVC模式而不是设置为其他模式

在看Uboot的start.S文件时候,发现其最开始初始化系统,做的第一件事情,就是将CPU设置为SVC模式,但是S3C2440的CPU的core是ARM920T,其有7种模式,为何非要设置为SVC模式,而不是设置为其他模式呢?对此,经过一些求证,得出如下原因:

首先,先要了解ARM的CPU的7种模式是哪些:

http://http://www.docin.com/p-73665362.html


 

 3.1. ARM中CPU的模式

处理器模式

说明

备注

用户(usr)

正常程序工作模式

此模式下程序不能够访问一些受操作系统保护的系统资源,应用程序也不能直接进行处理器模式的切换。

系统(sys)

用于支持操作系统的特权任务等

与用户模式类似,但具有可以直接切换到其它模式等特权

快中断(fiq)

支持高速数据传输及通道处理

FIQ异常响应时进入此模式

中断(irq)

用于通用中断处理

IRQ异常响应时进入此模式

管理(svc)

操作系统保护代码

系统复位和软件中断响应时进入此模式

中止(abt)

用于支持虚拟内存和/或存储器保护

在ARM7TDMI没有大用处

未定义(und)

支持硬件协处理器的软件仿真

未定义指令异常响应时进入此模式

另外,7种模式中,除用户usr模式外,其它模式均为特权模式。

对于为何此处是svc模式,而不是其他某种格式,其原因,可以从两方面来看:

    从uboot的目的是初始化硬件的角度来说,设置为svc模式,更有利于其工作。

    因此,此处将CPU设置为SVC模式。

    1. 或者Linux内核文档:
    2. kernel_source_root\ documentation\ arm\ booting
    3. 中也是同样的解释:
      1. The CPU must be in SVC mode
  1. 所以,uboot在最初的初始化阶段,就将CPU设置为SVC模式,也是最合适的。

综上所述,uboot在初始化阶段,就应该将CPU设置为SVC模式。


 

3.3. 什么是watchdog + 为何在要系统初始化的时候关闭watchdog

关于Uboot初始化阶段,在start.S中,为何要去关闭watchdog,下面解释具体的原因:


 

3.3.1. 什么是watchdog

参考嵌入式系统之WATCHDOG(看门狗)概述2

简要摘录如下:

watchdog一般是一个硬件模块,其作用是,在嵌入式操作系统中,很多应用情况是系统长期运行且无人看守,所以难免或者怕万一出现系统死机,那就杯具了,这时,watchdog就会自动帮你重启系统。

那么其是如何实现此功能的呢?那么就要简单解释一下其实现原理了。

watchdog硬件的逻辑就是,其硬件上有个记录超时功能,然后要求用户需要每隔一段时间(此时间可以根据自己需求而配置)去对其进行一定操作,比如往里面写一些固定的值,俗称”喂狗”,那么我发现超时了,即过了这么长时间你还不给偶喂食,那么偶就认为你系统是死机了,出问题了,偶就帮你重启系统。

说白了就是弄个看家狗dog,你要定期给其喂食,如果超时不喂食,那么狗就认为你,他的主人,你的系统,死机了,就帮你reset重启系统。


 

3.3.2. 为何在要系统初始化的时候关闭watchdog

了解了watchdog的原理后,此问题就很容易理解了。

如果不禁用watchdog,那么就要单独写程序去定期”喂狗”,那多麻烦,多无聊啊。

毕竟咱此处只是去用uboot初始化必要的硬件资源和系统资源而已,完全用不到这个watchdog的机制。需要用到,那也是你linux内核跑起来了,是你系统关心的事情,和我uboot没啥关系的,所以肯定此处要去关闭watchdog(的reset功能)了。


 

3.4. 为何ARM7中PC=PC+8

此处解释为何ARM7中,CPU地址,即PC,为何有PC=PC+8这一说法:

众所周知,AMR7,是三级流水线,其细节见图:


 

 3.1. AMR7三级流水线


首先,对于ARM7对应的流水线的执行情况,如下面这个图所示:


 

 3.2. ARM7三级流水线状态


然后对于三级流水线举例如下:


 

 3.3. ARM7三级流水线示例


从上图,其实很容易看出,第一条指令:

add r0, r1,$5

执行的时候,此时PC已经指向第三条指令:

cmp r2,#3

的地址了,所以,是PC=PC+8.


 

3.4.1. 为何ARM9和ARM7一样,也是PC=PC+8

ARM7的三条流水线,PC=PC+8,很好理解,但是AMR9中,是五级流水线,为何还是PC=PC+8,而不是

PC

=PC+(5-1)*4

=PC + 16,

呢?

下面就需要好好解释一番了。

具体解释之前,先贴上ARM7和ARM9的流水线的区别和联系:


 

 3.4. ARM7三级流水线 vs ARM9五级流水线



 

 3.5. ARM7三级流水线到ARM9五级流水线的映射


下面开始对为何ARM9也是PC=PC+8进行解释。

先列出ARM9的五级流水线的示例:


 

 3.6. ARM9的五级流水线示例


举例分析为何PC=PC+8

然后我们以下面uboot中的start.S的最开始的汇编代码为例来进行解释:



00000000 <_start>:
0: ea000014 b 58 <reset>
4: e59ff014 ldr pc, [pc, #20] ; 20 <_undefined_instruction>
8: e59ff014 ldr pc, [pc, #20] ; 24 <_software_interrupt>
c: e59ff014 ldr pc, [pc, #20] ; 28 <_prefetch_abort>
10: e59ff014 ldr pc, [pc, #20] ; 2c <_data_abort>
14: e59ff014 ldr pc, [pc, #20] ; 30 <_not_used>
18: e59ff014 ldr pc, [pc, #20] ; 34 <_irq>
1c: e59ff014 ldr pc, [pc, #20] ; 38 <_fiq>

00000020 <_undefined_instruction>:
20: 00000120 .word 0x00000120

下面对每一个指令周期,CPU做了哪些事情,分别详细进行阐述:

在看下面具体解释之前,有一句话要牢记,那就是:

PC不是指向你正在运行的指令,而是

PC始终指向你要取的指令的地址

认识清楚了这个前提,后面的举例讲解,就容易懂了。

其实,分析到这里,大家就可以看出:

在Cycle3的时候,PC的值,刚好已经在Cycle1和Cycle2,分别加了4,所以Cycle3的时候,PC=PC+8,而同样道理,对于任何一条指令的,都是在Cycle3,指令的Execute执行阶段,如果用到PC的值,那么PC那一时刻,就是PC=PC+8。

所以,此处虽然是五级流水线,但是却不是PC=PC+16,而是PC=PC+8。

进一步地,我们发现,其实PC=PC+N的N,是和指令的执行阶段所处于流水线的深度有关,即此处指令的执行Execute阶段,是五级流水线中的第三个,而这个第三阶段的Execute和指令的第一个阶段的Fetch取指,相差的值是 3 -1 =2,即两个CPU的Cycle,而每个Cycle都会导致PC=+PC+4,所以,指令到了Execute阶段,才会发现,此时PC已经变成PC=PC+8了。

回过头来反观ARM7的三级流水线,也是同样的道理,指令的Execute执行阶段,是处于指令的第三个阶段,同理,在指令计算数据的时候,如果用到PC,就会发现此时PC=PC+8。

同理,假如ARM9的五级流水线,把指令的Execute执行阶段,设计在了第四个阶段,那么就是PC=PC+(第4阶段-1)*4个字节 = PC= PC+12了。

用图来说明PC=PC+8个过程

对于上面的文字的分析过程,可能看起来不是太容易理解,所以,下面这里通过图表来表示具体的流程,就更容易看懂了。其中,下图,是以ARM9的五级流水线的内部架构图为基础,而编辑的出来用于说明为何ARM9的五级流水线,也是PC=PC+8:


 

 3.7. ARM9的五级流水线中为何PC=PC+8


对于上图中的,第一个指令在执行的时候,是使用到了PC的值,其实,我们可以看到,

对于指令在执行中,不论是否用到PC的值,PC都会按照既定逻辑,没一个cycle,自动增加4的,套用《非诚勿扰2》中的经典对白,即为:

你(指令执行的时候)用,

或者不用,

PC就在那里,

自动增4

所以,经过两个cycle的增4,就到了指令执行的时候,此时PC已经增加了8了,即使你指令执行的时候,没有用到PC的值,其也还是已经加了8了。而一般来说,大多数的指令,肯定也都是没有用到PC的,但是其实任何指令执行的那一时刻,也已经是PC=PC+8,而多数指令没有用到,所以很多人没有注意到这点罢了。

【总结】

ARM7的三级流水线,PC=PC+8,

ARM9的五级流水线,也是PC=PC+8,

根本的原因是,两者的流水线设计中,指令的Execute执行阶段,都是处于流水线的第三级。

所以使得PC=PC+8。

类似地,可以推导出:

假设,Execute阶段处于流水线中的第E阶段,每条指令是T个字节,那么

PC

= PC + N*T

= PC + (E – 1) * T

此处ARM7和ARM9:

Execute阶段都是第3阶段 E=3

每条指令是4个字节 T=4

所以:

PC

=PC + N* T

=PC + (3 -1 ) * 4

= PC + 8


 

3.5. AMR寄存器的别名 + APCS

此处简单介绍一下,ARM寄存器的别名,以及什么是APCS。

用文字解释之前,先看这个版本的解释,显得很直观,很好理解:


 

 3.8. ARM Application Procedure Call Standard (AAPCS)



 

3.5.1. ARM中的寄存器的别名

默认的情况下,这些寄存器只是叫做r0,r1,…,r14等,而APCS 对其起了不同的别名。

使用汇编器预处理器的功能,你可以定义 R0 等名字,但在你修改其他人写的代码的时候,最好还是学习使用 APCS 名字。

一般编程过程中,最好按照其约定,使用对应的名字,这样使得程序可读性更好。

关于不同寄存器所对应的名字,见下表:


 

 3.2. ARM寄存器的别名

寄存器名字

Reg# 

APCS 

意义

R0 

a1 

工作寄存器

R1 

a2 

R2 

a3 

R3 

a4 

R4 

v1 

必须保护

R5 

v2 

R6 

v3 

R7 

v4 

R8 

v5 

R9 

v6 

R10 

sl 

栈限制

R11 

fp 

桢指针

R12 

ip 

内部过程调用寄存器

R13 

sp 

栈指针

R14 

lr 

连接寄存器

R15 

pc 

程序计数器

更加详细一点,见下:

Predeclared register names
3

The following register names are predeclared:

  1. r0-r15 and R0-R15
  2. a1-a4 (argument, result, or scratch registers, synonyms for r0 to r3)
  3. v1-v8 (variable registers, r4 to r11)
  4. sb and SB (static base, r9)
  5. ip and IP (intra-procedure-call scratch register, r12)
  6. sp and SP (stack pointer, r13)
  7. lr and LR (link register, r14)
  8. pc and PC (program counter, r15).

Predeclared extension register names
4

The following extension register names are predeclared:

  1. d0-d31 and D0-D31(VFP double-precision registers)
  2. s0-s31 and S0-S31(VFP single-precision registers)

Predeclared coprocessor names
5

The following coprocessor names and coprocessor register names are predeclared:

  1. p0-p15 (coprocessors 0-15)
  2. c0-c15 (coprocessor registers 0-15).


 

3.5.2. 什么是APCS

APCS,ARM 过程调用标准(ARM Procedure Call Standard),提供了紧凑的编写例程的一种机制,定义的例程可以与其他例程交织在一起。最显著的一点是对这些例程来自哪里没有明确的限制。它们可以编译自 C、 Pascal、也可以是用汇编语言写成的。

APCS 定义了:


 

3.6. 为何C语言(的函数调用)需要堆栈,而汇编语言却不需要堆栈

之前看了很多关于uboot的分析,其中就有说要为C语言的运行,准备好堆栈。

而自己在Uboot的start.S汇编代码中,关于系统初始化,也看到有堆栈指针初始化这个动作。但是,从来只是看到有人说系统初始化要初始化堆栈,即正确给堆栈指针sp赋值,但是却从来没有看到有人解释,为何要初始化堆栈。所以,接下来的内容,就是经过一定的探究,试图来解释一下,为何要初始化堆栈,即:

为何C语言的函数调用要用到堆栈,而汇编却不需要初始化堆栈。

要明白这个问题,首先要了解堆栈的作用。

关于堆栈的作用,要详细讲解的话,要很长的篇幅,所以此处只是做简略介绍。

总的来说,堆栈的作用就是:保存现场/上下文,传递参数。


 

3.6.1. 保存现场/上下文

现场,意思就相当于案发现场,总有一些现场的情况,要记录下来的,否则被别人破坏掉之后,你就无法恢复现场了。而此处说的现场,就是指CPU运行的时候,用到了一些寄存器,比如r0,r1等等,对于这些寄存器的值,如果你不保存而直接跳转到子函数中去执行,那么很可能就被其破坏了,因为其函数执行也要用到这些寄存器。

因此,在函数调用之前,应该将这些寄存器等现场,暂时保持起来,等调用函数执行完毕返回后,再恢复现场。这样CPU就可以正确的继续执行了。

在计算机中,你常可以看到上下文这个词,对应的英文是context。那么:


 

3.6.1.1. 什么叫做上下文context

保存现场,也叫保存上下文。

上下文,英文叫做context,就是上面的文章,和下面的文章,即与你此刻,当前CPU运行有关系的内容,即那些你用到寄存器。所以,和上面的现场,是一个意思。

保存寄存器的值,一般用的是push指令,将对应的某些寄存器的值,一个个放到堆栈中,把对应的值压入到堆栈里面,即所谓的压栈

然后待被调用的子函数执行完毕的时候,再调用pop,把堆栈中的一个个的值,赋值给对应的那些你刚开始压栈时用到的寄存器,把对应的值从堆栈中弹出去,即所谓的出栈

其中保存的寄存器中,也包括lr的值(因为用bl指令进行跳转的话,那么之前的pc的值是存在lr中的),然后在子程序执行完毕的时候,再把堆栈中的lr的值pop出来,赋值给pc,这样就实现了子函数的正确的返回。


 

3.6.2. 传递参数

C语言进行函数调用的时候,常常会传递给被调用的函数一些参数,对于这些C语言级别的参数,被编译器翻译成汇编语言的时候,就要找个地方存放一下,并且让被调用的函数能够访问,否则就没发实现传递参数了。对于找个地方放一下,分两种情况。

一种情况是,本身传递的参数就很少,就可以通过寄存器传送参数。

因为在前面的保存现场的动作中,已经保存好了对应的寄存器的值,那么此时,这些寄存器就是空闲的,可以供我们使用的了,那就可以放参数,而参数少的情况下,就足够存放参数了,比如参数有2个,那么就用r0和r1存放即可。(关于参数1和参数2,具体哪个放在r0,哪个放在r1,就是和APCS中的”在函数调用之间传递/返回参数”相关了,APCS中会有详细的约定。感兴趣的自己去研究。)

但是如果参数太多,寄存器不够用,那么就得把多余的参数堆栈中了。

即,可以用堆栈来传递所有的或寄存器放不下的那些多余的参数。


 

3.6.3. 举例分析C语言函数调用是如何使用堆栈的

对于上面的解释的堆栈的作用显得有些抽象,此处再用例子来简单说明一下,就容易明白了:

用:

arm-inux-objdump –d u-boot > dump_u-boot.txt

可以得到dump_u-boot.txt文件。该文件就是中,包含了u-boot中的程序的可执行的汇编代码,其中我们可以看到C语言的函数的源代码,到底对应着那些汇编代码。

下面贴出两个函数的汇编代码,

一个是clock_init,另一个是与clock_init在同一C源文件中的,另外一个函数CopyCode2Ram



33d0091c <CopyCode2Ram>:
33d0091c: e92d4070 push { r4, r5, r6, lr}

33d00920: e1a06000 mov r6, r0
33d00924: e1a05001 mov r5, r1
33d00928: e1a04002 mov r4, r2
33d0092c: ebffffef bl 33d008f0 <bBootFrmNORFlash>
… …
33d00984: ebffff14 bl 33d005dc <nand_read_ll>
… …
33d009a8: e3a00000 mov r0, #0 ; 0x0
33d009ac: e8bd8070 pop { r4, r5, r6, pc}

33d009b0 <clock_init>:
33d009b0: e3a02313 mov r2, #1275068416 ; 0x4c000000
33d009b4: e3a03005 mov r3, #5 ; 0x5
33d009b8: e5823014 str r3, [r2, #20]
… …
33d009f8: e1a0f00e mov pc, lr


 

3.7. 关于为何不直接用mov指令,而非要用adr伪指令

在分析uboot的start.S中,看到一些指令,比如:

adr r0, _start

觉得好像可以直接用mov指令实现即可,为啥还要这么麻烦地,去用ldr去实现?

关于此处的代码,为何要用adr指令:

adr r0, _start

而不直接用mov指令直接将_start的值赋值给r0,类似于这样:

mov r0, _start

呢?

其原因主要是,

sub r0, pc, #172

这样的代码,所处理的值,都是相对于PC的偏移量来说的,这样的代码中,没有绝对的物理地址值,都是相对的值,利用产生位置无关代码。因为如果用mov指令:

mov r0, _start

那么就会被编译成这样的代码:

mov r0, 0x33d00000

如果用了上面这样的代码:

mov r0, 0x33d00000

那么,如果整个代码,即要执行的程序的指令,被移动到其他位置,那么

mov r0, 0x33d00000

这行指令,执行的功能,就是跳转到绝对的物理地址,而不是跳转到相对的_start的位置了,就不能实现我们想要的功能了,这样包含了绝对物理地址的代码,也就不是位置无关的代码了。

与此相对,这行指令:

sub r0, pc, #172

即使程序被移动到其他位置,那么该行指令还是可以跳转到相对PC往前172字节的地方,也还是我们想要的_start的位置,这样包含的都是相对的偏移位置的代码,就叫做位置无关代码。其优点就是不用担心你的代码被移动,即使程序的基地址变了,所有的代码的相对位置还是固定的,程序还是可以正常运行的。

关于,之所以不用上面的:

mov r0, 0x33d00000

类似的代码,除了上面说的,不是位置无关的代码之外,其还有个潜在的问题,那就是,关于mov指令的源操作数,此处即为0x33d00000,不一定是合法的mov 指令所允许的值,这也正是下面要详细解释的内容 3.8 节 “mov指令的操作数的取值范围到底是多少”

【总结】

之所以用adr而不用mov,主要是为了生成地址无关代码,以及由于不方便判断一个数,是否是有效的mov的操作数。


 

3.8. mov指令的操作数的取值范围到底是多少

关于mov指令操作数的取值范围,网上看到一些人说是0x00-0xFF,也有人说是其他的值的,但是经过一番求证,发现这些说法都不对。下面就是来详细解释,mov指令的操作数的取指范围,到底是多少。

在看了我说的,关于这行代码:

mov r0, 0x33d00000

的源操作数0x33d0000,可能是mov指令所不允许的,这句话后,可能有人会说,我知道,那是因为mov的操作数的值,不允许大于255,至少网上很多人的资料介绍中,都是这么说的。

对此,要说的是,你的回答是错误的。

关于mov操作数的真正的允许的取值范围,还真的不是那么容易就能搞懂的,下面就来详细解释解释。

总的来说,我是从ARM 汇编的mov操作立即数的疑问6

里面,才算清楚mov的取值范围,以及找了相应的datasheet,才最终看懂整个事情的来龙去脉的。

首先,mov的指令,是属于ARM指令集中,数据处理(Data Process)分类中的其中一个指令,

而数据处理指令的具体格式是:ARM Processor Instruction Set7


 

 3.9. 数据处理指令的指令格式


对于此格式,我们可以拿:

arm-linux-objdump –d u-boot > dump_u-boot.txt

中得到的汇编代码中关于:

ldr r0, =0x53000000

所对应的,真正的汇编代码:

33d00068: e3a00453 mov r0, #1392508928 ; 0x53000000

来分析,就容易看懂了:

mov r0, #1392508928

= mov r0, #0x53000000

的作用就是,把0x53000000移动到r0中去。

其对应的二进制指令是上面的:

0xe3a00453 = 1110 0011 1010 0000 0000 0100 0101 0011 b

下面对照mov指令的格式,来分析这些位所对应的含义:


 

 3.3. mov指令0xe3a00453的位域含义解析

31-28 

27-26 

25 

24-21 

20 

19-16 

15-12 

11-0 

Condition Field 

00 

I(Immediate Operand)

OpCode(Operation Code)

S(Set Condition Code)

Rn(1st Operand Register)

Rd(Destination Register)

Operand 2

1 = operand
2 is an immediate value

  

  

  

  

  

  

  

11-8:Rotate 

7-0:Imm 

1110 

00 

1 

1101 

0 

0000 

0000 

0100 

0101 0011 

  

  

表明是立即数

1101对应的是MOV指令

  

MOV指令做的事情是: Rd:= Op2,和Rn无关,所以忽略这个Rn

表示0000号寄存器,即r0

0100=4,含义参见注释1

0x53 


 

3.9. 汇编学习总结记录

对于我们之前分析的start.S中,涉及到很多的汇编的语句,其中,可以看出,很多包含了很多种不同的语法,使用惯例等,下面,就对此进行一些总结,借以实现一定的举一反三或者说触类旁通,这样,可以起到一定的借鉴功能,方便以后看其他类似汇编代码, 容易看懂汇编代码所要表达的含义。


 

3.9.1. 汇编中的标号=C中的标号

像前面汇编代码中,有很多的,以点开头,加上一个名字的形式的标号,比如:



reset:
/*
* set the cpu to SVC32 mode
*/
mrs r0,cpsr

中的reset,就是汇编中的标号,相对来说,比较容易理解,就相当于C语言的标号。

比如,C语言中定义一个标号ERR_NODEV:



ERR_NODEV: /* no device error */
… /* c code here */

然后对应在别处,使用goto去跳转到这个标号ERR_NODEV:



if (something)
goto ERR_NODEV ;


 

3.9.2. 汇编中的跳转指令=C中的goto

对应地,和上面的例子中的C语言中的编号和掉转到标号的goto类似,汇编中,对于定义了标号,那么也会有对应的指令,去跳转到对应的汇编中的标号。

这些跳转的指令,就是b指令,b是branch的缩写。

b指令的格式是:

b{ cond} label

简单说就是跳转到label处。

用和上面的例子相关的代码来举例:



.globl _start
_start: b reset

就是用b指令跳转到上面那个reset的标号。


 

3.9.3. 汇编中的.globl=C语言中的extern

对于上面例子中:



.globl _start

中的.global,就是声明_start为全局变量/标号,可以供其他源文件所访问。

即汇编器,在编译此汇编代码的时候,会将此变量记下来,知道其是个全局变量,遇到其他文件是用到此变量的的时候,知道是访问这个全局变量的。

因此,从功能上来说,就相当于C语言用extern去生命一个变量,以实现本文件外部访问此变量。


 

3.9.4. 汇编中用bl指令和mov pc,lr来实现子函数调用和返回

和b指令类似的,另外还有一个bl指令,语法是:

BL{ cond} label

其作用是,除了b指令跳转到label之外,在跳转之前,先把下一条指令地址存到lr寄存器中,以方便跳转到那边执行完毕后,将lr再赋值给pc,以实现函数返回,继续执行下面的指令的效果。

用下面这个start.S中的例子来说明:



bl cpu_init_crit
……
cpu_init_crit:
……
mov pc, lr

其中,就是先调用bl掉转到对应的标号cpu_init_crit,其实就是相当于一个函数了,

然后在cpu_init_crit部分,执行完毕后,最后调用 mov pc, lr,将lr中的值,赋给pc,即实现函数的返回原先 bl cpu_init_crit下面那条代码,继续执行函数。

上面的整个过程,用C语言表示的话,就相当于



……
cpu_init_crit();
……

void cpu_init_crit(void)
{
……
}

而关于C语言中,函数的跳转前后所要做的事情,都是C语言编译器帮我们实现好了,会将此C语言中的函数调用,转化为对应的汇编代码的。

其中,此处所说的,函数掉转前后所要做的事情,就是:

而如果你本身自己写汇编语言的话,那么这些函数跳转前后要做的事情,都是你程序员自己要关心,要实现的事情。


 

3.9.5. 汇编中的对应位置有存储值的标号 = C语言中的指针变量

像前文所解析的代码中类似于这样的:

LABEL1:.word Value2

比如:



_TEXT_BASE:
.word TEXT_BASE

所对应的含义是,有一个标号_TEXT_BASE

而该标号中对应的位置,所存放的是一个word的值,具体的数值是TEXT_BASE,此处的TEXT_BASE是在别处定义的一个宏,值是0x33D00000。

所以,即为:

有一个标号_TEXT_BASE,其对应的位置中,所存放的是一个word的值,值为

TEXT_BASE=0x33D00000

总的来说,此种用法的含义,如果用C语言来表示,其实更加容易理解:

int *_TEXT_BASE = TEXT_BASE = 0x33D00000

即:

int *_TEXT_BASE = 0x33D00000


 

3.9.6. 汇编中的ldr+标号,来实现C中的函数调用

接着上面的内容,继续解释,对于汇编中这样的代码:

第一种:



ldr pc, 标号1
……
标号1:.word 标号2
……
标号2:
……(具体要执行的代码)

或者是,

第二种:



ldr pc, 标号1
……
标号1:.word XXX(C语言中某个函数的函数名)

的意思就是,将地址为标号1中内容载入到pc中。

而地址为标号1中的内容,就是标号2。

TEXT_BASE=0x33D00000

所以上面第一种的意思:

就很容易看出来,就是把标号2这个地址值,给pc,即实现了跳转到标号2的位置执行代码,

就相当于调用一个函数,该函数名为标号2.

第二种的意思,和上面类似,是将C语言中某个函数的函数名,即某个地址值,给pc,实现调用C中对应的那个函数。

两种做法,其含义用C语言表达,其实很简单:

PC = *(标号1) = 标号2


 

 3.1. 汇编中的ldr加标号实现函数调用 示例

举个例子就是:

第一种:



……
ldr pc, _software_interrupt
……
_software_interrupt: .word software_interrupt
……
software_interrupt:
get_bad_stack
bad_save_user_regs
bl do_software_interrupt

就是实现了将标号1,_software_interrupt,对应的位置中的值,标号2,software_interrupt,给pc,即实现了将pc掉转到software_interrupt的位置,即实现了调用函数software_interrupt的效果。

第二种:



ldr pc, _start_armboot

_start_armboot: .word start_armboot

含义就是,将标号1,_start_armboot,所对应的位置中的值,start_armboot给pc,即实现了调用函数start_armboot的目的。

其中,start_armboot是C语言文件中某个C语言的函数。


 

3.9.7. 汇编中设置某个寄存器的值或给某个地址赋值

在汇编代码start.S中,看到不止一处, 类似于这样的代码:

形式1:



# define pWTCON 0x53000000
……
ldr r0, =pWTCON
mov r1, #0x0
str r1, [r0]

或者是,

形式2:



# define INTSUBMSK 0x4A00001C
……
ldr r1, =0x7fff
ldr r0, =INTSUBMSK
str r1, [r0]

其含义,都是将某个值,赋给某个地址,此处的地址,是用宏定义来定义的,对应着某个寄存器的地址。

其中,形式1是直接通过mov指令来将0这个值赋给r1寄存器,和形式2中的通过ldr伪指令来将0x3ff赋给r1寄存器,两者区别是,前者是因为已经确定所要赋的值0x0是mov的有效操作数,而后者对于0x3ff不确定是否是mov的有效操作数

所以才用ldr伪指令,让编译器来帮你自动判断:

 


 

参考书目

[1] 2010年6月 最新TQ2440光盘下载 (Linux内核,WinCE的eboot,uboot均有更新)1

[2] .globl,.word,.balignl的语法2

[3] label的解释3

[4] ldr的语法4

[5] ldr指令5

[6] ldr指令6

[7] .word的语法7

[8] ARM7体系结构8

[9] bootloader9

[10] S3C2440相关的软硬件资料10

[11] S3C2440的CPU的datasheet:s3c2440a_um_rev014_040712.pdf11

[12] 伪指令ldr语法和含义12

[13] ARM9 2410移植之ARM中断原理, 中断嵌套的误区,中断号的怎么来的13

[14] adr指令的语法和含义14

[15] ARM协处理器15

[16] ARM920T16

[17] CP15的各个寄存器的含义解释17

[18] Invalidate ICache and DCache18

[19] Invalidate TLB(s)19

[20] Control register20

[21] Domain access control21

[22] ARM920T的CPU的7种模式22

[23] ARM Linux Kernel Boot Requirements23

[24] 嵌入式系统之WATCHDOG(看门狗)概述24

[25] ARM流水线和program counter(PC)的增量25

[26] Predeclared register names26

[27] Predeclared extension register names27

[28] Predeclared coprocessor names28

[29] mov的操作数的取指范围29

[30] ARM Processor Instruction Set30

[31] ARM9流水线PC=PC+831

[32] Strange behaviour of ldr [pc, #value]32

转载请注明:胡椒小兄弟 » Uboot中start.S源码的指令级的详尽解析

喜欢 (0)or分享 (0)