摘要
本文對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的位域含義解析
本文的目標是,希望看完此文的讀者,可以達到:
- 微觀上,對此start.S的每一行,都有了基本的瞭解
- 宏觀上,對基於ARM核的S3C24X0的CPU的啓動過程,有更加清楚的概念
這樣的目的,是爲了讀者看完本文後,再去看其他類似的啓動相關的源碼,能明白需要做什麼事情,然後再看別的系統是如何實現相關的內容的,達到一定程度的觸類旁通。
總體說就是,要做哪些,爲何要這麼做,如何實現的,即英語中常說的:
- do what
- why do
- how do
此三方面都清楚理解了,那麼也才能算真正懂了。
所用代碼來自TQ2440官網,天嵌的bbs上下載下來的uboot中的源碼:
u-boot-1.1.6_20100601\opt\EmbedSky\u-boot-1.1.6\cpu\arm920t\start.S
閱讀此文之前,你至少要對TQ2440的板子有個基本的瞭解,
以及要了解開發板初始化的大概要做的事情,比如設置輸入頻率,設置堆棧等等。
另外,至少要有一定的C語言的基礎,這樣更利於理解彙編代碼。
摘要
下面將詳細解釋uboot中的start.S中的每一行代碼。詳細到,每個指令的語法和含義,都進行詳細講解,使得此文讀者可以真正搞懂具體的含義,即what。
以及對於一些相關的問題,深入探究爲何要這麼做,即why。
對於uboot的start.S,主要做的事情就是系統的各個方面的初始化。
從大的方面分,可以分成這幾個部分:
- 設置CPU模式
- 關閉看門狗
- 關閉中斷
- 設置堆棧sp指針
- 清除bss段
- 異常中斷處理
下面來對start.S進行詳細分析,看看每一個部分,是如何實現的。
/* * 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的語法
所以,意思很簡單,就是相當於C語言中的Extern,聲明此變量,並且告訴鏈接器此變量是全局的,外部可以訪問 所以,你可以看到
中,有用到此變量: ENTRY(_start) 即指定入口爲_start,而由下面的_start的含義可以得知,_start就是整個start.S的最開始,即整個uboot的代碼的開始。 |
_start: b reset
_start後面加上一個冒號’:’,表示其是一個標號Label,類似於C語言goto後面的標號。 而同時,_start的值,也就是這個代碼的位置了,此處即爲代碼的最開始,相對的0的位置。 而此處最開始的相對的0位置,在程序開始運行的時候,如果是從NorFlash啓動,那麼其地址是0, _stat=0 如果是重新relocate代碼之後,就是我們定義的值了,即,在
中的: TEXT_BASE = 0x33D00000 表示是代碼段的基地址,即 _start=TEXT_BASE=0x33D00000 關於標號的語法解釋:
而_start標號後面的: b reset 就是跳轉到對應的標號爲reset的位置。 |
ldr pc, _undefined_instruction ldr pc, _software_interrupt ldr pc, _prefetch_abort ldr pc, _data_abort ldr pc, _not_used ldr pc, _irq ldr pc, _fiq
ldr命令的語法爲:
上面那些ldr的作用,以第一個_undefined_instruction爲例,就是將地址爲_undefined_instruction中的一個word的值,賦值給pc。 |
_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://re-eject.gbadev.org/files/GasARMRef.pdf
所以上面的含義,以_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的位置,執行對應的代碼。 |
.balignl 16,0xdeadbeef
balignl這個標號的語法及含義:
所以意思就是,接下來的代碼,都要16字節對齊,不足之處,用0xdeadbeef填充。 其中關於所要填充的內容0xdeadbeef,剛開始沒看懂是啥意思,後來終於搞懂了。 經過( 此處0xdeadbeef本身沒有真正的意義,但是很明顯,字面上的意思是,(壞)死的牛肉。 雖然其本身沒有實際意義,但是其是在十六進制下,能表示出來的,爲數不多的,可讀的單詞之一了。 另外一個相對常見的是:0xbadc0de,意思是bad code,壞的代碼,注意其中的o是0,因爲十六進制中是沒有o的。 這些“單詞”,相對的作用是,使得讀代碼的人,以及在查看程序運行結果時,容易看懂,便於引起注意。 而關於自己之前,隨意杜撰出來的,希望起到搞笑作用,表示good beef(好的牛肉)的0xgoodbeef,實際上,在十六進制下,會出錯的,因爲十六進制下沒有o和 g這兩個字母。 |
/* ************************************************************************* * * 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
/* * 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,這兩個的值,根據註釋所說,是定義在開發板相關的鏈接腳本里面的,我們此處的開發板相關的鏈接腳本是:
其中可以找到__bss_start和_end的定義: __bss_start = .; .bss : { *(.bss) } _end = .; 而關於_bss_start和_bss_end定義爲.glogl即全局變量,是因爲uboot的其他源碼中要用到這兩個變量,詳情請自己去搜索源碼。 |
.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函數是在:
中: 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函數是在:
中: 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的值。 至於爲何這麼修改,後面遇到的時候會具體再解釋。 |
#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
/* * the actual reset code */ reset: /* * set the cpu to SVC32 mode */ mrs r0,cpsr
CPSR 是當前的程序狀態寄存器(Current Program Status Register), 而 SPSR 是保存的程序狀態寄存器(Saved Program Status Register)。 具體細節,可參考: |
|
MRS - Move From Status Register MRS指令的語法爲:
所以,上述彙編代碼含義爲,將CPSR的值賦給R0寄存器。 |
orr r0,r0,#0xd3
msr cpsr,r0
MSR - Move to Status Register msr的指令格式是:
此行彙編代碼含義爲,將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模式而不是設置爲其他模式”
/* 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: 其中有對應的寄存器定義: 而關於每個寄存器的具體含義,見後面的分析。 |
#if defined(CONFIG_S3C2400) || defined(CONFIG_S3C2410) || defined(CONFIG_S3C2440) ldr r0, =pWTCON
這裏的ldr和前面介紹的ldr指令不是一個意思。 這裏的ldr是僞指令ldr。
而這裏的: ldr r0, =pWTCON 意思就很清楚了,就是把宏pWTCON的值賦值給r0寄存器,即 r0=0x53000000 |
mov r1, #0x0
mov指令語法:
不過對於MOV指令多說一句,那就是,一般可以用類似於: MOV R0,R0 的指令來實現NOP操作。 上面這句mov指令很簡單,就是把0x0賦值給r1,即 r1=0x0 |
所以,上面幾行代碼意思也很清楚:
先是用r0寄存器存pWTCON的值,然後r1=0,再將r1中的0寫入到pWTCON中,其實就是
pWTCON = 0;
而pWTCON寄存器的具體含義是什麼呢?下面就來了解其詳細含義:
注意到bit[0]是Reset Enable/Disable,而設置爲0的話,那就是關閉Watchdog的reset了,所以其他位的配置選項,就更不需要看了。
我們只需要瞭解,在此處禁止了看門狗WatchDog(的復位功能),即可。
關於看門狗的作用,以及爲何要在系統初始化的時候關閉看門狗,請參見本文檔後面的章節:第 3.3 節 “什麼是watchdog + 爲何在要系統初始化的時候關閉watchdog”
/* * mask all IRQs by setting all bits in the INTMR - default */ mov r1, #0xffffffff ldr r0, =INTMSK str r1, [r0])
上面這幾行代碼,和前面的很類似,作用很簡單,就是將INTMSK寄存器設置爲0xffffffff,即,將所有的中端都mask了。 關於每一位的定義,其實可以不看的,反正此處都已mask了,不過還是貼出來,以備後用: 此處,關於mask這個詞,解釋一下。 mask這個單詞,是面具的意思,而中斷被mask了,就是中斷被掩蓋了,即雖然硬件上中斷髮生了,但是此處被屏蔽了,所以從效果上來說,就相當於中斷被禁止了,硬件上即使發生了中斷,CPU也不會去執行對應中斷服務程序ISR了。 關於中斷的內容的詳細解釋,推薦看這個,解釋的很通俗易懂:【轉】ARM9 2410移植之ARM中斷原理, 中斷嵌套的誤區,中斷號的怎麼來的 |
# 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。 先貼出對應每一位的含義: 然後我們再來分析對應的0x7fff是啥含義。 其實也很簡單,意思就是: 0x7fff = bit[14:0]全是1 = 上表中的全部中斷都被屏蔽(mask)。 |
#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的具體含義,參見下表: 而此處代碼被#if 0註釋掉了。 問:爲何要註釋掉,難道想要使用其默認的值,即HDIVN和PDIVN上電後,默認值Initial State,都是0,對應的含義爲,FCLK:HCLK:PCLK = 1:1:1 ??? 答:不是,是因爲我們在其他地方會去初始化時鐘,去設置對應的CLKDIVN,詳情參考後面的代碼第 1.4.3 節 “bl clock_init”的部分。 |
|
此處是結束上面的#ifdef |
/* * 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的語法爲:
對於上面的代碼來說,意思就很清晰了,就是當沒有定義CONFIG_SKIP_LOWLEVEL_INIT的時候,就掉轉到cpu_init_crit的位置,而在後面的代碼cpu_init_crit中,你可以看到最後一行彙編代碼就是 mov pc, lr, 又將PC跳轉回來,所以整個的含義就是,調用子程序cpu_init_crit,等cpu_init_crit執行完畢,再返回此處繼續執行下面的代碼。 於此對應地b指令,就只是單純的掉轉到某處繼續執行,而不能再通過mov pc, lr跳轉回來了。 |
/* 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的內存中的內容,就是
中的: TEXT_BASE = 0x33D00000 所以,此處即: r0 = TEXT_BASE = 0x33D00000 而關於sub指令:
所以對應含義爲: r0 = r0 - #CFG_MALLOC_LEN r0 = r0 - #CFG_GBL_DATA_SIZE 其中,對應的兩個宏的值是:
中: #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 |
#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的堆棧的值, 而對應的宏的值也是在
中: /*------------------------------------------------------------------- * 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” |
bl clock_init
在上面,經過計算,算出了堆棧的地址,然後賦值給了sp,此處,接着纔去調用函數clock_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語言(的函數調用)需要堆棧,而彙編語言卻不需要堆棧” |
#ifndef CONFIG_SKIP_RELOCATE_UBOOT relocate: /* relocate U-Boot to RAM */ adr r0, _start /* r0 <- current position of code */
adr指令的語法和含義:
所以,上述: 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
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的內存中的內容,就是
中的: TEXT_BASE = 0x33D00000 所以,此處就是 r1 = 0x33D00000 |
|
含義很簡單,就是比較r0和r1。而 r0 = 0x0 r1 = 0x33D00000 所以不相等 |
|
因此beq發現兩者不相等,就不會去跳轉到clear_bss,不會去執行對應的將bss段清零的動作了。 |
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的值,是在
中的: 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 = r3-r2,計算出 text + rodata + data 的大小,即整個需要載入的數據量是多少,用於下面的函數去拷貝這麼多的數據到對應的內存的位置。 這裏的實際的值是 r2 = r3 –r2 = 0x33d339d4 - 0x33d00000 = 0x000339d4
|
#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函數,前面也提到過了,是在:
中: 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中定義的:
上面說的a1-a4,就是寄存器r0-r3。 而CopyCode2Ram函數的邏輯也很清晰,就是先去判斷是從Nor Flash啓動還是從Nand Flash啓動,然後決定從哪裏拷貝所需要的代碼到對應的目標地址中。 |
clear_bss: ldr r0, _bss_start /* find start of bss segment */ ldr r1, _bss_end /* stop here */ mov r2, #0x00000000 /* clear */
#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文件中:
中: void start_armboot (void) { ...... } 這就是傳說中的,調用第二層次,即C語言級別的初始化了,去初始化各個設備了。 其中包括了CPU,內存等,以及串口,正常初始化後,就可以從串口看到uboot的打印信息了。 |
/* ************************************************************************* * * 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的來龍去脈:
一些要說明的內容,見下::
CP15有很多個寄存器,分別叫做寄存器0(Register 0),到寄存器15(Register 15), 每個寄存器分別控制不同的功能,而且有的是隻讀,有的是隻寫,有的是可讀寫。 而且這些寄存器的含義,隨着版本ARM內核版本變化而不斷擴展,詳情請參考:Processor setup via co-processor 15 and about co-processors 其中,根據我們此處關心的內容,摘錄部分內容如下:
而MCR的詳細的語法爲:
對照上面的那行代碼: 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呢? 那是因爲,本身這些寄存器位域的定義,都是向下兼容的,參見上面引用的內容,也寫到了:
即,對於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的表:
其含義爲: 向寄存器8中寫入數據,會導致對應的TLB被清空。具體是哪個TLB,由opcode_2和CRm組合決定, 此處opcode_2爲0,CRm爲7=0111b,所以對應的作用是“Flush I + D”,即清空指令和數據的TLB。
|
/* * 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的相關的定義爲:
所以,對應內容就是,向bit[CRm]中寫入opcode_2,即向bit[0]寫入0,對應的作用爲“On-chip MMU turned off”,即關閉MMU。 |
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
此處幾行代碼,註釋中寫的也很清楚了,就是去清楚對應的位和設置對應的位,具體位域的含義見下:
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
此行作用是:
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
此行作用是:
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
此行作用是:
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
此行作用是:
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
mcr指令,將剛纔設置的r0的值,再寫入到寄存器1中。 |
/* * 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 是對應的。 |
摘要
/* ************************************************************************* * * 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相對應,其語法是: 所以,此處就相當於一個無參數的宏bad_save_user_regs,也就相當於一個函數了。 |
|
即 sp = sp- S_FRAME_SIZE = sp - 72 |
|
stmia的語法爲: 其中,條件域的具體含義如下: 更具體的含義:
所以,此行的含義是, 將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 |
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的內容,即地址爲0x33C9FF78和0x33C9FF7C中的內容,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函數是做什麼的,還是不太清楚,有待以後慢慢深入理解。
.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,即在
中定義的全局變量: DECLARE_GLOBAL_DATA_PTR; 而此宏對應的值在:
中: #define DECLARE_GLOBAL_DATA_PTR register volatile gd_t *gd asm ("r8") 即,用一個固定的寄存器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堆棧的大小。 |
/* * 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函數是在:
中: 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是類似的,就不多說了。 |
@ 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
#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,是在:
中: 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()是在:
中: 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,也沒做什麼實際工作。 |
摘要
其實關於start.S這個彙編文件,主要做的事情就是系統的各個方面的初始化。
關於每個部分,上面具體的代碼實現,也都一行行的解釋過了,此處不再贅述。
此處,只是簡單總結一下,其實現的方式,或者其他需要注意的地方。
- 設置CPU模式
總的來說,就是將CPU設置爲SVC模式。
至於爲何設置CPU是SVC模式,請參見後面章節的詳細解釋。
- 關閉看門狗
就是去設置對應的寄存器,將看門狗關閉。
至於爲何關閉看門狗,請參見後面章節的詳細解釋。
- 關閉中斷
關閉中斷,也是去設置對應的寄存器,即可。
- 設置堆棧sp指針
所謂的設置堆棧sp指針,這樣的句子,之前聽到N次了,但是說實話,一直不太理解,到底更深一層的含義是什麼。
後來,看了更多的代碼,纔算有一點點了解。所謂的設置堆棧sp指針,就是設置堆棧,而所謂的設置堆棧,要做的事情,看起來很簡單,就只是一個很簡單的動作:讓sp等於某個地址值,即可。
但是背後的邏輯是:
首先你自己要搞懂當前系統是如何使用堆棧的,堆棧是向上生長的還是向下生長的。
然後知道系統如何使用堆棧之後,給sp賦值之前,你要保證對應的地址空間,是專門分配好了,專門給堆棧用的,保證堆棧的大小相對合適,而不要太小以至於後期函數調用太多,導致堆棧溢出,或者堆棧太大,浪費存儲空間,等等。
所有這些背後的邏輯,都是要經過一定的編程經驗,才更加容易理解其中的含義的。
此處,也只是簡單說說,更多相關的內容,還是要靠每個人自己多實踐,慢慢的更加深入的理解。
- 清除bss段
此處很簡單,就是將對應bss段,都設置爲,0,即清零。
其對應的地址空間,就是那些未初始化的全局變量之類的地址。
- 異常中斷處理
異常中斷處理,就是實現對應的常見的那些處理中斷的部分內容。
說白了就是實現一個個中斷函數。uboot在初始化的時候,主要目的只是爲了初始化系統,及引導系統,所以,此處的中斷處理部分的代碼,往往相對比較簡單,不是很複雜。
總結了start.S做的事情之後,另外想在此總結一下,uboot中,初始化部分的代碼執行後,對應的內存空間,都是如何規劃,什麼地方放置了什麼內容。此部分內容,雖然和start.S沒有直接的關係,但是start.S中,堆棧sp的計算等,也和這部分內容有關。
下面這部分的uboot的內存的layout,主要是根據:
- 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
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; }
u-boot-1.1.6_20100601\opt\EmbedSky\u-boot-1.1.6\board\EmbedSky\config.mk
中的定義TEXT_BASE = 0x33D00000
分析而得出的。
uboot的內存的layout,用圖表表示就是:
目錄
摘要
首先解釋一下,由於彙編代碼中會存在一些僞指令等內容,所以,寫出來的彙編代碼,並不一定是真正可以執行的代碼,這些類似於僞指令的彙編代碼,經過彙編器,轉換或翻譯成真正的可以執行的彙編指令。所以,上面纔會有將“彙編源代碼”轉換爲“真正的彙編代碼”這一說。
然後,此處對於有些人不是很熟悉的,如何查看源代碼真正對應的彙編代碼。
此處,對於彙編代碼,有兩種:
- 一種是隻是進過編譯階段,生成了對應的彙編代碼
- 另外一種是,編譯後的彙編代碼,經過鏈接器鏈接後,對應的彙編代碼。
總的來說,兩者區別很小,後者主要是更新了外部函數的地址等等,對於彙編代碼本身,至少對於我們一般所去查看源代碼所對應的彙編來說,兩者可以視爲沒區別。
在查看源代碼所對應的真正的彙編代碼之前,先要做一些相關準備工作:
- 編譯uboot
在Linux下,一般編譯uboot的方法是:
-
make distclean
去清除之前配置,編譯等生成的一些文件。
-
make EmbedSky_config
去配置我們的uboot
-
make
去執行編譯
-
- 查看源碼所對應的彙編代碼
對於我們此處的uboot的start.S來說:
- 對於編譯所生成的彙編的查看方式是
用交叉編譯器的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>
- 對於鏈接所生成的彙編的查看方式是
和上面方法一樣,即:
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目標文件中,導出對應的彙編代碼,即可。 |
在看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模式,而不是其他某種格式,其原因,可以從兩方面來看:
-
我們先簡單的來分析一下那7種模式:
- 中止abt和未定義und模式
首先可以排除的是,中止abt和未定義und模式,那都是不太正常的模式,此處程序是正常運行的,所以不應該設置CPU爲其中任何一種模式,所以可以排除。
- 快中斷fiq和中斷irq模式
其次,對於快中斷fiq和中斷irq來說,此處uboot初始化的時候,也還沒啥中斷要處理和能夠處理,而且即使是註冊了終端服務程序後,能夠處理中斷,那麼這兩種模式,也是自動切換過去的,所以,此處也不應該設置爲其中任何一種模式。
- 用戶usr模式
雖然從理論上來說,可以設置CPU爲用戶usr模式,但是由於此模式無法直接訪問很多的硬件資源,而uboot初始化,就必須要去訪問這類資源,所以此處可以排除,不能設置爲用戶usr模式。
- 系統sys模式 vs 管理svc模式
首先,sys模式和usr模式相比,所用的寄存器組,都是一樣的,但是增加了一些訪問一些在usr模式下不能訪問的資源。
而svc模式本身就屬於特權模式,本身就可以訪問那些受控資源,而且,比sys模式還多了些自己模式下的影子寄存器,所以,相對sys模式來說,可以訪問資源的能力相同,但是擁有更多的硬件資源。
所以,從理論上來說,雖然可以設置爲sys和svc模式的任一種,但是從uboot方面考慮,其要做的事情是初始化系統相關硬件資源,需要獲取儘量多的權限,以方便操作硬件,初始化硬件。
從uboot的目的是初始化硬件的角度來說,設置爲svc模式,更有利於其工作。
因此,此處將CPU設置爲SVC模式。
- 中止abt和未定義und模式
-
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模式。
關於Uboot初始化階段,在start.S中,爲何要去關閉watchdog,下面解釋具體的原因:
簡要摘錄如下:
watchdog一般是一個硬件模塊,其作用是,在嵌入式操作系統中,很多應用情況是系統長期運行且無人看守,所以難免或者怕萬一出現系統死機,那就杯具了,這時,watchdog就會自動幫你重啓系統。
那麼其是如何實現此功能的呢?那麼就要簡單解釋一下其實現原理了。
watchdog硬件的邏輯就是,其硬件上有個記錄超時功能,然後要求用戶需要每隔一段時間(此時間可以根據自己需求而配置)去對其進行一定操作,比如往裏面寫一些固定的值,俗稱“喂狗”,那麼我發現超時了,即過了這麼長時間你還不給偶餵食,那麼偶就認爲你係統是死機了,出問題了,偶就幫你重啓系統。
說白了就是弄個看家狗dog,你要定期給其餵食,如果超時不餵食,那麼狗就認爲你,他的主人,你的系統,死機了,就幫你reset重啓系統。
此處解釋爲何ARM7中,CPU地址,即PC,爲何有PC=PC+8這一說法:
衆所周知,AMR7,是三級流水線,其細節見圖:
首先,對於ARM7對應的流水線的執行情況,如下面這個圖所示:
然後對於三級流水線舉例如下:
從上圖,其實很容易看出,第一條指令:
add r0, r1,$5
執行的時候,此時PC已經指向第三條指令:
cmp r2,#3
的地址了,所以,是PC=PC+8.
ARM7的三條流水線,PC=PC+8,很好理解,但是AMR9中,是五級流水線,爲何還是PC=PC+8,而不是
PC
=PC+(5-1)*4
=PC + 16,
呢?
下面就需要好好解釋一番了。
具體解釋之前,先貼上ARM7和ARM9的流水線的區別和聯繫:
下面開始對爲何ARM9也是PC=PC+8進行解釋。
先列出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始終指向你要取的指令的地址
認識清楚了這個前提,後面的舉例講解,就容易懂了。
- 指令週期Cycle1
- 取指
PC總是指向將要讀取的指令的地址(即我們常說的,指向下一條指令的地址),而當前PC=4,
所以去取物理地址爲4對對應的指令
ldr pc, [pc, #20]
其對應二進制代碼爲e59ff014。
此處取指完之後,自動更新PC的值,即PC=PC+4(單個指令佔4字節,所以加4)=4+4=8
- 取指
- 指令週期Cycle2
- 譯指
翻譯指令e59ff014
- 同時再去取指
PC總是指向將要讀取的指令的地址(即我們常說的,指向下一條指令的地址),而當前PC=8,
所以去物理地址爲8所對應的指令“ldr pc, [pc, #20]” 其對應二進制代碼爲e59ff014。
此處取指完之後,自動更新PC的值,即PC=PC+4=8+4=12=0xc
- 譯指
- 指令週期Cycle3
- 執行(指令)
執行“e59ff014”,即
ldr pc, [pc, #20]
所對錶達的含義,即PC
= PC + 20
= 12 + 20
= 32
= 0x20
此處,只是計算出待會要賦值給PC的值是0x20,這個0x20還只是放在執行單元中內部的緩衝中。
- 譯指
翻譯e59ff014
- 取指
此步驟由於是和上面(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:
對於上圖中的,第一個指令在執行的時候,是使用到了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,會繼續執行完成。 |
此處簡單介紹一下,ARM寄存器的別名,以及什麼是APCS。
用文字解釋之前,先看這個版本的解釋,顯得很直觀,很好理解:
默認的情況下,這些寄存器只是叫做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 | 程序計數器 |
更加詳細一點,見下:
The following register names are predeclared:
- r0-r15 and R0-R15
- a1-a4 (argument, result, or scratch registers, synonyms for r0 to r3)
- v1-v8 (variable registers, r4 to r11)
- sb and SB (static base, r9)
- ip and IP (intra-procedure-call scratch register, r12)
- sp and SP (stack pointer, r13)
- lr and LR (link register, r14)
- pc and PC (program counter, r15).
Predeclared extension register names
The following extension register names are predeclared:
- d0-d31 and D0-D31(VFP double-precision registers)
- s0-s31 and S0-S31(VFP single-precision registers)
The following coprocessor names and coprocessor register names are predeclared:
- p0-p15 (coprocessors 0-15)
- c0-c15 (coprocessor registers 0-15).
之前看了很多關於uboot的分析,其中就有說要爲C語言的運行,準備好堆棧。
而自己在Uboot的start.S彙編代碼中,關於系統初始化,也看到有堆棧指針初始化這個動作。但是,從來只是看到有人說系統初始化要初始化堆棧,即正確給堆棧指針sp賦值,但是卻從來沒有看到有人解釋,爲何要初始化堆棧。所以,接下來的內容,就是經過一定的探究,試圖來解釋一下,爲何要初始化堆棧,即:
爲何C語言的函數調用要用到堆棧,而彙編卻不需要初始化堆棧。
要明白這個問題,首先要了解堆棧的作用。
關於堆棧的作用,要詳細講解的話,要很長的篇幅,所以此處只是做簡略介紹。
總的來說,堆棧的作用就是:保存現場/上下文,傳遞參數。
現場,意思就相當於案發現場,總有一些現場的情況,要記錄下來的,否則被別人破壞掉之後,你就無法恢復現場了。而此處說的現場,就是指CPU運行的時候,用到了一些寄存器,比如r0,r1等等,對於這些寄存器的值,如果你不保存而直接跳轉到子函數中去執行,那麼很可能就被其破壞了,因爲其函數執行也要用到這些寄存器。
因此,在函數調用之前,應該將這些寄存器等現場,暫時保持起來,等調用函數執行完畢返回後,再恢復現場。這樣CPU就可以正確的繼續執行了。
在計算機中,你常可以看到上下文這個詞,對應的英文是context。那麼:
保存現場,也叫保存上下文。
上下文,英文叫做context,就是上面的文章,和下面的文章,即與你此刻,當前CPU運行有關係的內容,即那些你用到寄存器。所以,和上面的現場,是一個意思。
保存寄存器的值,一般用的是push指令,將對應的某些寄存器的值,一個個放到堆棧中,把對應的值壓入到堆棧裏面,即所謂的壓棧。
然後待被調用的子函數執行完畢的時候,再調用pop,把堆棧中的一個個的值,賦值給對應的那些你剛開始壓棧時用到的寄存器,把對應的值從堆棧中彈出去,即所謂的出棧。
其中保存的寄存器中,也包括lr的值(因爲用bl指令進行跳轉的話,那麼之前的pc的值是存在lr中的),然後在子程序執行完畢的時候,再把堆棧中的lr的值pop出來,賦值給pc,這樣就實現了子函數的正確的返回。
C語言進行函數調用的時候,常常會傳遞給被調用的函數一些參數,對於這些C語言級別的參數,被編譯器翻譯成彙編語言的時候,就要找個地方存放一下,並且讓被調用的函數能夠訪問,否則就沒發實現傳遞參數了。對於找個地方放一下,分兩種情況。
一種情況是,本身傳遞的參數就很少,就可以通過寄存器傳送參數。
因爲在前面的保存現場的動作中,已經保存好了對應的寄存器的值,那麼此時,這些寄存器就是空閒的,可以供我們使用的了,那就可以放參數,而參數少的情況下,就足夠存放參數了,比如參數有2個,那麼就用r0和r1存放即可。(關於參數1和參數2,具體哪個放在r0,哪個放在r1,就是和APCS中的“在函數調用之間傳遞/返回參數”相關了,APCS中會有詳細的約定。感興趣的自己去研究。)
但是如果參數太多,寄存器不夠用,那麼就得把多餘的參數堆棧中了。
即,可以用堆棧來傳遞所有的或寄存器放不下的那些多餘的參數。
對於上面的解釋的堆棧的作用顯得有些抽象,此處再用例子來簡單說明一下,就容易明白了:
用:
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的寄存器的使用的約定而設計的,你最好不要隨便改變使用方式,最好還是按照其約定的來處理,這樣程序更加符合規範。 |
在分析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的操作數。
關於mov指令操作數的取值範圍,網上看到一些人說是0x00-0xFF,也有人說是其他的值的,但是經過一番求證,發現這些說法都不對。下面就是來詳細解釋,mov指令的操作數的取指範圍,到底是多少。
在看了我說的,關於這行代碼:
mov r0, 0x33d00000
的源操作數0x33d0000,可能是mov指令所不允許的,這句話後,可能有人會說,我知道,那是因爲mov的操作數的值,不允許大於255,至少網上很多人的資料介紹中,都是這麼說的。
對此,要說的是,你的回答是錯誤的。
關於mov操作數的真正的允許的取值範圍,還真的不是那麼容易就能搞懂的,下面就來詳細解釋解釋。
總的來說,我是從ARM 彙編的mov操作立即數的疑問
裏面,纔算清楚mov的取值範圍,以及找了相應的datasheet,才最終看懂整個事情的來龍去脈的。
首先,mov的指令,是屬於ARM指令集中,數據處理(Data Process)分類中的其中一個指令,
而數據處理指令的具體格式是:ARM Processor Instruction Set
對於此格式,我們可以拿:
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 |
|
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中寫到:
意思是,對於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的操作數,而這樣的數,其實是很多的。 |
對於我們之前分析的start.S中,涉及到很多的彙編的語句,其中,可以看出,很多包含了很多種不同的語法,使用慣例等,下面,就對此進行一些總結,藉以實現一定的舉一反三或者說觸類旁通,這樣,可以起到一定的借鑑功能,方便以後看其他類似彙編代碼, 容易看懂彙編代碼所要表達的含義。
像前面彙編代碼中,有很多的,以點開頭,加上一個名字的形式的標號,比如:
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 |
對應地,和上面的例子中的C語言中的編號和掉轉到標號的goto類似,彙編中,對於定義了標號,那麼也會有對應的指令,去跳轉到對應的彙編中的標號。
這些跳轉的指令,就是b指令,b是branch的縮寫。
b指令的格式是:
b{cond} label
簡單說就是跳轉到label處。
用和上面的例子相關的代碼來舉例:
.globl _start _start: b reset
就是用b指令跳轉到上面那個reset的標號。
【總結】 | |
---|---|
彙編中的b跳轉指令 = C語言中的goto |
對於上面例子中:
.globl _start
中的.global,就是聲明_start爲全局變量/標號,可以供其他源文件所訪問。
即彙編器,在編譯此彙編代碼的時候,會將此變量記下來,知道其是個全局變量,遇到其他文件是用到此變量的的時候,知道是訪問這個全局變量的。
因此,從功能上來說,就相當於C語言用extern去生命一個變量,以實現本文件外部訪問此變量。
【總結】 | |
---|---|
彙編中的.globl或.global = C語言中的extern |
和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,實現函數跳轉回來。繼續執行下一條指令。
而如果你本身自己寫彙編語言的話,那麼這些函數跳轉前後要做的事情,都是你程序員自己要關心,要實現的事情。
像前文所解析的代碼中類似於這樣的:
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
【總結】 | |
---|---|
彙編中類似這樣的代碼: label1: .word value2 就相當於C語言中的: int *label1 = value2 但是在C語言中引用該標號/變量的時候,卻是直接拿來用的,就像這樣: label1 = other_value 其中label1就是個int型的變量。 |
接着上面的內容,繼續解釋,對於彙編中這樣的代碼:
第一種:
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語言的函數。
在彙編代碼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僞指令,讓編譯器來幫你自動判斷:
- 如果該操作數是mov的有效操作數,那麼ldr僞指令就會被翻譯成對應的mov指令
例 3.2.
舉例說明:
彙編代碼:
# define pWTCON 0x53000000 ...... ldr r0, =pWTCON
被翻譯後的真正的彙編代碼:
33d00068: e3a00453 mov r0, #1392508928 ; 0x53000000
- 如果該操作數不是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寄存器。
轉載自:http://www.crifan.com/files/doc/docbook/uboot_starts_analysis/release/html/uboot_starts_analysis.html