Uboot中start.S源碼的指令級的詳盡解析

摘要

本文對Uboot中的Start.S的源碼的幾乎每一行,都進行了詳細的解析。


參考書目

插圖清單

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. 本文目標

本文的目標是,希望看完此文的讀者,可以達到:

  1. 微觀上,對此start.S的每一行,都有了基本的瞭解
  2. 宏觀上,對基於ARM核的S3C24X0的CPU的啓動過程,有更加清楚的概念

這樣的目的,是爲了讀者看完本文後,再去看其他類似的啓動相關的源碼,能明白需要做什麼事情,然後再看別的系統是如何實現相關的內容的,達到一定程度的觸類旁通。

總體說就是,要做哪些,爲何要這麼做,如何實現的,即英語中常說的:

  • do what
  • why do
  • how do

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

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均有更新)

4. 閱讀此文所要具有的前提知識

閱讀此文之前,你至少要對TQ2440的板子有個基本的瞭解,

以及要了解開發板初始化的大概要做的事情,比如設置輸入頻率,設置堆棧等等。

另外,至少要有一定的C語言的基礎,這樣更利於理解彙編代碼。

5. 聲明

由於水平有限,難免有誤,歡迎指正:admin (at) crifan.com

歡迎轉載,但請註明作者。

第 1 章 start.S詳解

摘要

下面將詳細解釋uboot中的start.S中的每一行代碼。詳細到,每個指令的語法和含義,都進行詳細講解,使得此文讀者可以真正搞懂具體的含義,即what。

以及對於一些相關的問題,深入探究爲何要這麼做,即why。

對於uboot的start.S,主要做的事情就是系統的各個方面的初始化。

從大的方面分,可以分成這幾個部分:

  • 設置CPU模式
  • 關閉看門狗
  • 關閉中斷
  • 設置堆棧sp指針
  • 清除bss段
  • 異常中斷處理

下面來對start.S進行詳細分析,看看每一個部分,是如何實現的。

1.1. 設置CPU模式

1.1.1. globl

/*
 *  armboot - Startup Code for ARM920 CPU-core
 *
 *  Copyright (c) 2001	Marius Gr鰃er <[email protected]>
 *  Copyright (c) 2002	Alex Z黳ke <[email protected]>
 *  Copyright (c) 2002	Gary Jennejohn <[email protected]>
 *
 * 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
        


globl是個關鍵字,對應含義爲:

http://re-eject.gbadev.org/files/GasARMRef.pdf

表 1.1. global的語法

Directive Description Syntax Example
.global Makes symbol visible to the linker .global symbol .global MyAsmFunc
.globl Same as .global .globl symbol .globl MyOtherAsmFunc

所以,意思很簡單,就是相當於C語言中的Extern,聲明此變量,並且告訴鏈接器此變量是全局的,外部可以訪問

所以,你可以看到

u-boot-1.1.6_20100601\opt\EmbedSky\u-boot-1.1.6\board\EmbedSky\u-boot.lds

中,有用到此變量:

ENTRY(_start)

即指定入口爲_start,而由下面的_start的含義可以得知,_start就是整個start.S的最開始,即整個uboot的代碼的開始。

1.1.2. _start

_start:	b       reset
        

_start後面加上一個冒號’:’,表示其是一個標號Label,類似於C語言goto後面的標號。

而同時,_start的值,也就是這個代碼的位置了,此處即爲代碼的最開始,相對的0的位置。

而此處最開始的相對的0位置,在程序開始運行的時候,如果是從NorFlash啓動,那麼其地址是0,

_stat=0

如果是重新relocate代碼之後,就是我們定義的值了,即,在

u-boot-1.1.6_20100601\opt\EmbedSky\u-boot-1.1.6\board\EmbedSky\config.mk

中的:

TEXT_BASE = 0x33D00000

表示是代碼段的基地址,即

_start=TEXT_BASE=0x33D00000

關於標號的語法解釋:

http://sourceware.org/binutils/docs-2.20/as/Labels.html#Labels

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.

而_start標號後面的:

b       reset

就是跳轉到對應的標號爲reset的位置。

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
        


ldr命令的語法爲:

http://wenku.baidu.com/view/f7cc280102020740be1e9bea.html

LDR指令的格式爲:

LDR{條件} 目的寄存器,<存儲器地址>

LDR指令用於從存儲器中將一個32位的字數據傳送到目的寄存器中。該指令通常用於從存儲器中讀取32位的字數據到通用寄存器,然後對數據進行處理。當程序計數器PC作爲目的寄存器時,指令從存儲器中讀取的字數據被當作目的地址,從而可以實現程序流程的跳轉。該指令在程序設計中比較常用,且尋址方式靈活多樣,請讀者認真掌握。

指令示例:

LDR R0,[R1] ;將存儲器地址爲R1的字數據讀入寄存器R0。

LDR R0,[R1,R2] ;將存儲器地址爲R1+R2的字數據讀入寄存器R0。

LDR R0,[R1,#8] ;將存儲器地址爲R1+8的字數據讀入寄存器R0。

LDR R0,[R1,R2]! ;將存儲器地址爲R1+R2的字數據讀入寄存器R0,並將新地址R1+R2寫入R1。

LDR R0,[R1,#8]! ;將存儲器地址爲R1+8的字數據讀入寄存器R0,並將新地址R1+8寫入R1。

LDR R0,[R1],R2 ;將存儲器地址爲R1的字數據讀入寄存器R0,並將新地址R1+R2寫入R1。

LDR R0,[R1,R2,LSL#2]! ;將存儲器地址爲R1+R2×4的字數據讀入寄存器R0,並將新地址R1+R2×4寫入R1。

LDRR0,[R1],R2,LSL#2 ;將存儲器地址爲R1的字數據讀入寄存器R0,並將新地址R1+R2×4寫入R1。”

http://www.pczpg.com/a/2010/0607/11062.html

ARM是RISC結構,數據從內存到CPU之間的移動只能通過L/S指令來完成,也就是ldr/str指令。

比如想把數據從內存中某處讀取到寄存器中,只能使用ldr

比如:

ldr r0, 0x12345678

就是把0x12345678這個地址中的值存放到r0中。

上面那些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
        


http://blogold.chinaunix.net/u3/115924/showart_2280163.html

.word .word expr {,expr}… 分配一段字內存單元,並用expr初始化字內存單元(32bit)

http://re-eject.gbadev.org/files/GasARMRef.pdf

表 1.2. .word的語法

Directive Description Syntax Example
.word Define word expr (32bit numbers) .word expr {, …} .word 144511, 0x11223

所以上面的含義,以_undefined_instruction爲例,就是,此處分配了一個word=32bit=4字節的地址空間,裏面存放的值是undefined_instruction。

而此處_undefined_instruction也就是該地址空間的地址了。用C語言來表達就是:

_undefined_instruction = &undefined_instruction

*_undefined_instruction = undefined_instruction

在後面的代碼,我們可以看到,undefined_instruction也是一個標號,即一個地址值,對應着就是在發生“未定義指令”的時候,系統所要去執行的代碼。

(其他幾個對應的“軟件中斷”,“預取指錯誤”,“數據錯誤”,“未定義”,“(普通)中斷”,“快速中斷”,也是同樣的做法,跳轉到對應的位置執行對應的代碼。)

所以:

ldr pc, 標號1
......
標號1:.word 標號2
......
標號2:
......(具體要執行的代碼)
                

的意思就是,將地址爲標號1中內容載入到pc,而地址爲標號1中的內容,正好裝的是標號2。

用C語言表達其實很簡單:

PC = *(標號1) = 標號2

對PC賦值,即是實現代碼跳轉,所以整個這段彙編代碼的意思就是:

跳轉到標號2的位置,執行對應的代碼。

1.1.5. .balignl

	.balignl 16,0xdeadbeef
        


balignl這個標號的語法及含義:

http://re-eject.gbadev.org/files/GasARMRef.pdf

表 1.3. balignl的語法

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 don't align (default=alignment ). .balignl {alignment} {, fill} {, max} .balignl

所以意思就是,接下來的代碼,都要16字節對齊,不足之處,用0xdeadbeef填充。

其中關於所要填充的內容0xdeadbeef,剛開始沒看懂是啥意思,後來終於搞懂了。

經過(等)多位網友提示和糾正,覺得這樣解釋會更加合理些:

此處0xdeadbeef本身沒有真正的意義,但是很明顯,字面上的意思是,(壞)死的牛肉。

雖然其本身沒有實際意義,但是其是在十六進制下,能表示出來的,爲數不多的,可讀的單詞之一了。

另外一個相對常見的是:0xbadc0de,意思是bad code,壞的代碼,注意其中的o是0,因爲十六進制中是沒有o的。

這些“單詞”,相對的作用是,使得讀代碼的人,以及在查看程序運行結果時,容易看懂,便於引起注意。

而關於自己之前,隨意杜撰出來的,希望起到搞笑作用,表示good beef(好的牛肉)的0xgoodbeef,實際上,在十六進制下,會出錯的,因爲十六進制下沒有o和 g這兩個字母。

1.1.6. _TEXT_BASE _armboot_start

/*
 *************************************************************************
 *
 * Startup Code (reset vector)
 *
 * do important init only if we don't 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
        


此處和上面的類似,_TEXT_BASE是一個標號地址,此地址中是一個word類型的變量,變量名是TEXT_BASE,此值見名知意,是text的base,即代碼的基地址,可以在

u-boot-1.1.6_20100601\opt\EmbedSky\u-boot-1.1.6\board\EmbedSky\config.mk

中找到其定義:

TEXT_BASE = 0x33D00000


同理,此含義可用C語言表示爲:

*(_armboot_start) = _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
        


關於_bss_start和_bss_end都只是兩個標號,對應着此處的地址。

而兩個地址裏面分別存放的值是__bss_start和_end,這兩個的值,根據註釋所說,是定義在開發板相關的鏈接腳本里面的,我們此處的開發板相關的鏈接腳本是:

u-boot-1.1.6_20100601\opt\EmbedSky\u-boot-1.1.6\board\EmbedSky\u-boot.lds

其中可以找到__bss_start和_end的定義:

	__bss_start = .;
	.bss : { *(.bss) }
	_end = .;
                

而關於_bss_start和_bss_end定義爲.glogl即全局變量,是因爲uboot的其他源碼中要用到這兩個變量,詳情請自己去搜索源碼。

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
        


關於FREE_RAM_END和FREE_RAM_SIZE,這裏只是兩個標號,之所以也是聲明爲全局變量,是因爲uboot的源碼中會用到這兩個變量。

但是這裏有點特別的是,這兩個變量,將在本源碼start.S中的後面要用到,而在後面用到這兩個變量之前,uboot的C源碼中,會先去修改這兩個值,具體的邏輯是:

本文件start.S中,後面有這兩句:

	ldr	pc, _start_armboot

_start_armboot:	.word start_armboot
                

意思很明顯,就是去調用start_armboot函數。

而start_armboot函數是在:

u-boot-1.1.6_20100601\opt\EmbedSky\u-boot-1.1.6\lib_arm\board.c

中:

init_fnc_t *init_sequence[] = {
	cpu_init,		/* basic cpu dependent setup */
