2012年5月8日 星期二

Linux Kernel: __lookup_processor_type

最近在trace Linux Kernel的bootup流程
過程中也發現了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 for information about the __proc_info structure.
 */
.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
不過我不知道為何這樣就可以取得實體位址與虛擬位址之間的offset值

#Update: r4儲存的是鏈結位址,若參考上例#Update,則此時r4的值即為0xC0008514
因此,將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
也就是註解所說明之valuemask

後面的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.