過程中也發現了assembly與linker script之間的有趣"交流"
特別記錄一下.....
以下的code將以Linux 3.3.4的版本做說明
Linux Kernel在bootup時,會去確定我們今天所使用的Processor type:
arch/arm/kernel/head.S:
/*
* Kernel startup entry point.
* ---------------------------
*
* This is normally called from the decompressor code. The requirements
* are: MMU = off, D-cache = off, I-cache = dont care, r0 = 0,
* r1 = machine nr, r2 = atags or dtb pointer.
*
* This code is mostly position independent, so if you link the kernel at
* 0xc0008000, you call this at __pa(0xc0008000).
*
* See linux/arch/arm/tools/mach-types for the complete list of machine
* numbers for r1.
*
* We're trying to keep crap to a minimum; DO NOT add any machine specific
* crap here - that's what the boot loader (or in extreme, well justified
* circumstances, zImage) is for.
*/
.arm
__HEAD
ENTRY(stext)
THUMB( adr r9, BSYM(1f) ) @ Kernel is always entered in ARM.
THUMB( bx r9 ) @ If this is a Thumb-2 kernel,
THUMB( .thumb ) @ switch to Thumb now.
THUMB(1: )
setmode PSR_F_BIT | PSR_I_BIT | SVC_MODE, r9 @ ensure svc mode
@ and irqs disabled
mrc p15, 0, r9, c0, c0 @ get processor id
bl __lookup_processor_type @ r5=procinfo r9=cpuid
movs r10, r5 @ invalid processor (r5=0)?
THUMB( it eq ) @ force fixup-able long branch encoding
beq __error_p @ yes, error 'p'
__lookup_processor_type被定義在:arch/arm/kernel/head-common.S
/*
* Read processor ID register (CP#15, CR0), and look up in the linker-built
* supported processor list. Note that we can't use the absolute addresses
* for the __proc_info lists since we aren't running with the MMU on
* (and therefore, we are not in the correct address space). We have to
* calculate the offset.
*
* r9 = cpuid
* Returns:
* r3, r4, r6 corrupted
* r5 = proc_info pointer in physical address space
* r9 = cpuid (preserved)
*/
__CPUINIT
__lookup_processor_type:
adr r3, __lookup_processor_type_data
ldmia r3, {r4 - r6}
sub r3, r3, r4 @ get offset between virt&phys
add r5, r5, r3 @ convert virt addresses to
add r6, r6, r3 @ physical address space
1: ldmia r5, {r3, r4} @ value, mask
and r4, r4, r9 @ mask wanted bits
teq r3, r4
beq 2f
add r5, r5, #PROC_INFO_SZ @ sizeof(proc_info_list)
cmp r5, r6
blo 1b
mov r5, #0 @ unknown processor
2: mov pc, lr
ENDPROC(__lookup_processor_type)
/*
* Look in
*/
.align 2
.type __lookup_processor_type_data, %object
__lookup_processor_type_data:
.long .
.long __proc_info_begin
.long __proc_info_end
.size __lookup_processor_type_data, . - __lookup_processor_type_data
adr為ARM的偽指令,根據參考資料:理解adr,ldr指令 的說明
adr是小範圍的位址讀取偽指令,adr指令將基於pc相對偏移的地址值讀取到暫存器中
所以在此r3的值 = (標號__lookup_processor_type_data的位址與此指令的距離差) + (此指令的位址)
= 標號__lookup_processor_type_data的位址
#Update: 此位址為實際運行時的位址,因為參考了PC值
ex. 如果我們的Kernel是被鏈結到0xC0008000的位址
而__lookup_processor_type_data在Kernel中的offset為0x514
(也就是說__lookup_processor_type_data的鏈結位址為0xC0008000 + 0x514 = 0xC0008514)
但若實際運行時我們的Kernel是被搬移到0x30008000 (實體記憶體位址) 去執行
則此時r3的值為0x30008000 + 0x514 = 0x30008514,而非原本的鏈結位址0xC0008514
而ldmia r3, {r4 - r6}這條指令最後的結果為:
r4 = 標號__lookup_processor_type_data的位址
r5 = __proc_info_begin的位址
r6 = __proc_info_end的位址
(ldmia會把較底位址的值載入到相對小的暫存器,而r4相較於r5,r6為"相對小"的暫存器,以此類推...)
最後r3的值即為.long __proc_info_end的下一條指令的位址
另外根據參考資料: arm linux kernel 從入口到start_kernel 的代碼分析
這裡需要注意的是連結位址與運行時地址的區別
在此r4儲存的是鏈結位址(虛擬位址)
而r3儲存的是運行時的地址(實體位址)
sub r3, r3, r4 @ get offset between virt&phys
這條指令是取得實體位址與虛擬位址之間的offset值並存至暫存器r3
因此,將r3減去r4的值即為實際運行位址與鏈結位址之間的offset值
add r5, r5, r3 @ convert virt addresses to
add r6, r6, r3 @ physical address space
這邊將__proc_info_begin及__proc_info_end的位址加上offset值 (r3)
轉換成實體記憶體位址以便存取
1: ldmia r5, {r3, r4} @ value, mask
這條指令從r5 = __proc_info_begin的位址開始取值
每次4 bytes, 並將值存至r3, r4暫存器
這邊就要去尋找__proc_info_begin在哪裡被定義才知道所存取值的內容為何
我們可以在linker script: arch/arm/kernel/vmlinux.lds.S中找到其定義
#define PROC_INFO \
. = ALIGN(4); \
VMLINUX_SYMBOL(__proc_info_begin) = .; \
*(.proc.info.init) \
VMLINUX_SYMBOL(__proc_info_end) = .;
........... (中間省略)
.text : { /* Real text segment */
_stext = .; /* Text and read-only data */
__exception_text_start = .;
*(.exception.text)
__exception_text_end = .;
IRQENTRY_TEXT
TEXT_TEXT
SCHED_TEXT
LOCK_TEXT
KPROBES_TEXT
IDMAP_TEXT
#ifdef CONFIG_MMU
*(.fixup)
#endif
*(.gnu.warning)
*(.glue_7)
*(.glue_7t)
. = ALIGN(4);
*(.got) /* Global offset table */
ARM_CPU_KEEP(PROC_INFO)
}
其中VMLINUX_SYMBOL巨集被定義在: include/asm-generic/vmlinux.lds.h
#ifndef SYMBOL_PREFIX
#define VMLINUX_SYMBOL(sym) sym
#else
#define PASTE2(x,y) x##y
#define PASTE(x,y) PASTE2(x,y)
#define VMLINUX_SYMBOL(sym) PASTE(SYMBOL_PREFIX, sym)
#endif
如果SYMBOL_PREFIX有被定義
則在sym前面加上SYMBOL_PREFIX的字串
不過這邊我們可以先不考慮是否有加上SYMBOL_PREFIX
而ARM_CPU_KEEP巨集在同一個檔案(vmlinux.lds.S)中被定義為:
#ifdef CONFIG_HOTPLUG_CPU
#define ARM_CPU_DISCARD(x)
#define ARM_CPU_KEEP(x) x
#else
#define ARM_CPU_DISCARD(x) x
#define ARM_CPU_KEEP(x)
#endif
也就是說如果我們有選定CONFIG_HOTPLUG_CPU這個選項
則會將PROC_INFO這個巨集給展開
Kernel在編譯時便會將所有標記為.pro.info.init的section
給加入到.text section中最尾端的部位
並以__proc_info_begin及__proc_info_end標記出此section的開始及結束位址
至於CONFIG_HOTPLUG_CPU這個選項可以參考:Documentation/cpu-hotplug.txt 的說明,
不過我並沒有深入研究就是...
在vmlinux.lds.S中我們找到了__proc_info_begin及__proc_info_end的定義
接下來就要尋找究竟哪段code是被標記為.proc.info.init的section
假設我們所使用的CPU為ARM926EJ-S
那麼在: /arch/arm/mm/proc-arm926.S可以找到被標記為.proc.info.init的section
(在相同的資料夾下會有針對不同CPU的proc-檔)
.section ".proc.info.init", #alloc, #execinstr
(後面接的#alloc, #execinstr還不清楚有啥作用...)
.type __arm926_proc_info,#object
__arm926_proc_info:
.long 0x41069260 @ ARM926EJ-S (v5TEJ)
.long 0xff0ffff0
.long PMD_TYPE_SECT | \
PMD_SECT_BUFFERABLE | \
PMD_SECT_CACHEABLE | \
PMD_BIT4 | \
PMD_SECT_AP_WRITE | \
PMD_SECT_AP_READ
.long PMD_TYPE_SECT | \
PMD_BIT4 | \
PMD_SECT_AP_WRITE | \
PMD_SECT_AP_READ
b __arm926_setup
.long cpu_arch_name
.long cpu_elf_name
.long HWCAP_SWP|HWCAP_HALF|HWCAP_THUMB|HWCAP_FAST_MULT|HWCAP_EDSP|HWCAP_JAVA
.long cpu_arm926_name
.long arm926_processor_functions
.long v4wbi_tlb_fns
.long v4wb_user_fns
.long arm926_cache_fns
.size __arm926_proc_info, . - __arm926_proc_info
在proc-arm926.S中我們可以看到以上這段code被宣告為:
.section ".proc.info.init", #alloc, #execinstr
也就是說接下來的code皆會被標記為.proc.info.init的section
對應回arch/arm/kernel/head-common.S:
1: ldmia r5, {r3, r4} @ value, mask
我們可以知道:
r3 = 0x41069260 @ ARM926EJ-S (v5TEJ)
r4 = 0xff0ffff0
也就是註解所說明之value及mask
後面的r5, r6也可以用同樣的方式去取得並運算:
and r4, r4, r9 @ mask wanted bits
teq r3, r4
beq 2f
add r5, r5, #PROC_INFO_SZ @ sizeof(proc_info_list)
cmp r5, r6
blo 1b
mov r5, #0 @ unknown processor
2: mov pc, lr
ENDPROC(__lookup_processor_type)
主要就是檢查我們今天所使用的Processor type與目前Kernel所編譯之選項是否相符
如果有誤,則會將r5設為0並return
----------------------------------------------------------
由這幾段code,我們可以發現在Kernel中使用了相當多平常所不會使用的技巧
像是assmebly code及linker script之間調用的關係
以及最後在asssebly code中取用由另外assembly code所定義之CPU value及mask值的方式
都是非常值得學習的...
試想要是我們一開始就把CPU value及mask的值寫死在head-common.S中
那麼未來想要擴增新的CPU平台這段code就必須重新修改
增加程式維護上的困難.....
而透過assembly code及linker script的調用方式
我們只需要如同proc-arm926.S
在arch/arm/mm資料夾中新增相對應平台之CPU檔案
即可將新的CPU平台給移植過去
對於程式的維護及移植性都是相當便利的!!
----------------------------------------------------------
其實我原本在trace的Kernel版本是Linux 2.6.2x
不過在寫這篇文章的時候手邊只有最新版的Linux 3.3.4
原本想說可能內容差不多就沒下載2.6.2x版本的source code
結果沒想到裡面又多了一些原本所沒有的macro
不得不說Kernel真的變動的很快....
而且寫法多變, 又很複雜!!
Ctags + Cscope其實已經沒辦法應付這麼複雜架構的source code了
有時候不得已還是得搭配grep才能找到某個function或macro真正定義的檔案所在
但從Kernel中真的可以學到不少C或assembly code的寫作技巧就是!!
(只是自己未來會不會用就又是另外一回事了...)
----------------------------------------------------------
預計下一篇會記錄C和組語對於組語內部符號引用的差異
是之前trace U-Boot所碰到的問題.....
C和組語真的是博大精深!! XD
1 則留言:
關於文章中所提到:
.section ".proc.info.init", #alloc, #execinstr
後面所接的#alloc, #execinstr參數
在Google後發現其實#alloc, #execinstr就是"ax"的別名
詳細可以參考:
http://lkml.indiana.edu/hypermail/linux/kernel/0304.1/1924.html
gives all the details. To summarise though:
"a" or "#alloc" - the section is allocatable
"x" or "#execinstr" - the section is executable
"ax" seems to be what Linus uses. I used to use the long versions, but changed to the shorter version - less characters to type, but still fairly readable.
張貼留言