......
	NULL,
};

void start_armboot (void)
{
	init_fnc_t **init_fnc_ptr;
......

	for (init_fnc_ptr = init_sequence; *init_fnc_ptr; ++init_fnc_ptr) {
		if ((*init_fnc_ptr)() != 0) {
			hang ();
		}
	}
......

}
                

即在start_armboot去調用了cpu_init。

cpu_init函數是在:

u-boot-1.1.6_20100601\opt\EmbedSky\u-boot-1.1.6\cpu\arm920t\cpu.c

中:

cpu_init源碼. 

int cpu_init (void)
{
    /*
     * setup up stacks if necessary
     */
#ifdef CONFIG_USE_IRQ
    IRQ_STACK_START = _armboot_start - CFG_MALLOC_LEN - CFG_GBL_DATA_SIZE - 4;
    FIQ_STACK_START = IRQ_STACK_START - CONFIG_STACKSIZE_IRQ;
    FREE_RAM_END = FIQ_STACK_START - CONFIG_STACKSIZE_FIQ - CONFIG_STACKSIZE;
    FREE_RAM_SIZE = FREE_RAM_END - PHYS_SDRAM_1;
#else    
    FREE_RAM_END = _armboot_start - CFG_MALLOC_LEN - CFG_GBL_DATA_SIZE - 4 - CONFIG_STACKSIZE;
    FREE_RAM_SIZE = FREE_RAM_END - PHYS_SDRAM_1;
#endif
    return 0;
}
                    

在cpu_init中,根據我們的一些定義,比如堆棧大小等等,去修改了IRQ_STACK_START ,FIQ_STACK_START ,FREE_RAM_END和FREE_RAM_SIZE的值。

至於爲何這麼修改,後面遇到的時候會具體再解釋。

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
        


同上,IRQ_STACK_START和FIQ_STACK_START,也是在cpu_init中用到了。

不過此處,是隻有當定義了宏CONFIG_USE_IRQ的時候,纔用到這兩個變量,其含義也很明顯,

只有用到了中斷IRQ,纔會用到中斷的堆棧,纔有中端堆棧的起始地址。

快速中斷FIQ,同理。

1.1.10. cpsr

/*
 * the actual reset code
 */

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


CPSR 是當前的程序狀態寄存器(Current Program Status Register),

而 SPSR 是保存的程序狀態寄存器(Saved Program Status Register)。

具體細節,可參考:

表 1.4. CPSR Bitfield

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 模式


MRS - Move From Status Register

MRS指令的語法爲:

四、程序狀態寄存器訪問指令

1、 MRS指令

MRS指令的格式爲:

MRS{條件} 通用寄存器,程序狀態寄存器(CPSR或SPSR)

MRS指令用於將程序狀態寄存器的內容傳送到通用寄存器中。該指令一般用在以下兩種情況:

Ⅰ.當需要改變程序狀態寄存器的內容時,可用MRS將程序狀態寄存器的內容讀入通用寄存器,修改後再寫回程序狀態寄存器。

Ⅱ.當在異常處理或進程切換時,需要保存程序狀態寄存器的值,可先用該指令讀出程序狀態寄存器的值,然後保存。

指令示例:

MRS R0,CPSR ;傳送CPSR的內容到R0

MRS R0,SPSR ;傳送SPSR的內容到R0”

所以,上述彙編代碼含義爲,將CPSR的值賦給R0寄存器。

1.1.11. bic

	bic	r0,r0,#0x1f
        


bic指令的語法是:

16、BIC指令

BIC指令的格式爲:

BIC{條件}{S} 目的寄存器,操作數1,操作數2

BIC指令用於清除操作數1的某些位,並把結果放置到目的寄存器中。操作數1應是一個寄存器,

操作數2可以是一個寄存器,被移位的寄存器,或一個立即數。操作數2爲32位的掩碼,如果在掩碼中設置了某一位,則清除這一位。未設置的掩碼位保持不變。

而0x1f=11111b

所以,此行代碼的含義就是,清除r0的bit[4:0]位。

1.1.12. orr

	orr	r0,r0,#0xd3
        


orr指令的語法是:

14、ORR指令

ORR指令的格式爲:

ORR{條件}{S} 目的寄存器,操作數1,操作數2

ORR指令用於在兩個操作數上進行邏輯或運算,並把結果放置到目的寄存器中。操作數1應是一個寄存器,操作數2可以是一個寄存器,被移位的寄存器,或一個立即數。該指令常用於設置操作數1的某些位。

指令示例:

ORR R0,R0,#3 ; 該指令設置R0的0、1位,其餘位保持不變。

所以此行彙編代碼的含義爲:

而0xd3=1101 0111[4:0]位。

將r0與0xd3算數或運算,然後將結果給r0,即把r0的bit[7:6]和bit[4]和bit[2:0]置爲1。

1.1.13. msr

	msr	cpsr,r0
        


MSR - Move to Status Register

msr的指令格式是:

四、程序狀態寄存器訪問指令

......

2、 MSR指令

MSR指令的格式爲:

MSR{條件} 程序狀態寄存器(CPSR或SPSR)_<域>,操作數

MSR指令用於將操作數的內容傳送到程序狀態寄存器的特定域中。其中,操作數可以爲通用寄存器或立即數。<域>用於設置程序狀態寄存器中需要操作的位,32位的程序狀態寄存器可分爲4個域:

位[31:24]爲條件標誌位域,用f表示;

位[23:16]爲狀態位域,用s表示;

位[15:8]爲擴展位域,用x表示;

位[7:0]爲控制位域,用c表示;

該指令通常用於恢復或改變程序狀態寄存器的內容,在使用時,一般要在MSR指令中指明將要操作的域。

指令示例:

MSR CPSR,R0 ;傳送R0的內容到CPSR

MSR SPSR,R0 ;傳送R0的內容到SPSR

MSR CPSR_c,R0 ;傳送R0的內容到SPSR,但僅僅修改CPSR中的控制位域

此行彙編代碼含義爲,將r0的值賦給CPSR。

所以,上面四行彙編代碼的含義就很清楚了。

先是把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
        


上面幾個宏定義所對應的地址,都可以在對應的datasheet中找到對應的定義:

其中,S3C2410和TQ2440開發板所用的CPU S3C2440,兩者在這部分的寄存器定義,都是一樣的,所以此處,採用CONFIG_S3C2410所對應的定義。

關於S3C2440相關的軟硬件資料,這個網站提供的很全面:

http://just4you.springnote.com/pages/1052612

其中有S3C2440的CPU的datasheet:

s3c2440a_um_rev014_040712.pdf

其中有對應的寄存器定義:

圖 1.3. pWTCON



圖 1.4. INTMOD



圖 1.5. INTMSK



圖 1.6. INTSUBMSK



圖 1.7. CLKDIVN



而關於每個寄存器的具體含義,見後面的分析。

1.2.2. ldr pWTCON

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


這裏的ldr和前面介紹的ldr指令不是一個意思。

這裏的ldr是僞指令ldr。

僞指令

僞指令,就是“僞”的指令,是針對“真”的指令而言的。

真的指令就是那些常見的指令,比如上面說的arm的ldr,bic,msr等等指令,是arm體系架構中真正存在的指令,你在arm彙編指令集中找得到對應的含義。

而僞指令是寫出來給彙編程序看的,彙編程序能看的僞指令具體表示的是啥意思,然後將其翻譯成真正的指令或者進行相應的處理。

僞指令ldr語法和含義:

http://blog.csdn.net/lihaoweiV/archive/2010/11/24/6033003.aspx

另外還有一個就是ldr僞指令,雖然ldr僞指令和ARM的ldr指令很像,但是作用不太一樣。ldr僞指令可以在立即數前加上=,以表示把一個地址寫到某寄存器中,比如:

ldr r0, =0x12345678

這樣,就把0x12345678這個地址寫到r0中了。所以,ldr僞指令和mov是比較相似的。

只不過mov指令後面的立即數是有限制的,這個立即數,能夠必須由一個8位的二進制數,即屬於0x00-0xFF內的某個值,經過偶數次右移後得到,這樣纔是合法數據,而ldr僞指令沒有這個限制。

那爲何ldr僞指令的操作數沒有限制呢,那是因爲其是僞指令,寫出來的僞指令,最終會被編譯器解釋成爲真正的,合法的指令的,一般都是對應的mov指令。

這樣的話,寫彙編程序的時候,使用MOV指令是比較麻煩的,因爲有些簡單的數據比較容易看出來,有些數據即不容易看出來是否是合法數據。所以,對此,ldr僞指令的出現,就是爲了解決這個問題的,你只管放心用ldr僞指令,不用關心操作數,而寫出的ldr僞指令,編譯器會幫你翻譯成對應的真正的彙編指令的。

而關於編譯器是如何將這些ldr僞指令翻譯成爲真正的彙編指令的,我的理解是,其自動會去算出來對應的操作數,是否是合法的mov 的操作數,如果是,就將該ldr僞指令翻譯成mov指令,否則就用別的方式處理,我所觀察到的,其中一種方式就是,單獨申請一個4字節的空間用於存放操作數,然後用ldr指令實現。

在uboot中,最後make完畢之後,會生產u-boot,

通過:

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

就可以把對應的彙編代碼輸出到該txt文件了,其中就能找到僞指令:

ldr     r0, =0x53000000

所對應的,真正的彙編代碼:

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

所以被翻譯成了mov指令。

而經過我的嘗試,故意將0x53000000改爲0x53000010,對應的生產的彙編代碼爲:

33d00068:	e59f0408 	ldr	r0, [pc, #1032]	; 33d00478 <fiq+0x58>
......
33d00478:	53000010 	.word	0x53000010
                    

其中可以看到,由於0x53000010不是有效的mov的操作數,沒法找到合適的0x00-0Xff去通過偶數次循環右移而得到,所以只能換成此處這種方式,即在另外申請一個word的空間用於存放這個值:

33d00478:	53000010 	.word	0x53000010

然後通過計算出相對當前PC的偏移,得到的地址,用ldr指令去除該地址中的值,即0x53000010,送給r0,比起mov指令,要複雜的多,也多消耗了一個word的空間。

對應地,其他的方式,個人理解,好像也可以通過MVN指令來實現,具體細節,有待進一步探索。

而這裏的:

ldr     r0, =pWTCON

意思就很清楚了,就是把宏pWTCON的值賦值給r0寄存器,即

r0=0x53000000

1.2.3. mov

	mov     r1, #0x0
        


mov指令語法:

1、 MOV指令

MOV指令的格式爲:

MOV{條件}{S} 目的寄存器,源操作數

MOV指令可完成從另一個寄存器、被移位的寄存器或將一個立即數加載到目的寄存器。其中S選項決定指令的操作是否影響CPSR中條件標誌位的值,當沒有S時指令不更新CPSR中條件標誌位的值。

指令示例:

MOV R1,R0 ;將寄存器R0的值傳送到寄存器R1

MOV PC,R14 ;將寄存器R14的值傳送到PC,常用於子程序返回

MOV R1,R0,LSL#3 ;將寄存器R0的值左移3位後傳送到R1

不過對於MOV指令多說一句,那就是,一般可以用類似於:

MOV R0,R0

的指令來實現NOP操作。

上面這句mov指令很簡單,就是把0x0賦值給r1,即

r1=0x0

1.2.4. str

	str     r1, [r0]
        


str指令語法:

4、STR指令

STR指令的格式爲:

STR{條件} 源寄存器,<存儲器地址>

STR指令用於從源寄存器中將一個32位的字數據傳送到存儲器中。該指令在程序設計中比較常用,且尋址方式靈活多樣,使用方式可參考指令LDR。

指令示例:

STR R0,[R1],#8 ;將R0中的字數據寫入以R1爲地址的存儲器中,並

將新地址R1+8寫入R1。

STR R0,[R1,#8] ;將R0中的字數據寫入以R1+8爲地址的存儲器中。

所以這句str的作用也很簡單,那就是將r1寄存器的值,傳送到地址值爲r0的(存儲器)內存中。

用C語言表示就是:

*r0 = r1

所以,上面幾行代碼意思也很清楚:

先是用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])
        


上面這幾行代碼,和前面的很類似,作用很簡單,就是將INTMSK寄存器設置爲0xffffffff,即,將所有的中端都mask了。

關於每一位的定義,其實可以不看的,反正此處都已mask了,不過還是貼出來,以備後用:

圖 1.9. INTMSK寄存器的位域



此處,關於mask這個詞,解釋一下。

mask這個單詞,是面具的意思,而中斷被mask了,就是中斷被掩蓋了,即雖然硬件上中斷髮生了,但是此處被屏蔽了,所以從效果上來說,就相當於中斷被禁止了,硬件上即使發生了中斷,CPU也不會去執行對應中斷服務程序ISR了。

關於中斷的內容的詳細解釋,推薦看這個,解釋的很通俗易懂:【轉】ARM9 2410移植之ARM中斷原理, 中斷嵌套的誤區,中斷號的怎麼來的

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
        


此處是將2410的INTSUBMSK設置爲0x3ff。

後經HateMath的提醒後,去查證,的確此處設置的0x3ff,是不嚴謹的。

因爲,根據2410的datasheet中關於INTSUBMSK的解釋,bit[10:0]共11位,雖然默認reset的每一位都是1,但是此處對應的mask值,應該是11位全爲1=0x7ff。

即寫成0x3ff,雖然是可以正常工作的,但是卻不夠嚴謹的。


此處CPU是是S3C2440,所以用到0x7fff這段代碼。

其意思也很容易看懂,就是將INTSUBMSK寄存器的值設置爲0x7fff。

先貼出對應每一位的含義:

圖 1.10. INTSUBMSK寄存器的位域



然後我們再來分析對應的0x7fff是啥含義。

其實也很簡單,意思就是:

0x7fff = bit[14:0]全是1 = 上表中的全部中斷都被屏蔽(mask)。

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


此處,關於CLKDIVN的具體含義,參見下表:

圖 1.11. INTSUBMSK寄存器的位域



而此處代碼被#if 0註釋掉了。

問:爲何要註釋掉,難道想要使用其默認的值,即HDIVN和PDIVN上電後,默認值Initial State,都是0,對應的含義爲,FCLK:HCLK:PCLK = 1:1:1 ???

答:不是,是因爲我們在其他地方會去初始化時鐘,去設置對應的CLKDIVN,詳情參考後面的代碼第 1.4.3 節 “bl clock_init”的部分。


此處是結束上面的#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
        


關於bl指令的含義:

b指令,是單純的跳轉指令,即CPU直接跳轉到某地址繼續執行。

而BL是Branch with Link,帶分支的跳轉,而Link指的是Link Register,鏈接寄存器R14,即lr,所以,bl的含義是,除了包含b指令的單純的跳轉功能,在跳轉之前,還把r15寄存器=PC=cpu地址,賦值給r14=lr,然後跳轉到對應位置,等要做的事情執行完畢之後,再用

mov pc, lr

使得cpu再跳轉回來,所以整個邏輯就是調用子程序的意思。

bl的語法爲:

2、 BL指令

BL指令的格式爲:

BL{條件} 目標地址

BL 是另一個跳轉指令,但跳轉之前,會在寄存器R14中保存PC的當前內容,因此,可以通過將R14 的內容重新加載到PC中,來返回到跳轉指令之後的那個指令處執行。該指令是實現子程序調用的一個基本但常用的手段。以下指令:

BL Label ;當程序無條件跳轉到標號Label處執行時,同時將當前的PC值保存到R14中

對於上面的代碼來說,意思就很清晰了,就是當沒有定義CONFIG_SKIP_LOWLEVEL_INIT的時候,就掉轉到cpu_init_crit的位置,而在後面的代碼cpu_init_crit中,你可以看到最後一行彙編代碼就是

mov pc, lr,

又將PC跳轉回來,所以整個的含義就是,調用子程序cpu_init_crit,等cpu_init_crit執行完畢,再返回此處繼續執行下面的代碼。

於此對應地b指令,就只是單純的掉轉到某處繼續執行,而不能再通過mov pc, lr跳轉回來了。

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


此句含義是,把地址爲_TEXT_BASE的內存中的內容給r0,即,將所有的中斷都mask了。

而查看前面的相關部分的代碼,即:

_TEXT_BASE:
	.word	TEXT_BASE
                

得知,地址爲_TEXT_BASE的內存中的內容,就是

u-boot-1.1.6_20100601\opt\EmbedSky\u-boot-1.1.6\board\EmbedSky\config.mk

中的:

TEXT_BASE = 0x33D00000

所以,此處即:

r0

= TEXT_BASE

= 0x33D00000

而關於sub指令:

SUB : 減法

(Subtraction)

SUB{條件}{S} <dest>, <op 1>, <op 2>

dest = op_1 - op_2

SUB 用操作數 one 減去操作數 two,把結果放置到目的寄存器中。操作數 1 是一個寄存器,操作數 2 可以是一個寄存器,被移位的寄存器,或一個立即值:

SUB R0, R1, R2 ; R0 = R1 - R2

SUB R0, R1, #256 ; R0 = R1 - 256

SUB R0, R2, R3,LSL#1 ; R0 = R2 - (R3 << 1)

減法可以在有符號和無符號數上進行。

所以對應含義爲:

r0 = r0 - #CFG_MALLOC_LEN

r0 = r0 - #CFG_GBL_DATA_SIZE

其中,對應的兩個宏的值是:

u-boot-1.1.6_20100601\opt\EmbedSky\u-boot-1.1.6\include\configs\EmbedSky.h

中:

#define CONFIG_64MB_Nand		0		//添加了對64MB Nand Flash支持

/*
 * Size of malloc() pool
 */
#define CFG_MALLOC_LEN				(CFG_ENV_SIZE + 128*1024)
#define CFG_GBL_DATA_SIZE			128	/* size in bytes reserved for initial data */

#if(CONFIG_64MB_Nand == 1)
#define CFG_ENV_SIZE			0xc000	/* Total Size of Environment Sector */
#else
#define CFG_ENV_SIZE			0x20000	/* Total Size of Environment Sector */
#endif
                

所以,從源碼中的宏定義中可以看出,

CFG_MALLOC_LEN

= (CFG_ENV_SIZE + 128*1024)

= 0x20000 + 128*1024

= 0x40000

= 256*1024

= 256KB

CFG_GBL_DATA_SIZE

= 128

所以,此三行的含義就是算出r0的值:

r0

= (r0 - #CFG_MALLOC_LEN) - #CFG_GBL_DATA_SIZE

= r0 - 0x40000 – 128

= r0 – 0x40080

= 33CBFF80

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


如果定義了CONFIG_USE_IRQ,即如果使用中斷的話,那麼再把r0的值減去IRQ和FIQ的堆棧的值,

而對應的宏的值也是在

u-boot-1.1.6_20100601\opt\EmbedSky\u-boot-1.1.6\include\configs\EmbedSky.h

中:

/*-------------------------------------------------------------------
 * Stack sizes
 *
 * The stack sizes are set up in start.S using the settings below
 */
#define CONFIG_STACKSIZE		(128*1024)	/* regular stack */
#ifdef CONFIG_USE_IRQ
#define CONFIG_STACKSIZE_IRQ		(4*1024)	/* IRQ stack */
#define CONFIG_STACKSIZE_FIQ		(4*1024)	/* FIQ stack */
#endif
                

所以,此時r0的值就是:

#ifdef CONFIG_USE_IRQ

r0

= r0 - #(CONFIG_STACKSIZE_IRQ+CONFIG_STACKSIZE_FIQ)

= r0 – (4*1024 + 4*1024)

= r0 – 8*1024

= 33CBFF80 – 8*1024

= 33CBDF80

#endif


最後,再減去終止異常所用到的堆棧大小,即12個字節。

現在r0的值爲:

#ifdef CONFIG_USE_IRQ

r0

= r0 – 12

= 33CBDF80 - 12

= 33CBDF74

#else

r0

= r0 – 12

= 33CBFF80 - 12

= 33CBFF74

#endif

然後將r0的值賦值給sp,即堆棧指針。

關於:

sp代表stack pointer,堆棧指針;

和後面要提到的ip寄存器:

ip代表instruction pointer,指令指針。

更多詳情參見下面的解釋。

關於ARM的寄存器的別名和相關的APCS,參見本文後面的內容:第 3.5 節 “AMR寄存器的別名 + APCS”

1.4.3. bl clock_init

	bl clock_init
        


在上面,經過計算,算出了堆棧的地址,然後賦值給了sp,此處,接着纔去調用函數clock_init去初始化時鐘。

其中此函數是在C文件:

u-boot-1.1.6_20100601\opt\EmbedSky\u-boot-1.1.6\board\EmbedSky\boot_init.c

中:

void clock_init(void)
{
...設置系統時鐘clock的相關代碼...
}
                

看到這裏,讓我想起,關於其他人的關於此start.S代碼解釋中說到的,此處是先去設置好堆棧,即初始化sp指針,然後纔去調用C語言的函數clock_init的。

而我們可以看到,前面那行代碼:

#ifndef CONFIG_SKIP_LOWLEVEL_INIT
	bl	cpu_init_crit
#endif
                

就不需要先設置好堆棧,再去進行函數調用。

其中cpu_init_crit對應的代碼也在start.S中(詳見後面對應部分的代碼),是用匯編實現的。

而對於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   */
        


adr指令的語法和含義:

http://blog.mcuol.com/User/cdkfGao/article/8057_1.htm

1、ADR僞指令--- 小範圍的地址讀取

ADR僞指令將基於PC相對偏移的地址值或基於寄存器相對偏移的地址值讀取到寄存器中。

在彙編編譯器編譯源程序時,ADR僞指令被編譯器替換成一條合適的指令。通常,編譯器用一條ADD指令或SUB指令來實現該ADR僞指令的功能,若不能用一條指令實現,則產生錯誤,編譯失敗。

ADR僞指令格式 :ADR{cond} register, expr

地址表達式expr的取值範圍:

當地址值是字節對齊時,其取指範圍爲: +255 ~ 255B;

當地址值是字對齊時,其取指範圍爲: -1020 ~ 1020B;

所以,上述:

adr r0, _start

的意思其實很簡單,就是將_start的地址賦值給r0.但是具體實現的方式就有點複雜了,對於用adr指令實現的話,說明_start這個地址,相對當前PC的偏移,應該是很小的,意思就是向前一段後者向後一段去找,肯定能找到_start這個標號地址的,此處,自己通過看代碼也可以看到_start,就是在當前指令的前面,距離很近,編譯後,對應彙編代碼,也可以猜得出,應該是上面所說的,用sub來實現,即當前PC減去某個值,得到_start的值,

參照前面介紹的內容,去:

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

然後打開dump_u-boot.txt,可以找到對應的彙編代碼,如下:

33d00000 <_start>:
33d00000:	ea000014 	b	33d00058 <reset>
。。。
33d000a4 <relocate>:
33d000a4:	e24f00ac 	sub	r0, pc, #172	; 0xac
                

可以看到,這個相對當前PC的距離是0xac=172,細心的讀者可以看到,那條指令的地址減去0xac,卻並不等於_start的值,即

33d000a4 - 33d00000 = 0xa4 != 0xac

而0xac – 0xa4 = 8,

那是因爲,由於ARM920T的五級流水線的緣故導致指令執行那一時刻的PC的值等於該條指令PC的值加上8,即

sub r0, pc, #172中的PC的值是

sub r0, pc, #172

指令地址:33d000a4,再加上8,即33d000a4+8 = 33d000ac,

所以,33d000ac – 0xac,纔等於我們看到的33d00000,纔是_start的地址。

這個由於流水線導致的PC的值和當前指令地址不同的現象,就是我們常說的,ARM中,PC=PC+8。

對於爲何是PC=PC+8,請參見後面的內容:第 3.4 節 “爲何ARM7中PC=PC+8”

對於此處爲何不直接用mov指令,卻使用adr指令,請參見後面內容:第 3.7 節 “關於爲何不直接用mov指令,而非要用adr僞指令”

對於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                  /* don't reloc during debug         */
	beq     clear_bss
        


這裏的_TEXT_BASE的含義,前面已經說過了,那就是:

_TEXT_BASE:
	.word	TEXT_BASE
                    

得知,地址爲_TEXT_BASE的內存中的內容,就是

u-boot-1.1.6_20100601\opt\EmbedSky\u-boot-1.1.6\board\EmbedSky\config.mk

中的:

TEXT_BASE = 0x33D00000

所以,此處就是

r1 = 0x33D00000


含義很簡單,就是比較r0和r1。而

r0 = 0x0

r1 = 0x33D00000

所以不相等


因此beq發現兩者不相等,就不會去跳轉到clear_bss,不會去執行對應的將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            */
        


這兩行代碼意思也很清楚,分別裝載_armboot_start和_bss_start地址中的值,賦值給r2和r3

而_armboot_start和_bss_start的值,前面都已經提到過了,就是:

.globl _armboot_start
_armboot_start:
	.word _start

.globl _bss_start
_bss_start:
	.word __bss_start
                
_TEXT_BASE:
	.word	TEXT_BASE
                    

而其中的_start,是我們uboot的代碼的最開始的位置,而__bss_start的值,是在

u-boot-1.1.6_20100601\opt\EmbedSky\u-boot-1.1.6\board\EmbedSky\u-boot.lds

中的:

SECTIONS
{
	. = 0x00000000;

	. = ALIGN(4);
	.text      :
...
	. = ALIGN(4);
	.rodata : { *(.rodata) }

	. = ALIGN(4);
	.data : { *(.data) }

...
	. = ALIGN(4);
	__bss_start = .;
	.bss : { *(.bss) }
	_end = .;
}
                

所以,可以看出,__bss_start的位置,是bss的start開始位置,同時也是text+rodata+data的結束位置,即代碼段,只讀數據和已初始化的可寫的數據的最末尾的位置。

其實我們也可以通過前面的方法,objdump出來,看到對應的值:

33d00048 <_bss_start>:
33d00048:	33d339d4 	.word	0x33d339d4
                

是0x33d339d4。

注意

【總結】

r2 = _start = 0x33d00000

r3 =__bss_start = 0x33d339d4


此處的意思就很清楚了,就是r2 = r3-r2,計算出

text + rodata + data

的大小,即整個需要載入的數據量是多少,用於下面的函數去拷貝這麼多的數據到對應的內存的位置。

這裏的實際的值是

r2

= r3 –r2

= 0x33d339d4 - 0x33d00000

= 0x000339d4

注意

【總結】

到此刻位置,假定是從Nor Flash啓動的話:

r0 = 0x0 = 我們代碼此刻所在的位置

r1 = 0x33D00000 = 我們想要把我們的代碼放到哪裏

r2 = 0x000339d4 = 對應的代碼的大小(此處的代碼 = text + rodata + data)

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


此處,代碼很簡單,只是註釋掉了原先的那些代碼,而單純的只是去調用CopyCode2Ram這個函數。

CopyCode2Ram函數,前面也提到過了,是在:

u-boot-1.1.6_20100601\opt\EmbedSky\u-boot-1.1.6\board\EmbedSky\boot_init.c

中:

int CopyCode2Ram(unsigned long start_addr, unsigned char *buf, int size)
{
	unsigned int *pdwDest;
	unsigned int *pdwSrc;
	int i;

	if (bBootFrmNORFlash())
	{
		pdwDest = (unsigned int *)buf;
		pdwSrc  = (unsigned int *)start_addr;
		/* 從 NOR Flash啓動 */
		for (i = 0; i < size / 4; i++)
		{
			pdwDest[i] = pdwSrc[i];
		}
		return 0;
	}
	else
	{
		/* 初始化NAND Flash */
		nand_init_ll();

		/* 從 NAND Flash啓動 */
		if (NF_ReadID() == 0x76 )
			nand_read_ll(buf, start_addr, (size + NAND_BLOCK_MASK)&~(NAND_BLOCK_MASK));
		else
			nand_read_ll_lp(buf, start_addr, (size + NAND_BLOCK_MASK_LP)&~(NAND_BLOCK_MASK_LP));
		return 0;
	}
}
                

可以看到,其有三個參數,start_addr,*buf和size,這三個參數,分別正好對應着我們剛纔所總結的r0,r1和r2.

這些寄存器和參數的對應關係,也是APSC中定義的:

實際參數

APCS 沒有定義記錄、數組、和類似的格局。這樣語言可以自由的定義如何進行這些活動。但是,如果你自己的實現實際上不符合 APCS 的精神,那麼將不允許來自你的編譯器的代碼與來自其他編譯器的代碼連接在一起。典型的,使用 C 語言的慣例。

  • 前4個整數實參(或者更少!)被裝載到 a1 - a4
  • 前 4 個浮點實參(或者更少!)被裝載到 f0 - f3
  • 其他任何實參(如果有的話)存儲在內存中,用進入函數時緊接在 sp 的值上面的字來指向。換句話說,其餘的參數被壓入棧頂。所以要想簡單。最好定義接受 4 個或更少的參數的函數

上面說的a1-a4,就是寄存器r0-r3。

而CopyCode2Ram函數的邏輯也很清晰,就是先去判斷是從Nor Flash啓動還是從Nand Flash啓動,然後決定從哪裏拷貝所需要的代碼到對應的目標地址中。

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


此處的_bss_start是:

.globl _bss_start
_bss_start:
	.word __bss_start
                


而_bss_end,是:

.globl _bss_end
_bss_end:
	.word _end
                


對應的,__bss_start和_end,都在前面提到過的那個鏈接腳本里面:

u-boot-1.1.6_20100601\opt\EmbedSky\u-boot-1.1.6\board\EmbedSky\u-boot.lds

中的:

	__bss_start = .;
	.bss : { *(.bss) }
	_end = .;
                

即bss段的起始地址和結束地址。

1.5.2. clear css loop

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


此段代碼含義也很清晰,那就是,

先將r2,即0x0,存到地址爲r0的內存中去,然後r0地址加上4,比較r0地址和r1地址,即比較當前地址是否到了bss段的結束位置,如果le,little or equal,小於或等於,那麼就跳到clbss_l,即接着這幾個步驟,直到地址超過了bss的_end位置,即實現了將整個bss段,都清零。

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
        


此處忽略已經註釋掉的代碼


最後的那兩行,意思也很簡單,那就是將地址爲_start_armboot中的內容,即

start_armboot,賦值給PC,即調用start_armboot函數。

至此,彙編語言的start.S的整個工作,就完成了。

而start_armboot函數,在C文件中:

u-boot-1.1.6_20100601\opt\EmbedSky\u-boot-1.1.6\board\EmbedSky\EmbedSky.c

中:

void start_armboot (void)
{
    ......
}
                

這就是傳說中的,調用第二層次,即C語言級別的初始化了,去初始化各個設備了。

其中包括了CPU,內存等,以及串口,正常初始化後,就可以從串口看到uboot的打印信息了。

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


關於mcr的來龍去脈:

http://apps.hi.baidu.com/share/detail/32319228

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 )指令訪問

一些要說明的內容,見下::

http://infocenter.arm.com/help/topic/com.arm.doc.ddi0151c/ARM920T_TRM1_S.pdf

you can only access CP15 registers with MRC and MCR instructions in a privileged mode. The assembler for these instructions is:

MCR/MRC{cond} P15,opcode_1,Rd,CRn,CRm,opcode_2

The CRn field of MRC and MCR

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).

Note:

Attempting to read from a nonreadable register, or to write to a nonwritable register causes unpredictable results.

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.

Using other values results in unpredictable behavior

CP15有很多個寄存器,分別叫做寄存器0(Register 0),到寄存器15(Register 15),

每個寄存器分別控制不同的功能,而且有的是隻讀,有的是隻寫,有的是可讀寫。

而且這些寄存器的含義,隨着版本ARM內核版本變化而不斷擴展,詳情請參考:Processor setup via co-processor 15 and about co-processors

其中,根據我們此處關心的內容,摘錄部分內容如下:

ARM 710

  • Register 7 - IDC flush (write only)

    Any data written to this location will cause the IDC (Instruction/Data cache) to be flushed.

......

StrongARM SA110

......

  • Register 7 - Cache control (write only)

    Any data written to this location will cause the selected cache to be flushed.

    The OPC_2 and CRm co-processor fields select which cache

    operation should occur:

    Function OPC_2 CRm Data

    Flush I + D %0000 %0111 -

    Flush I %0000 %0101 -

    Flush D %0000 %0110 -

    Flush D single %0001 %0110 Virtual address

    Clean D entry %0001 %1010 Virtual address

    Drain write buf. %0100 %1010 -

  • Register 8 - TLB operations (write only)

    Any data written to this location will cause the selected TLB flush operation.

    The OPC_2 and CRm co-processor fields select which cache

    operation should occur:

    Function OPC_2 CRm Data

    Flush I + D %0000 %0111 -

    Flush I %0000 %0101 -

    Flush D %0000 %0110 -

    Flush D single %0001 %0110 Virtual address”

而MCR的詳細的語法爲:

MCR指令

MCR指令將ARM處理器的寄存器中的數據傳送到協處理器寄存器中。如果協處理器不能成功地執行該操作,將產生未定義的指令異常中斷。

指令語法格式

MCR{<cond>} <p>,< opcode_1>,<Rd>,<CRn>,<CRm>{,<opcode_2>}

MCR{<cond>} p15,0,<Rd>,<CRn>,<CRm>{,<opcode_2>}

其中

  • <cond>

    指令執行的條件碼.當<cond>忽略時指令爲無條件執行。

  • <opcode_1>

    協處理器將執行的操作的操作碼。對於CP15協處理器來說,<opcode_1>永遠爲0b000,當<opcode_1>不爲0b000時,該指令操作結果不可預知。

  • <Rd>

    作爲源寄存器的ARM寄存器,其值將被傳送到協處理器寄存器中

  • <CRn>

    作爲目標寄存器的協處理器寄存器,其編號可能是C0,C1,…,C15。

<CRm>和<opcode_2>兩者組合決定對協處理器寄存器進行所需要的操作,如果沒有指定,則將爲<CRm>爲C0,opcode_2爲0

對照上面的那行代碼:

mcr	p15, 0, r0, c7, c7, 0	/* flush v3/v4 cache */

可以看出,其中

rd爲r0=0

CRn爲C7

CRm爲C7

對於這行代碼的作用,以此按照語法,來一點點解釋如下:

首先,mcr做的事情,其實很簡單,就是“ARM處理器的寄存器中的數據傳送到協處理器寄存器中”,

此處即是,將ARM的寄存器r0中的數據,此時r0=0,所以就是把0這個數據,傳送到協處理器CP15中。

而對應就是寫入到“<CRn>”這個“目標寄存器的協處理器寄存器”,此處CRn爲C7,即將0寫入到寄存器7(Register 7)中去。

而上面關於Register 7的含義中也說了,“Any data written to this location will cause the selected cache to be flushed”,即你往這個寄存器7中寫入任何數據,都會導致對應的緩存被清空。而到底那個緩存被清空呢,即我們這行指令

mcr	p15, 0, r0, c7, c7, 0

起了什麼作用呢

那是由“<CRm>和<opcode_2>兩者組合決定”的。

而此處CRm爲C7,opcode_2爲0,而對於C7和0的組合的作用,參見上面的那個表中Register 7中的Flash I+D那一行,

當opcode_2爲0,CRm爲0111=7,就是我們要找的,其作用是“Flush I + D”,即清空指令緩存I Cache和數據緩存D Cache。

根據該表,同理,如果是opcode_2=0,而CRm=0101b=5,那麼對應的就是去“Flush I”,即只清除指令緩存I Cache了。

而對應的指令也就是

mcr	p15, 0, r0, c7, c5, 0

了。


此註釋說此行代碼的作用是,清理v3或v4的緩存

其中v4,我們很好理解,因爲我們此處的CPU是ARM920T的核心,是屬於ARM V4的,而爲何又說,也可以清除v3的cache呢?

那是因爲,本身這些寄存器位域的定義,都是向下兼容的,參見上面引用的內容,也寫到了:

ARM 710

  • Register 7 - IDC flush (write only)

    Any data written to this location will cause the IDC (Instruction/Data cache) to be flushed.

即,對於ARM7的話,你寫同樣的這行代碼

mcr	p15, 0, r0, c7, c7, 0

也還是向register 7中寫入了數據0,這也同樣滿足了其所說的“Any data written to this location”,也會產生同樣的效果“cause the IDC (Instruction/Data cache) to be flushed”。


同理,可以看出此行是去操作寄存器8,而對應的各個參數爲:

rd爲r0=0

CRn爲C8

CRm爲C7

opcode_2爲0

對照寄存器8的表:

  • Register 8 - TLB operations (write only)

    Any data written to this location will cause the selected TLB flush operation.

    The OPC_2 and CRm co-processor fields select which cache

    operation should occur:

    Function OPC_2 CRm Data

    Flush I + D %0000 %0111 -

    Flush I %0000 %0101 -

    Flush D %0000 %0110 -

    Flush D single %0001 %0110 Virtual address”

其含義爲:

向寄存器8中寫入數據,會導致對應的TLB被清空。具體是哪個TLB,由opcode_2和CRm組合決定,

此處opcode_2爲0,CRm爲7=0111b,所以對應的作用是“Flush I + D”,即清空指令和數據的TLB。

提示

上述兩行代碼,其實都可以ARM的官方網站上面找到:

http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.ddi0184b/Chdcfejb.html

Function Rd Instruction
Invalidate ICache and DCache SBZ MCR p15,0,Rd,c7,c7,0

http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.ddi0184b/Chdifbjc.html

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
        


此處,對應的值爲:

rd爲r0=0

CRn爲C1

CRm爲C0

opcode_2爲0

即,此行代碼是將r0的值,即0,寫入到CP15的寄存器1中。

寄存器1的相關的定義爲:

http://www.heyrick.co.uk/assembler/coprocmnd.html

StrongARM SA110

  • Register 1 - Control (read/write)

    All values set to 0 at power-up.

    • Bit 0 - On-chip MMU turned off (0) or on (1)
    • Bit 1 - Address alignment fault disabled (0) or enabled (1)
    • Bit 2 - Data cache turned off (0) or on (1)
    • Bit 3 - Write buffer turned off (0) or on (1)
    • Bit 7 - Little-endian operation if 0, big-endian if 1
    • Bit 8 - System bit - controls the MMU permission system
    • Bit 9 - ROM bit - controls the MMU permission system
    • Bit 12 - Instruction cache turned off (0) or on (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
        


此處幾行代碼,註釋中寫的也很清楚了,就是去清楚對應的位和設置對應的位,具體位域的含義見下:

http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.ddi0184b/Chdifbjc.html

表 1.6. 控制寄存器1的位域含義

Register bits Name Function Value
31 iA bit Asynchronous clock select See Table 2.11
30 nF bit notFastBus select See Table 2.11
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 control
8 S bit System protection This bit modifies the MMU protection system. See Domain access control
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


表 1.7. 時鐘模式

Clocking mode iA nF
FastBus mode 0 0
Synchronous 0 1
Reserved 1 0
Asynchronous 1 1

http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.ddi0151c/I273867.html

Domain access control

表 1.8. 關於訪問控制位在域訪問控制寄存器中的含義

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

表 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)

表 1.9. 關於訪問允許(AP)位的含義

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. 清除bit[13]

    Base location of exception register(異常寄存器基地址)

    0 = Low address = 0x0000 0000

  2. 清除bit[9]和bit[8]

    此處不是很懂,待後續深入瞭解。

    目前的理解是:

    不論是Supervisor還是user,誰都不能訪問,否則就出現權限錯誤“Any access generates a permission fault”


此行作用是:

  1. 清除bit[7]

    使用little endian

  2. 清除bit[2-0]

    DCache disabled,關閉Dcache;

    Alignment Fault checking disabled,關閉地址對齊的錯誤檢查;

    MMU disabled,關閉MMU。


此行作用是:

  1. 設置bit[1]

    “Enable Data address alignment fault checking”打開數據地址對齊的錯誤檢查,即如果數據地址爲非法(奇數?)地址,就報錯。


此行作用是:

  1. 設置bit[12]

    開啓指令緩存I cache。


mcr指令,將剛纔設置的r0的值,再寫入到寄存器1中。

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


將lr的值給ip,即指令指針r12,此處之所以要保存一下lr是因爲此處是在子函數cpu_init_crit中,lr已經保存了待會用於返回主函數的地址,即上次調用時候的pc的值,而此處如果在子函數cpu_init_crit中繼續調用其他子函數lowlevel_init,而不保存lr的話,那麼調用完lowlevel_init返回來時候,就丟失了cpu_init_crit要返回的位置。

說白了就是,每次你要調用函數之前,你自己要確保是否已經正確保存了lr的值,要保證函數調用完畢後,也能正常返回。當然,如果你此處根本不需要返回,那麼就不用去保存lr的值了。


典型的子函數調用,通過將lr的值賦值給pc,實現函數調用完成後而返回的。


這裏,其是和前面的代碼:

#ifndef CONFIG_SKIP_LOWLEVEL_INIT
	bl	cpu_init_crit
#endif
                

是對應的。

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    
        


此處很簡單,只是一些宏定義而已。

後面用到的時候再解釋。


.macro和後面的.endm相對應,其語法是:

圖 1.12. macro的語法



所以,此處就相當於一個無參數的宏bad_save_user_regs,也就相當於一個函數了。


sp

= sp- S_FRAME_SIZE

= sp - 72


stmia的語法爲:

圖 1.13. LDM/STM的語法



其中,條件域的具體含義如下:

圖 1.14. 條件碼的含義



更具體的含義:

六、批量數據加載/存儲指令ARM微處理器所支持批量數據加載/存儲指令可以一次在一片連續的存儲器單元和多個寄存器之間傳送數據,批量加載指令用於將一片連續的存儲器中的數據傳送到多個寄存器,批量數據存儲指令則完成相反的操作。常用的加載存儲指令如下:

LDM(或STM)指令

LDM(或STM)指令的格式爲:

LDM(或STM){條件}{類型} 基址寄存器{!},寄存器列表{∧}

LDM(或STM)指令用於從由基址寄存器所指示的一片連續存儲器到寄存器列表所指示的多個寄存器之間傳送數據,該指令的常見用途是將多個寄存器的內容入棧或出棧。

其中,{類型}爲以下幾種情況:

IA 每次傳送後地址加1;

IB 每次傳送前地址加1;

DA 每次傳送後地址減1;

DB 每次傳送前地址減1;

FD 滿遞減堆棧;

ED 空遞減堆棧;

FA 滿遞增堆棧;

EA 空遞增堆棧;

{!}爲可選後綴,若選用該後綴,則當數據傳送完畢之後,將最後的地址寫入基址寄存器,否則基址寄存器的內容不改變。

基址寄存器不允許爲R15,寄存器列表可以爲R0~R15的任意組合。

{∧}爲可選後綴,當指令爲LDM且寄存器列表中包含R15,選用該後綴時表示:除了正常的數據傳送之外,還將SPSR複製到CPSR。同時,該後綴還表示傳入或傳出的是用戶模式下的寄存器,而不是當前模式下的寄存器。

指令示例:

STMFD R13!,{R0,R4-R12,LR} ;將寄存器列表中的寄存器(R0,R4到

R12,LR)存入堆棧。

LDMFD R13!,{R0,R4-R12,PC} ;將堆棧內容恢復到寄存器(R0,R4到

R12,LR)。

所以,此行的含義是,

將r0到r12的值,一個個地傳送到對應的地址上,基地址是sp的值,傳完一個,sp的值加4,一直到傳送完爲止。

此處,可見,前面那行代碼:

sp = sp - 72

就是爲此處傳送r0到r12,共13個寄存器,地址空間需要13*4=72個字節,

即前面sp減去72,就是爲了騰出空間,留此處將r0到r12的值,放到對應的位置的。


此處的含義就是,將_armboot_start中的值,參考前面內容,即爲_start,

而_start的值:

從 Nor Flash啓動時:_stat=0

relocate代碼之後爲:_start=TEXT_BASE=0x33D00000

此處是已經relocate代碼了,所以應該理解爲後者,即_start=0x33D00000

所以:

r2=0x33D00000

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
        


此處:

r2

= r2 - ( CONFIG_STACKSIZE+CFG_MALLOC_LEN)

= r2 – (128*1024 + 256*1024)

= 0x33D00000 - 384KB

= 0x33CA0000


此處:

r2

= r2 - (CFG_GBL_DATA_SIZE + 8)

= 0x33CA0000 – (128 + 8)

= 0x33C9FF78


分別將地址爲r2和r2+4的內容,即地址爲0x33C9FF780x33C9FF7C中的內容,load載入給r2和r3寄存器。


將sp的值,加上72,送給r0


前面的定義是:

#define S_SP		52

所以此處就是將sp的值,加上52,送給r5


將lr給r1


然後將r0到r3中的內容,存儲到地址爲r5-r5+12中的位置去。


將sp再賦值給r0


結束宏bad_save_user_regs

此處雖然每行代碼基本看懂了,但是到底此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

        


上面兩段代碼,基本上和前面很類似,雖然每一行都容易懂,但是整個兩個函數的意思,除了看其宏的名字irq_save_user_regs和irq_restore_user_regs,分別對應着中斷中,保存和恢復用戶模式寄存器,之外,其他的,個人目前還是沒有太多瞭解。


此處的get_bad_stack被後面undefined_instruction,software_interrupt等處調用,目前能理解的意思是,在出錯的時候,獲得對應的堆棧的值。


此處的含義很好理解,就是把地址爲IRQ_STACK_START中的值賦值給sp。

即獲得IRQ的堆棧的起始地址。

而對於IRQ_STACK_START,是前面就提到過的cpu_init源碼

而此處,就是用到了,前面已經在cpu_init()中重新計算正確的值了。

即算出IRQ堆棧的起始地址,其算法很簡單,就是:

	IRQ_STACK_START = _armboot_start - CFG_MALLOC_LEN - CFG_GBL_DATA_SIZE - 4;

即,先減去malloc預留的空間,和global data,即在

u-boot-1.1.6_20100601\opt\EmbedSky\u-boot-1.1.6\board\EmbedSky\board.c

中定義的全局變量:

DECLARE_GLOBAL_DATA_PTR;

而此宏對應的值在:

u-boot-1.1.6_20100601\opt\EmbedSky\u-boot-1.1.6\include\asm-arm\global_data.h

中:

#define DECLARE_GLOBAL_DATA_PTR     register volatile gd_t *gd asm ("r8")

即,用一個固定的寄存器r8來存放此結構體的指針。

提示

這也對應着編譯uboot的時候,你所看到的編譯參數-ffixed-r8

此gd_t的結構體,不同體系結構,用的不一樣。

而此處arm的平臺中,gd_t的定義在同一文件中:

typedef	struct	global_data {
	bd_t		*bd;
	unsigned long	flags;
	unsigned long	baudrate;
	unsigned long	have_console;	/* serial_init() was called */
	unsigned long	reloc_off;	/* Relocation Offset */
	unsigned long	env_addr;	/* Address  of Environment struct */
	unsigned long	env_valid;	/* Checksum of Environment valid? */
	unsigned long	fb_base;	/* base address of frame buffer */
#ifdef CONFIG_VFD
	unsigned char	vfd_type;	/* display type */
#endif
#if 0
	unsigned long	cpu_clk;	/* CPU clock in Hz!		*/
	unsigned long	bus_clk;
	unsigned long	ram_size;	/* RAM size */
	unsigned long	reset_status;	/* reset status register at boot */
#endif
	void		**jt;		/* jump table */
} gd_t;
                

而此全局變量gd_t *gd會被其他很多文件所引用,詳情自己去代碼中找。


此處和上面類似,把地址爲FIQ_STACK_START中的內容,給sp。

其中:

	FIQ_STACK_START = IRQ_STACK_START - CONFIG_STACKSIZE_IRQ;

即FIQ的堆棧起始地址,是IRQ堆棧起始地址減去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

        


如果發生未定義指令異常,CPU會掉轉到start.S開頭中對應的位置:

	ldr	pc, _undefined_instruction

即把地址爲_undefined_instruction中的內容給pc,即跳轉到此處執行對應的代碼。

其做的事情依次是:

獲得出錯時候的堆棧

保存用戶模式寄存器

跳轉到對應的函數:do_undefined_instruction

而do_undefined_instruction函數是在:

u-boot-1.1.6_20100601\opt\EmbedSky\u-boot-1.1.6\cpu\arm920t\interrupts.c

中:

void bad_mode (void)
{
	panic ("Resetting CPU ...\n");
	reset_cpu (0);
}

void do_undefined_instruction (struct pt_regs *pt_regs)
{
	printf ("undefined instruction\n");
	show_regs (pt_regs);
	bad_mode ();
}
                

可以看到,此處起始啥事沒錯,只是打印一下出錯時候的寄存器的值,然後跳轉到bad_mode中取reset CPU,直接重啓系統了。


以上幾個宏,和前面的do_undefined_instruction是類似的,就不多說了。

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
        


此處相當於一個叫做Launch的函數,做了也是類似的系統初始化的動作。

但是沒找到此函數在哪裏被調用的。具體不太清楚。

1.6.6. int_return

#ifdef CONFIG_USE_IRQ
	.align	5
irq:
/* add by 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
        


此處,做的事情,很容易看懂,就是中斷髮生後,掉轉到這裏,然後保存對應寄存器,然後跳轉到對應irq函數IRQ_Handle中去。

但是前面爲何sp爲何去減去4,原因不太懂。


關於IRQ_Handle,是在:

u-boot-1.1.6_20100601\opt\EmbedSky\u-boot-1.1.6\cpu\arm920t\s3c24x0\interrupts.c

中:

void IRQ_Handle(void)
{
	unsigned long oft = intregs->INTOFFSET;
	S3C24X0_GPIO * const gpio = S3C24X0_GetBase_GPIO();

//	printk("IRQ_Handle: %d\n", oft);

	//清中斷
	if( oft == 4 ) gpio->EINTPEND = 1<<7;	
	intregs->SRCPND = 1<<oft;
	intregs->INTPND	= intregs->INTPND;

	/* run the isr */
	isr_handle_array[oft]();
}
                

此處細節就不多解釋了,大體含義是,找到對應的中斷源,然後調用對應的之前已經註冊的中斷服務函數ISR。


此處也很簡單,就是發生了快速中斷FIQ的時候,保存IRQ的用戶模式寄存器,然後調用函數do_fiq,調用完畢後,再恢復IRQ的用戶模式寄存器。


do_fiq()是在:

u-boot-1.1.6_20100601\opt\EmbedSky\u-boot-1.1.6\cpu\arm920t\interrupts.c

中:

void do_fiq (struct pt_regs *pt_regs)
{
	printf ("fast interrupt request\n");
	show_regs (pt_regs);
	bad_mode ();
}
                

和前面提到過的do_undefined_instruction的一樣,就是打印寄存器信息,然後跳轉到bad_mode()去重啓CPU而已。


此處就是,如果沒有定義CONFIG_USE_IRQ,那麼就用這段代碼,可以看到,都只是直接調用do_irq和do_fiq,也沒做什麼實際工作。

第 2 章 start.S的總結

摘要

2.1. start.S各個部分的總結

其實關於start.S這個彙編文件,主要做的事情就是系統的各個方面的初始化。

關於每個部分,上面具體的代碼實現,也都一行行的解釋過了,此處不再贅述。

此處,只是簡單總結一下,其實現的方式,或者其他需要注意的地方。

  1. 設置CPU模式

    總的來說,就是將CPU設置爲SVC模式。

    至於爲何設置CPU是SVC模式,請參見後面章節的詳細解釋。

  2. 關閉看門狗

    就是去設置對應的寄存器,將看門狗關閉。

    至於爲何關閉看門狗,請參見後面章節的詳細解釋。

  3. 關閉中斷

    關閉中斷,也是去設置對應的寄存器,即可。

  4. 設置堆棧sp指針

    所謂的設置堆棧sp指針,這樣的句子,之前聽到N次了,但是說實話,一直不太理解,到底更深一層的含義是什麼。

    後來,看了更多的代碼,纔算有一點點了解。所謂的設置堆棧sp指針,就是設置堆棧,而所謂的設置堆棧,要做的事情,看起來很簡單,就只是一個很簡單的動作:讓sp等於某個地址值,即可。

    但是背後的邏輯是:

    首先你自己要搞懂當前系統是如何使用堆棧的,堆棧是向上生長的還是向下生長的。

    然後知道系統如何使用堆棧之後,給sp賦值之前,你要保證對應的地址空間,是專門分配好了,專門給堆棧用的,保證堆棧的大小相對合適,而不要太小以至於後期函數調用太多,導致堆棧溢出,或者堆棧太大,浪費存儲空間,等等。

    所有這些背後的邏輯,都是要經過一定的編程經驗,才更加容易理解其中的含義的。

    此處,也只是簡單說說,更多相關的內容,還是要靠每個人自己多實踐,慢慢的更加深入的理解。

  5. 清除bss段

    此處很簡單,就是將對應bss段,都設置爲,0,即清零。

    其對應的地址空間,就是那些未初始化的全局變量之類的地址。

  6. 異常中斷處理

    異常中斷處理,就是實現對應的常見的那些處理中斷的部分內容。

    說白了就是實現一個個中斷函數。uboot在初始化的時候,主要目的只是爲了初始化系統,及引導系統,所以,此處的中斷處理部分的代碼,往往相對比較簡單,不是很複雜。

2.2. Uboot中的內存的Layout

總結了start.S做的事情之後,另外想在此總結一下,uboot中,初始化部分的代碼執行後,對應的內存空間,都是如何規劃,什麼地方放置了什麼內容。此部分內容,雖然和start.S沒有直接的關係,但是start.S中,堆棧sp的計算等,也和這部分內容有關。

下面這部分的uboot的內存的layout,主要是根據:

  1. start.S中關於設置堆棧指針的部分的代碼
    	/* 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                        */
    
    #ifdef CONFIG_USE_IRQ
    	sub	r0, r0, #(CONFIG_STACKSIZE_IRQ+CONFIG_STACKSIZE_FIQ)
    #endif
    	sub	sp, r0, #12		/* leave 3 words for abort-stack    */
    
    	bl clock_init
                
  2. u-boot-1.1.6_20100601\opt\EmbedSky\u-boot-1.1.6\cpu\arm920t\cpu.c中的代碼
    int cpu_init (void)
    {
    	/*
    	 * setup up stacks if necessary
    	 */
    #ifdef CONFIG_USE_IRQ
    	IRQ_STACK_START = _armboot_start - CFG_MALLOC_LEN - CFG_GBL_DATA_SIZE - 4;
    	FIQ_STACK_START = IRQ_STACK_START - CONFIG_STACKSIZE_IRQ;
        FREE_RAM_END = FIQ_STACK_START - CONFIG_STACKSIZE_FIQ - CONFIG_STACKSIZE;
        FREE_RAM_SIZE = FREE_RAM_END - PHYS_SDRAM_1;
    #else    
        FREE_RAM_END = _armboot_start - CFG_MALLOC_LEN - CFG_GBL_DATA_SIZE - 4 - CONFIG_STACKSIZE;
        FREE_RAM_SIZE = FREE_RAM_END - PHYS_SDRAM_1;
    #endif
    	return 0;
    }
                
  3. u-boot-1.1.6_20100601\opt\EmbedSky\u-boot-1.1.6\board\EmbedSky\config.mk中的定義
    TEXT_BASE = 0x33D00000

分析而得出的。

uboot的內存的layout,用圖表表示就是:

圖 2.1. Uboot中的內存的Layout



第 3 章 相關知識點詳解

摘要

3.1. 如何查看C或彙編的源代碼所對應的真正的彙編代碼

首先解釋一下,由於彙編代碼中會存在一些僞指令等內容,所以,寫出來的彙編代碼,並不一定是真正可以執行的代碼,這些類似於僞指令的彙編代碼,經過彙編器,轉換或翻譯成真正的可以執行的彙編指令。所以,上面纔會有將“彙編源代碼”轉換爲“真正的彙編代碼”這一說。

然後,此處對於有些人不是很熟悉的,如何查看源代碼真正對應的彙編代碼。

此處,對於彙編代碼,有兩種:

  1. 一種是隻是進過編譯階段,生成了對應的彙編代碼
  2. 另外一種是,編譯後的彙編代碼,經過鏈接器鏈接後,對應的彙編代碼。

總的來說,兩者區別很小,後者主要是更新了外部函數的地址等等,對於彙編代碼本身,至少對於我們一般所去查看源代碼所對應的彙編來說,兩者可以視爲沒區別。

在查看源代碼所對應的真正的彙編代碼之前,先要做一些相關準備工作:

  1. 編譯uboot

    在Linux下,一般編譯uboot的方法是:

    1. make distclean

      去清除之前配置,編譯等生成的一些文件。

    2. make EmbedSky_config

      去配置我們的uboot

    3. make

      去執行編譯

  2. 查看源碼所對應的彙編代碼

    對於我們此處的uboot的start.S來說:

    1. 對於編譯所生成的彙編的查看方式是

      用交叉編譯器的dump工具去將彙編代碼都導出來:

      arm-linux-objdump –d cpu/arm920t/start.o > uboot_start.o_dump_result.txt

      這樣就把start.o中的彙編代碼導出到uboot_start.o_dump_result.txt中了。

      然後查看uboot_start.o_dump_result.txt,即可找到對應的彙編代碼。

      舉例來說,對於start.S中的彙編代碼:

      	/* 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                        */
      
      #ifdef CONFIG_USE_IRQ
      	sub	r0, r0, #(CONFIG_STACKSIZE_IRQ+CONFIG_STACKSIZE_FIQ)
      #endif
      	sub	sp, r0, #12		/* leave 3 words for abort-stack    */
      
      	bl clock_init
                          

      去uboot_start.o_dump_result.txt中,搜索stack_setup,即可找到對應部分的彙編代碼:

      00000090 <stack_setup>:
        90:	e51f0058 	ldr	r0, [pc, #-88]	; 40 <_TEXT_BASE>
        94:	e2400701 	sub	r0, r0, #262144	; 0x40000
        98:	e2400080 	sub	r0, r0, #128	; 0x80
        9c:	e240d00c 	sub	sp, r0, #12	; 0xc
        a0:	ebfffffe 	bl	0 <clock_init>
                          
    2. 對於鏈接所生成的彙編的查看方式是

      和上面方法一樣,即:

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

      然後打開該txt,找到stack_setup部分的代碼:

      33d00090 <stack_setup>:
      33d00090:	e51f0058 	ldr	r0, [pc, #-88]	; 33d00040 <_TEXT_BASE>
      33d00094:	e2400701 	sub	r0, r0, #262144	; 0x40000
      33d00098:	e2400080 	sub	r0, r0, #128	; 0x80
      33d0009c:	e240d00c 	sub	sp, r0, #12	; 0xc
      33d000a0:	eb000242 	bl	33d009b0 <clock_init>
                          

      兩者不一樣地方在於,我們uboot設置了text_base,即代碼段的基地址,上面編譯後的彙編代碼,經過鏈接後,更新了對應的基地址,所以看起來,所以代碼對應的地址,都變了,但是具體地址中的彙編代碼,除了個別調用函數的地址和跳轉到某個標號的地址之外,其他都還是一樣的。

對於C語言的源碼,也是同樣的方法,用對應的dump工具,去從該C語言的.o文件中,dump出來彙編代碼。

  注意

【總結】

不論是C語言還是彙編語言的源文件,想要查看其對應的生成的彙編代碼的話,方法很簡單,就是用dump工具,從對應的.o目標文件中,導出對應的彙編代碼,即可。

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

在看Uboot的start.S文件時候,發現其最開始初始化系統,做的第一件事情,就是將CPU設置爲SVC模式,但是S3C2440的CPU的core是ARM920T,其有7種模式,爲何非要設置爲SVC模式,而不是設置爲其他模式呢?對此,經過一些求證,得出如下原因:

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

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

表 3.1. ARM中CPU的模式

處理器模式 說明 備註
用戶(usr) 正常程序工作模式 此模式下程序不能夠訪問一些受操作系統保護的系統資源,應用程序也不能直接進行處理器模式的切換。
系統(sys) 用於支持操作系統的特權任務等 與用戶模式類似,但具有可以直接切換到其它模式等特權
快中斷(fiq) 支持高速數據傳輸及通道處理 FIQ異常響應時進入此模式
中斷(irq) 用於通用中斷處理 IRQ異常響應時進入此模式
管理(svc) 操作系統保護代碼 系統復位和軟件中斷響應時進入此模式
中止(abt) 用於支持虛擬內存和/或存儲器保護 在ARM7TDMI沒有大用處
未定義(und) 支持硬件協處理器的軟件仿真 未定義指令異常響應時進入此模式

另外,7種模式中,除用戶usr模式外,其它模式均爲特權模式。

對於爲何此處是svc模式,而不是其他某種格式,其原因,可以從兩方面來看:

  1. 我們先簡單的來分析一下那7種模式:

    1. 中止abt和未定義und模式

      首先可以排除的是,中止abt和未定義und模式,那都是不太正常的模式,此處程序是正常運行的,所以不應該設置CPU爲其中任何一種模式,所以可以排除。

    2. 快中斷fiq和中斷irq模式

      其次,對於快中斷fiq和中斷irq來說,此處uboot初始化的時候,也還沒啥中斷要處理和能夠處理,而且即使是註冊了終端服務程序後,能夠處理中斷,那麼這兩種模式,也是自動切換過去的,所以,此處也不應該設置爲其中任何一種模式。

    3. 用戶usr模式

      雖然從理論上來說,可以設置CPU爲用戶usr模式,但是由於此模式無法直接訪問很多的硬件資源,而uboot初始化,就必須要去訪問這類資源,所以此處可以排除,不能設置爲用戶usr模式。

    4. 系統sys模式 vs 管理svc模式

      首先,sys模式和usr模式相比,所用的寄存器組,都是一樣的,但是增加了一些訪問一些在usr模式下不能訪問的資源。

      而svc模式本身就屬於特權模式,本身就可以訪問那些受控資源,而且,比sys模式還多了些自己模式下的影子寄存器,所以,相對sys模式來說,可以訪問資源的能力相同,但是擁有更多的硬件資源。

      所以,從理論上來說,雖然可以設置爲sys和svc模式的任一種,但是從uboot方面考慮,其要做的事情是初始化系統相關硬件資源,需要獲取儘量多的權限,以方便操作硬件,初始化硬件。

    從uboot的目的是初始化硬件的角度來說,設置爲svc模式,更有利於其工作。

    因此,此處將CPU設置爲SVC模式。

  2. uboot作爲一個bootloader來說,最終目的是爲了啓動Linux的kernel,在做好準備工作(即初始化硬件,準備好kernel和rootfs等)跳轉到kernel之前,本身就要滿足一些條件,其中一個條件,就是要求CPU處於SVC模式的。

    所以,uboot在最初的初始化階段,就將CPU設置爲SVC模式,也是最合適的。

      提示

    關於滿足哪些條件,詳情請參考

    ARM Linux Kernel Boot Requirements

    或者Linux內核文檔:

    kernel_source_root\documentation\arm\booting

    中也是同樣的解釋:

    The CPU must be in SVC mode

    所以,uboot在最初的初始化階段,就將CPU設置爲SVC模式,也是最合適的。

綜上所述,uboot在初始化階段,就應該將CPU設置爲SVC模式。

3.3. 什麼是watchdog + 爲何在要系統初始化的時候關閉watchdog

關於Uboot初始化階段,在start.S中,爲何要去關閉watchdog,下面解釋具體的原因:

3.3.1. 什麼是watchdog

參考嵌入式系統之WATCHDOG(看門狗)概述

簡要摘錄如下:

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始終指向你要取的指令的地址

認識清楚了這個前提,後面的舉例講解,就容易懂了。

  1. 指令週期Cycle1
    1. 取指

      PC總是指向將要讀取的指令的地址(即我們常說的,指向下一條指令的地址),而當前PC=4,

      所以去取物理地址爲4對對應的指令

      ldr	pc, [pc, #20]

      其對應二進制代碼爲e59ff014。

      此處取指完之後,自動更新PC的值,即PC=PC+4(單個指令佔4字節,所以加4)=4+4=8

  2. 指令週期Cycle2
    1. 譯指

      翻譯指令e59ff014

    2. 同時再去取指

      PC總是指向將要讀取的指令的地址(即我們常說的,指向下一條指令的地址),而當前PC=8,

      所以去物理地址爲8所對應的指令“ldr pc, [pc, #20]” 其對應二進制代碼爲e59ff014。

      此處取指完之後,自動更新PC的值,即PC=PC+4=8+4=12=0xc

  3. 指令週期Cycle3
    1. 執行(指令)

      執行“e59ff014”,即

      ldr	pc, [pc, #20]

      所對錶達的含義,即PC

      = PC + 20

      = 12 + 20

      = 32

      = 0x20

      此處,只是計算出待會要賦值給PC的值是0x20,這個0x20還只是放在執行單元中內部的緩衝中。

    2. 譯指

      翻譯e59ff014

    3. 取指

      此步驟由於是和上面(1)中的執行同步做的,所以,未受到影響,繼續取指,而取指的那一時刻,PC爲上一Cycle更新後的值,即PC=0xc,所以是去取物理地址爲0xc所對應的指令

      ldr	pc, [pc, #20]

      對應二進制爲e59ff014

其實,分析到這裏,大家就可以看出:

在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,而多數指令沒有用到,所以很多人沒有注意到這點罷了。

  PC(execute)=PC(fetch)+ 8

對於PC=PC+8中的兩個PC,其實含義不完全一樣.其更準確的表達,應該是這樣:

PC(execute)=PC(fetch)+ 8

其中:

PC(fetch):當前正在執行的指令,就是之前取該指令時候的PC的值

PC(execute):當前指令執行的計算中,如果用到PC,則此時PC的值。

  不同階段的PC值的關係

對應地,在ARM7的三級流水線(取指,譯指,執行)和ARM9的五級流水線(取指,譯指,執行,存儲,寫回)中,可以這麼說:

PC, 總是指向當前正在被取指的指令的地址,

PC-4,總是指向當前正在被譯指的指令的地址,

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

  關於直接改變PC的值,會導致流水線清空的解釋

把PC的值直接賦值爲0x20。而PC值更改,直接導致流水線的清空,即導致下一個cycle中的,對應的流水線中的其他幾個步驟,包括接下來的同一個Cycle中的取指的工作被取消。在PC跳轉到0x20的位置之後,流水線重新計算,重新一步步地按照流水線的邏輯,去一點點執行。當然要保證當前指令的執行完成,即執行之後,還有兩個cycle,分別做的Memory和Write,會繼續執行完成。

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

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

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

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
            


此處就是我們所期望的,用push指令,保存了r4,r5,r以及lr。

用push去保存r4,r5,r6,那是因爲所謂的保存現場,以後後續函數返回時候再恢復現場,


上述用push去保存lr,那是因爲函數CopyCode2Ram裏面在此處調用了bBootFrmNORFlash

以及也調用了nand_read_ll:

33d00984:	ebffff14 	bl	33d005dc <nand_read_ll>

也用到了bl指令,會改變我們最開始進入clock_init時候的lr的值,所以我們要用push也暫時保存起來。


把0賦值給r0寄存器,這個就是我們所謂返回值的傳遞,是通過r0寄存器的。

此處的返回值是0,也對應着C語言的源碼中的“return 0”.


把之前push的值,給pop出來,還給對應的寄存器,其中最後一個是將開始push的lr的值,pop出來給賦給PC,因爲實現了函數的返回。


可以看到此處是該函數第一行

其中沒有我們所期望的push指令,沒有去將一些寄存器的值放到堆棧中。這是因爲,我們clock_init這部分的內容,所用到的r2,r3等寄存器,和前面調用clock_init之前所用到的寄存器r0,沒有衝突,所以此處可以不用push去保存這類寄存器的值,不過有個寄存器要注意,那就是r14,即lr,其是在前面調用clock_init的時候,用的是bl指令,所以會自動把跳轉時候的pc的值賦值給lr,所以也不需要push指令去將PC的值保存到堆棧中。


而此處是clock_init的代碼的最後一行

就是我們常見的mov pc, lr,把lr的值,即之前保存的函數調用時候的PC值,賦值給現在的PC,這樣就實現了函數的正確的返回,即返回到了函數調用時候下一個指令的位置。

這樣CPU就可以繼續執行原先函數內剩下那部分的代碼了。

  對於使用哪個寄存器來傳遞返回值

當然你也可以用其他暫時空閒沒有用到的寄存器來傳遞返回值,但是這些處理方式,本身是根據ARM的APCS的寄存器的使用的約定而設計的,你最好不要隨便改變使用方式,最好還是按照其約定的來處理,這樣程序更加符合規範。

3.7. 關於爲何不直接用mov指令,而非要用adr僞指令

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

adr r0, _start

覺得好像可以直接用mov指令實現即可,爲啥還要這麼麻煩地,去用ldr去實現?

關於此處的代碼,爲何要用adr指令:

adr r0, _start
  adr r0, _start會被翻譯爲真正的彙編指令

其被編譯器編譯後,會被翻譯成:

sub	r0, pc, #172

而不直接用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操作立即數的疑問

裏面,纔算清楚mov的取值範圍,以及找了相應的datasheet,才最終看懂整個事情的來龍去脈的。

首先,mov的指令,是屬於ARM指令集中,數據處理(Data Process)分類中的其中一個指令,

而數據處理指令的具體格式是:ARM Processor Instruction Set

圖 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

  注意

上述datasheet中寫到:

5.4.3 Immediate operand rotates

The immediate operand rotate field is a 4 bit unsigned integer which specifies a shift operation on the 8 bit immediate value. This value is zero extended to 32 bits, and then subject to a rotate right by twice the value in the rotate field. This enables many common constants to be generated, for example all powers of 2

意思是,對於bit[11:8]的值,是個4位,無符號的整型,其指定了bit[7:0]的8bit立即數值的位移操作。具體如何指定呢,那就是將bit[7:0]的值,循環右移2x bit[11:8]位。

對於我們的例子,就是,將bit[7:0]的值0x53,循環右移 2xbit[11:8]= 2 x 4 = 8位,

而0x53循環右移8位,就得到了0x53000000,就是我們要mov值,mov到目的寄存器rd,此處爲r0中。

而上面英文最後一句說的是,通過將bit[7:0]的值,循環右移 2xbit[11:8]的方式,就可以產生出很多個數值了,即mov的操作數中,其中符合可以通過0x00-0xFF循環右移偶數位而產生的數值,都是合法的mov的操作數,而這樣的數,其實是很多的。

  總結mov取值範圍

所以,mov指令的操作數的真正的取指範圍,即不是0-0xFF(0-255),也不是隻有2的倍數,而是:

只要該數,可以通過0x00-0xFF中某個數,循環右移偶數位而產生,就是合法的mov的操作數,否則就是非法的mov的操作數。

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 ; 
        
  【總結】

彙編中的標號 = C語言中的標號Label

3.9.2. 彙編中的跳轉指令=C中的goto

對應地,和上面的例子中的C語言中的編號和掉轉到標號的goto類似,彙編中,對於定義了標號,那麼也會有對應的指令,去跳轉到對應的彙編中的標號。

這些跳轉的指令,就是b指令,b是branch的縮寫。

b指令的格式是:

b{cond} label

簡單說就是跳轉到label處。

用和上面的例子相關的代碼來舉例:

.globl _start
_start:	b       reset
        

就是用b指令跳轉到上面那個reset的標號。

  【總結】

彙編中的b跳轉指令 = C語言中的goto

3.9.3. 彙編中的.globl=C語言中的extern

對於上面例子中:

.globl _start
        

中的.global,就是聲明_start爲全局變量/標號,可以供其他源文件所訪問。

即彙編器,在編譯此彙編代碼的時候,會將此變量記下來,知道其是個全局變量,遇到其他文件是用到此變量的的時候,知道是訪問這個全局變量的。

因此,從功能上來說,就相當於C語言用extern去生命一個變量,以實現本文件外部訪問此變量。

  【總結】

彙編中的.globl或.global = 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語言中的函數調用,轉化爲對應的彙編代碼的。

其中,此處所說的,函數掉轉前後所要做的事情,就是:

  • 函數跳轉前

    要將當前指令的下一條指令的地址,保存到lr寄存器中

  • 函數調用完畢後

    將之前保存的lr的值給pc,實現函數跳轉回來。繼續執行下一條指令。

而如果你本身自己寫彙編語言的話,那麼這些函數跳轉前後要做的事情,都是你程序員自己要關心,要實現的事情。

  總結匯編中的:bl + mov pc,lr

彙編中bl + mov pc,lr = 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
  C語言中如何引用匯編中的標號

不過,對於這樣的類似於C語言中的指針的彙編中的標號,在C語言中調用到的話,卻是這樣引用的:

/* for the following variables, see start.S */
extern ulong _armboot_start;	/* code start */
extern ulong _bss_start;	/* code + data end == BSS start */
......
	IRQ_STACK_START = _armboot_start - CFG_MALLOC_LEN - CFG_GBL_DATA_SIZE - 4;
......
            

而不是我原以爲的,直接當做指針來引用該變量的方式:

	*IRQ_STACK_START = *_armboot_start - CFG_MALLOC_LEN - CFG_GBL_DATA_SIZE - 4;
            

其中,對應的彙編中的代碼爲:

.globl _armboot_start
_armboot_start:
	.word _start
            

所以,針對這點,還是需要注意一下的。至少以後如果自己寫代碼的時候,在C語言中引用匯編中的global的標號的時候,知道是如何引用該變量的。

  【總結】

彙編中類似這樣的代碼:

label1: .word value2

就相當於C語言中的:

int *label1 = value2

但是在C語言中引用該標號/變量的時候,卻是直接拿來用的,就像這樣:

label1 = other_value

其中label1就是個int型的變量。

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語言的函數。


  總結匯編中實現函數調用的方式

彙編中,實現函數調用的效果,有如下兩種方法:

  1. 方法1
    ldr pc, 標號1
    ......
    標號1:.word 標號2
    ......
    標號2:
    	......(具體要執行的代碼)
                        
  2. 方法2
    ldr pc, 標號1
    ......
    標號1:.word XXX(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的有效操作數

  警告

如果不是,則該指令無效,編譯的時候,也無法通過編譯,會出現類似於這樣的錯誤::

    start.S: Assembler messages:
    start.S:149: Error: invalid constant -- 'mov r1,#0xFFEFDFFF'
    make[1]: *** [start.o] 錯誤 1
    make: *** [cpu/arm920t/start.o] 錯誤 2
            

所以才用ldr僞指令,讓編譯器來幫你自動判斷:

  1. 如果該操作數是mov的有效操作數,那麼ldr僞指令就會被翻譯成對應的mov指令

    例 3.2. 

    舉例說明:

    彙編代碼:

    # define pWTCON		0x53000000
    ......
    	ldr     r0, =pWTCON
                        

    被翻譯後的真正的彙編代碼:

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

  2. 如果該操作數不是mov的有效操作數,那麼ldr僞指令就會被翻譯成ldr指令

    例 3.3. 

    舉例說明:

    彙編代碼:

    	ldr	r1, =0x7fff
                        

    被翻譯後的真正的彙編代碼:

    33d00080:	e59f13f8 	ldr	r1, [pc, #1016]	; 33d00480 <fiq+0x60>
    ......
    33d00480:	00007fff 	.word	0x00007fff
                        

    即把ldr僞指令翻譯成真正的ldr指令,並且另外分配了一個word的地址空間用於存放該數值,然後用ldr指令將對應地址中的值載入,賦值給r1寄存器。


  總結匯編中給某個地址賦值的方法

彙編中,一個常用的,用來給某個地址賦值的方法,類似如下形式:

#define 宏的名字  寄存器地址
......
	ldr	r1, =要賦的值
	ldr	r0, =宏的名字
	str	r1, [r0]
            



轉載自:http://www.crifan.com/files/doc/docbook/uboot_starts_analysis/release/html/uboot_starts_analysis.html

發佈了23 篇原創文章 · 獲贊 9 · 訪問量 12萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章