UBoot怎么跳轉到Kernel:uboot與linux的交界
不知道你是否有這種感覺,就是學習了一段時間Uboot,學習了一段時間kernel,也知道Uboot是引導程序。但是總是連不起來。
我為什么來的這樣的感受是因為,我最近在學習安全相關的東西。但是這個安全的東西應用在kernel里面進行,調用的。我想知道在整個啟動的過程中哪里被調用的。結果發(fā)現在Uboot到kernel過渡的時候接不上了。說來慚愧。于是就決定學習一下這個部分。
按照我的習慣,從宏觀和微觀兩個角度,宏觀了解到底是個啥?微觀了解具體是個啥?
記得關注哼哼哼?。。。。。。。。。。。。。。?!
1、宏觀-Linux內核是怎么被引導加載啟動的?
說明一
首先我們知道kernel的鏡像最開始是壓縮的zImage格式的存在,然后Uboot有工具mkimage把其轉換為uImage。
然后這個uImage被加載到哪里呢?這個就是Uboot里面的bootm機制來搞定的。

U-Boot命令bootm將內核映像復制到0x00010000,將RAMDISK映像復制到0x00800000。這時,U-Boot跳轉到地址0x00010000來啟動Linux內核。
說明二
zImage內核鏡像下載到開發(fā)板之后,可以使用u-boot的go命令進行直接跳轉,這個時候內核直接解壓啟動。
但是此時的內核無法掛載文件系統(tǒng),因為go命令沒有將內核需要的相關啟動參數從u-boot中傳遞給內核。
傳遞相關啟動參數必須使用u-boot的bootm命令進行跳轉,但是u-boot的bootm命令只能處理uImage鏡像。
uImage相對于zImage在頭部多了64個byte,即為0x40。
(這里你應該知道了為什么要使用bootm命令,以及為什么要是有uImage格式)
說明三
在前面我們曾經分析過Uboot的啟動流程,兩個階段。
程序最終執(zhí)行common/main.c中的main_loop。在此之前都是進行一些初始化工作,U-Boot的main_loop函數相當于main主函數。main_loop函數的結構很復雜,它所做的工作與具體的平臺無關,主要目的是處理用戶輸入的命令和引導內核啟動。(終于看到了引導內核加載)
main_loop 函數的調用關系錯綜復雜,而且摻雜關系復雜的條件編譯,我們抓住與命令實現密切相關的操作來分析命令的實現原理。命令實現的大致流程如圖2.12所示。

1.啟動延時
如果配置了啟動延遲功能,U-Boot等待用戶從控制臺(一般為串口)輸入字符,等待的時間由頂層配置文件中的宏定義 CONFIG_BOOTDELAY 決定。在此期間,只要用戶按下任意按鍵就會中斷等待,進入命令行輸入模式。
如果沒有配置啟動延時功能或者啟動延時超過了設置的時間, U-Boot 運行啟動命令行參數,啟動命令參數在頂層配置文件中,由 CONFIG_BOOTCOMMAND宏定義。
2.讀取命令行輸入
命令行輸入模式實際上是一個死循環(huán),循環(huán)體簡化后如下所示:
????????????for?(;;)?{
????????????????len?=?readline?(CONFIG_SYS_PROMPT);
????????????????flag?=?0;????/*?assume?no?special?flags?for?now?*/
????????????????if?(len?>?0)
????????????????????strcpy?(lastcommand,?console_buffer);
????????????????else?if?(len?==?0)
????????????????????flag?|=?CMD_FLAG_REPEAT;
????????????????if?(len?==?-1)
????????????????????puts?("<INTERRUPT>\n");
????????????????else
????????????????????rc?=?run_command(lastcommand,?flag);
????????????????if?(rc?<=?0)?{
????????????????????/*?invalid?command?or?not?repeatable,?forget?it?*/
????????????????????lastcommand[0]?=?0;
????????????????}
????????????}
每次循環(huán)調用readline函數從控制臺讀取命令行,并且讀取到的字符存儲在console_buffer緩沖區(qū)中。
console_buffer緩沖區(qū)的長度在頂層文件中通過CONFIG_SYS_CBSIZE宏定義。
當該函數在接收到一個回車鍵時認定為命令行輸入結束,返回命令行長度len。
如果len大于0,將存儲在緩沖區(qū)的命令行拷貝至靜態(tài)數組lastcommand中,flag設置為0。
如果len等于0,即readline函數僅僅接收到一個回車鍵,即直接返回,flag設置為CMD_FLAG_REPEAT,lastcommand數組存放的數據不變。
flag用于標志是否重復上次操作,每個命令都有一個 repeatable標志,當命令的該標志為1時,此時,命令能夠重復操作。
把lastcommand和flag作為run_command函數的參數,進而調用run_command函數。
從 run_command 函數是否會返回的角度看,U-Boot 的命令分為兩類。
一類是函數返回數值rc,rc小于等于0,則傳入的命令行參數有誤,命令無效,此時把lastcommand數組清零,不再執(zhí)行重復操作。
另外一類是不再返回,一去不再復返,例如bootm、go等命令,這類用于啟動內核,將CPU的管理權從U-Boot交付給內核,完成自己啟動內核的終極使命。
3.解析命令行
傳入的 lastcommand 參數僅僅是 readline 函數讀取到用戶輸入的字符,接下來最主要的工作是解析命令行。
首先判斷傳入的lastcommand參數是否為空,如果是返回?1,否則繼續(xù)往下解析。截取函數的關鍵代碼如下,str指針指向lastcommand區(qū)域。
????????????while?(*str)?{
??????????????????for?(inquotes?=?0,?sep?=?str;?*sep;?sep++)?{
????????????????????if?((*sep=='\'')?&&
????????????????????????(*(sep-1)?!=?'\\'))
????????????????????????inquotes=!inquotes;
????????????????????if?(!inquotes?&&
????????????????????????(*sep?==?';')?&&???/*?separator?????*/
????????????????????????(?sep?!=?str)?&&??/*?past?string?start??*/
????????????????????????(*(sep-1)?!=?'\\'))????/*?and?NOT?escaped????*/
????????????????????????break;
??????????????????}
U-Boot允許命令行存在多個命令,命令間用“;”或者“\;”字符分割。
??????????????????token?=?str;
??????????????????if?(*sep)?{
????????????????????str?=?sep?+?1;???/*?start?of?command?for?next?pass?*/
????????????????????*sep?=?'\0';
??????????????????}
??????????????????else
????????????????????str?=?sep;???/*?no?more?commands?for?next?pass?*/
??????????????????/*?Extract?arguments?*/
??????????????????if?((argc?=?parse_line?(finaltoken,?argv))?==?0)?{
????????????????????rc?=?-1;?/*?no?command?at?all?*/
????????????????????continue;
??????????????????}
??????????????????if?(cmd_process(flag,?argc,?argv,?&repeatable,?NULL))
????????????????????rc?=?-1;
首先解析一個命令,token指向待解析命令的地址。
parse_line函數分離出命令的各個參數,分別存放在argv中,參數的數目為argc,接著調用common/command.c文件中的cmd_process函數處理解析得到的命令。
值得注意的是,命令的第一個參數是命令的名稱。當前命令處理完畢后, token指向命令行中的下一個命令,直到所有的命令都處理完畢。
4.命令處理
main.c中的代碼實現了將一個命令的所有參數分離存放在argv數組中,參數的數目為argc,完成了讀取命令行和解析命令行的工作。命令的處理由common/command.c文件中的函數完成。U-Boot在include/command.h中定義了一個非常重要的cmd_tbl_s結構體,它在命令的實現方面起著至關重要的作用。
????????struct?cmd_tbl_s?{
????????????char????*name;???????/*?命令名稱????????????*/
????????????int?????maxargs;?/*?命令的最大參數???*/
????????????int?????repeatable;??/*?是否可重復(按回車鍵是否會重復執(zhí)行)
????????????*/
????????????int?????(*cmd)(struct?cmd_tbl_s?*,?int,?int,?char?*?const?[]);??/*?命令響應函數*/
????????????char????*usage;??????/*?簡短的用法說明???*/
????????
????????????char????*help;???????/*?較詳細的幫助*/
????????
????????
????????????/*?響應自動補全參數*/
????????????int?????(*complete)(int?argc,char*const?argv[],char?last_char,int?maxv,char*cmdv[]);
????????
????????};
cmd_tbl_s結構體包含的成員變量:命令名稱、最大參數個數、可重復性、命令響應函數、用法、幫助和命令補全函數,每個命令都由這個結構體來描述。當輸入“help”或者“?”會打印出所有的命令和它的usage,輸入“help”或者“?”和命令名稱時,會打印出help信息。
添加一個命令時,利用宏U_BOOT_CMD定義一個新的cmd_tbl_s結構體,并對這個結構體初始化和定義結構體的屬性。例如,在文件common/cmd_bdinfo.c中:
????????U_BOOT_CMD(
????????????bdinfo,??1,??1,??do_bdinfo,
????????????"print?Board?Info?structure",
????????????""
????????);
增加了一個命令,它的名稱為bdinfo,最大參數數目為1,可重復,響應函數是do_bdinfo, usage為“print Board Info structure”,沒有幫助信息。U_BOOT_CMD宏在include/command.h中定義,當不配置命令補全時,它最終被展開為:
??????????
其中,“##”與“#”是預編譯操作符,“##”表示字符串連接,“#”表示后面緊接著的是一個字符串。cmd_tbl_t就是struct cmd_tbl_s,用于u_boot_cmd_##name結構體。__attribute定義了結構體的屬性,將結構體放在.u_boot_cmd段中。簡單的說,就是利用U_BOOT_CMD定義struct cmd_tbl_s結構體變量,并把類變量都放在一個段中。在鏈接腳本中指定了.u_boot_cmd段的起始地址和結束地址,又已知每個struct cmd_tbl_s結構體占用內存空間的大小,這樣就很方便地遍歷所有的struct cmd_tbl_s結構體。這種巧妙的方式充分利用了鏈接器的功能特點,避免了花費大量的精力,去維護和更新命令結構體表。
????????????cmdtp?=?find_cmd(argv[0]);
????????????if?(cmdtp?==?NULL)?{
??????????????????printf("Unknown?command?'%s'?-?try?'help'\n",?argv[0]);
??????????????????return?1;
????????????}
cmd_process函數首先調用find_cmd函數根據傳入的參數,在.u_boot_cmd段區(qū)域查找命令,如果沒有找到對應的命令,打印出提示信息并返回。如果找到則返回命令結構體 cmdtp,再檢查傳入參數的合法性,最后通過cmd_call函數調用命令響應函數(cmdtp->cmd)(cmdtp, flag, argc, argv)。
說明四
U-boot是通過執(zhí)行u-boot提供的命令來加載Linux內核的,其中命令bootm的功能即為從memory啟動Linux內核映像文件。在講解bootm加載內核之前,先來看看u-boot中u-boot命令的執(zhí)行過程。
1、u-boot命令的執(zhí)行過程
在u-boot命令執(zhí)行到最后時,開始進入命令循環(huán),等待用戶輸入命令和處理命令,這是通過循環(huán)調用main_loop()函數來實現的,main_loop函數的主要代碼如下所示。
??????????????len=readline?(CONFIG_SYS_PROMPT);
??????????????????????flag=0;??/*assume?no?special?flags?for?now*/
??????????????????????if?(len?>?0)
??????????????????????????strcpy?(lastcommand,?console_buffer);
??????????????????????else?if?(len?==?0)
??????????????????????????flag?—=?CMD_FLAG_REPEAT;
??????????????????????rc=run_command?(lastcommand,?flag);
Main_loop函數從串口終端讀入用戶輸入的要執(zhí)行的命令行(包括命令和參數),然后調用run_command函數來執(zhí)行用戶輸入的命令行。下面分析run_command函數的主要工作流程,run_command的主要源碼如下所示。
????????????????strcpy(cmdbuf,?cmd);
???????????????/*Extract?arguments*/
???????????????????????????if((argc=parse_line(finaltoken,?argv))==0){
???????????????????????????????rc=-1;???/*no?command?at?all*/
???????????????????????????????continue;
???????????????????????????}
??????????????????????????/*Look?up?command?in?command?table*/
???????????????????????????if((cmdtp=find_cmd(argv[0]))==NULL){
???????????????????????????????printf("Unknown?command'?%s'?-try'?help'?\n",?argv[0]);
???????????????????????????????rc=-1;???/*give?up?after?bad?command*/
???????????????????????????????continue;
???????????????????????????}
??????????????????????????/*OK-call?function?to?do?the?command*/
???????????????????????????if((cmdtp->cmd)(cmdtp,?flag,?argc,?argv)!?=0){
???????????????????????????????rc=-1;
???????????????????????????}
從代碼中可以看出,run_command函數通過調用函數parse_line分析出該命令行所對應的參數個數argc和參數指針數組*argv[ ],其中argv[0]中保存的是u-boot命令名字符串,接著調用函數find_cmd,函數根據命令名在u-boot命令列表中找到該命令對應的u-boot命令結構體cmd_tbl_t所在的地址,找到該u-boot命令對應的命令結構體后,就可以調用該結構體中的u-boot命令對應的執(zhí)行函數來完成該u-boot命令的功能,這樣一個u-boot命令就執(zhí)行完成了。
下面再來看看u-boot命令結構體cmd_tbl_t及其定義過程和存放的位置。U-boot命令結構體cmd_tbl_t定義如下所示。
????????????????struct?cmd_tbl_s{
????????????????????char???????*name;?????????/*Command?Name?????????????????????*/
????????????????????int????????maxargs;?/*maximum?number?of?arguments*/
????????????????????int????????repeatable;/*autorepeat?allowed???????*/
????????????????????????????????????????/*Implementation?function????*/
????????????????????int????????(*cmd)(struct?cmd_tbl_s*,?int,?int,?char*[]);
????????????????????char???????*usage;????????/*Usage?message????????(short)?????*/
????????????????
????????????????????char???????*help;?????????/*Help??message???????(long)??????*/
????????????????
????????????????
???????????????????/*?do?auto?completion?on?the?arguments?*/
????????????????????int????????(*complete)(int?argc,?char*argv[],?char?last_char,?int?maxv,?char*cmdv[]);
????????????????
????????????????};
Cmd_tbl_t結構用來保存u-boot命令的相關信息,包括命令名稱、對應的執(zhí)行函數、使用說明、幫助信息等。每一條u-boot命令都對應一個cmd_tbl_t結構體變量,在u-boot中是通過宏U_BOOT_CMD來實現cmd_tbl_t結構體變量的定義和初始化的。例如,bootm命令對應U_BOOT_CMD調用,代碼如下所示。
??????????????U_BOOT_CMD(
??????????????????bootm,????CONFIG_SYS_MAXARGS,?????1,????do_bootm,
??????????????????"boot?application?image?from?memory",
??????????????????"[addr[arg...]]\n?????-boot?application?image?stored?in?memory\n"
??????????????????"\tpassing?arguments?'?arg?...'?;?when?booting?a?Linux?kernel,?\n"
??????????????????"\t'?arg'?can?be?the?address?of?an?initrd?image\n");
U_BOOT_CMD宏定義如下所示:
??????????????
??????????????cmd_tbl_t?__u_boot_cmd_
這樣我們通過U_BOOT_CMD宏就定義了cmd_tbl_t類型的結構體變量,變量名為__u_boot_cmd_bootm,同時用U_BOOT_CMD宏中的參數對cmd_tbl_t結構體中的每個成員進行初始化。
Struct_Section也是一個宏定義,定義如下所示。
??????????????
Struct_Section定義了結構體變量的段屬性,cmd_tbl_t類型的結構體變量鏈接時全部鏈接到u_boot_cmd段中,可以查看u-boot.lds文件對u_boot_cmd段位置的安排。
(這里我們應該有想到,關于uboot_cmd,我們可以外界輸入,內部的肯定也有提前預設的值,比如啟動內核這些。如果在這個啟動延時的過程中不進行輸入,那么就會去執(zhí)行這些默認的命令。)
上面我們知道了bootm這個命令是引導加載內核的,下面來看看bootm。
說明五
Bootm命令用來從memory啟動內核,bootm命令的執(zhí)行流程如下圖所示。
在串口終端輸入bootm命令后,執(zhí)行do_bootm函數來完成相應的功能。Do_bootm函數首先調用bootm_start函數。(如果不輸入,應該也有。)

Bootm_start函數的主要作用是獲取內核映像文件的相關信息,并保存到全局變量images中,image是struct bootm_headers結構類型,用來保存可執(zhí)行內核映像的相關信息,主要包括內核映像的加載地址、起始地址、可執(zhí)行入口地址等。
獲取內核映像的相關信息是為后面的加載內核做準備;
內核可執(zhí)行映像文件頭包含了這些信息,這是通過工具mkimage加上去的。接下來執(zhí)行bootm_load_os函數。
??????????????if?(load?!=image_start)?{
??????????????????????????????memmove_wd?((void?*)load,
??????????????????????????????????????(void?*)image_start,?image_len,?CHUNKSZ);
??????????????????????????}
Image_start是不包括內核映像文件頭的內核起始位置,也就是zImage的起始位置。
內核加載完成后,下面開始執(zhí)行內核映像,這是通過調用函數do_bootm_linux來實現的,下面來看do_bootm_linux的執(zhí)行過程。
Do_bootm_linux首先驅動內核的入口地址,代碼如下所示。
??????????????theKernel=(void?(*)(int,?int,?uint))images->ep;
Images.ep為內核可執(zhí)行映像文件的入口地址及zImage的起始地址,它是從內核映像文件頭獲取的,在前面的bootm_start函數中已經為它賦值,代碼如下所示。
??????????????images.ep=image_get_ep?(&images.legacy_hdr_os_copy);
如果需要,準備給內核傳遞的啟動參數,然后獲取啟動內核需要的兩個參數:machid和傳遞給內核參數的位置,這兩個參數都保存在全局數據結構體變量bd的成員變量中,如下所示。
??????????????bd->bi_boot_params
??????????????machid=bd->bi_arch_number;
最后調用內核映像的第一個可執(zhí)行函數,把控制權移交給內核,代碼如下所示。
??????????????theKernel?(0,?machid,?bd->bi_boot_params);
說明六
一個cmd_tbl_t結構體變量包含了調用一條命令的所需要的信息。
對于環(huán)境變量bootcmd,執(zhí)行run_command(bootcmd, flag)之后,最終是將bootcmd中的參數解析為命令,海思hi3521a中默認參數是bootcmd=bootm 0x82000000
相當于執(zhí)行bootm 0x82000000 命令
最終將調用do_bootm函數,do_bootm函數在cmd_bootm.c中實現

在這個里面有一個函數:
int?do_bootm_linux(int?flag,?int?argc,?char?*argv[],?bootm_headers_t?*images)
{
????bd_t????*bd?=?gd->bd;
????char????*s;
????int?machid?=?bd->bi_arch_number;
????void????(*theKernel)(int?zero,?int?arch,?uint?params);
????char?*commandline?=?getenv("slave_bootargs");
????char?*commandline?=?getenv("bootargs");???//(1)
????if?((flag?!=?0)?&&?(flag?!=?BOOTM_STATE_OS_GO))
????????return?1;
????theKernel?=?(void?(*)(int,?int,?uint))images->ep;?//(2)
????s?=?getenv?("machid");??????????????????????????//(3)
????if?(s)?{
????????machid?=?simple_strtoul?(s,?NULL,?16);
????????printf?("Using?machid?0x%x?from?environment\n",?machid);
????}
????show_boot_progress?(15);
????debug?("##?Transferring?control?to?Linux?(at?address?%08lx)?...\n",
???????????(ulong)?theKernel);
????setup_start_tag?(bd);???????????????????//(4)
????setup_memory_tags?(bd);?????????????????
????setup_commandline_tag?(bd,?commandline);?//(5)
????if?(images->rd_start?&&?images->rd_end)?????
????????setup_initrd_tag?(bd,?images->rd_start,?images->rd_end);
????setup_eth_use_mdio_tag(bd,?getenv("use_mdio"));
????setup_eth_mdiointf_tag(bd,?getenv("mdio_intf"));
????setup_ethaddr_tag(bd,?getenv("ethaddr"));???
????setup_end_tag?(bd);?????????????????????//(6)
????/*?we?assume?that?the?kernel?is?in?place?*/
????printf?("\nStarting?kernel?...\n\n");
????{
????????extern?void?udc_disconnect?(void);
????????udc_disconnect?();
????}
????cleanup_before_linux?();????????????//(7)
????theKernel?(0,?machid,?bd->bi_boot_params);?//(8)
????/*?does?not?return?*/
????return?1;
}
(1)獲取環(huán)境變量bootargs中的值,該環(huán)境變量用來傳遞參數給kernel
(2)images->ep的地址是kernel的程序的入口地址,也就是將函數指針theKernel指向kernel最先執(zhí)行的地方。
(3)獲取環(huán)境變量machid,這個應該是機器碼,海思設備沒有定義在環(huán)境變量中
(4)這里是建立一個鏈表用來存放傳遞給內核的參數,在board_init函數中有賦值 gd->bd->bi_boot_params =
CFG_BOOT_PARAMS; CFG_BOOT_PARAMS = 0x80000000 + 0x0100 = 0x80000100(5)將commandline的值添加到鏈表中
(6)結束參數的填充
(7)啟動linux內核前的一個清除操作,主要是關閉中斷,關閉緩存等操作
(8)由前面我們知道theKernel實際指向的是kernel的入口地址,執(zhí)行這一句之后,uboot就結束了運行,kernel正式運行就從這里開始。
說明七
1.uboot 調用do_bootm_linux 中的 theKernel (0, machid, bd->bi_boot_params)進入kernel部分代碼
該函數最終會通過r0,r1,r2這三個寄存器分別把0、machid、傳遞傳參的首地址傳給kernel。
2.Kernel 的入口 在head.S中ENTRY(stext)處,此階段是匯編階段,此階段會解析r0,r1,r2(也就是uboot的傳參)最終會通過進入start_kernel,進入到c語言環(huán)境執(zhí)行。
經過前面uboot的準備工作,通過theKernel (0, machid, bd->bi_boot_params);
開始進入到kernel部分開始執(zhí)行。
其中第二個參數為機器 ID, ?第三參數為 u-boot 傳遞給內核參數存放在內存中的首地址,此處是 0x30000100
由 zImage 的生成過程我們可以知道,第一階段運行的內核映像實際就是arch/arm/boot/compressed/vmlinux,而這一階段所涉及的文件也只有三個:
(1)arch/arm/boot/compressed/vmlinux.lds
(2)arch/arm/boot/compressed/head.S
(3)arch/arm/boot/compressed/misc.c

下面我們的分析集中在 arch/arm/boot/compressed/head.S, 適當參考 vmlinux.lds 。
從linux/arch/arm/boot/compressed/vmlinux.lds文件可以看出head.S的入口地址為ENTRY(_start),也就是head.S匯編文件的_start標號開始的第一條指令。
下面從head.S中得_start 標號開始分析。(有些指令不影響初始化,暫時略去不分析)
代碼位置在/arch/arm/boot/compressed/head.S中:
start:
.type?start,#function???/*uboot跳轉到內核后執(zhí)行的第一條代碼*/
.rept?8????????????/*重復定義8次下面的指令,也就是空出中斷向量表的位置*/
mov?r0,?r0????????????/*就是nop指令*/
.endr
b?1f???????????????????@?跳轉到后面的標號1處
.word?0x016f2818?@?輔助引導程序的幻數,用來判斷鏡像是否是zImage
.word?start?@?加載運行zImage的絕對地址,start表示賦的初值
.word?_edata?@?zImage結尾地址,_edata是在vmlinux.lds.S中定義的,表示init,text,data三個段的結束位置
1:?mov?r7,?r1?@?save?architecture?ID?保存體系結構ID?用r1保存
mov?r8,?r2?@?save?atags?pointer?保存r2寄存器?參數列表,r0始終為0
mrs?r2,?cpsr?@?get?current?mode??得到當前模式
tst?r2,?#3?@?not?user?,tst實際上是相與,判斷是否處于用戶模式
bne?not_angel????????????@?如果不是處于用戶模式,就跳轉到not_angel標號處
/*如果是普通用戶模式,則通過軟中斷進入超級用戶權限模式*/
mov?r0,?#0x17?@?angel_SWIreason_EnterSVC,向SWI中傳遞參數
swi?0x123456?@?angel_SWI_ARM這個是讓用戶空間進入SVC空間
not_angel:????????????????????????????????/*表示非用戶模式,可以直接關閉中斷*/
mrs?r2,?cpsr?@?turn?off?interrupts?to?讀出cpsr寄存器的值放到r2中
orr?r2,?r2,?#0xc0?@?prevent?angel?from?running關閉中斷
msr?cpsr_c,?r2???????????@?把r2的值從新寫回到cpsr中
/*讀入地址表。因為我們的代碼可以在任何地址執(zhí)行,也就是位置無關代碼(PIC),所以我們需要加上一個偏移量。下面有每一個列表項的具體意義。
LC0是表的首項,它本身就是在此head.s中定義的
.type?LC0,?#object
LC0:?.word?LC0?@?r1?LC0表的起始位置
.word?__bss_start?@?r2?bss段的起始地址在vmlinux.lds.S中定義
.word?_end?@?r3?zImage(bss)連接的結束地址在vmlinux.lds.S中定義
.word?zreladdr?@?r4?zImage的連接地址,我們在arch/arm/mach-s3c2410/makefile.boot中定義的
.word?_start?@?r5?zImage的基地址,bootp/init.S中的_start函數,主要起傳遞參數作用
.word?_got_start?@?r6?GOT(全局偏移表)起始地址,_got_start是在compressed/vmlinux.lds.in中定義的
.word?_got_end?@?ip?GOT結束地址
.word?user_stack+4096?@?sp?用戶棧底?user_stack是緊跟在bss段的后面的,在compressed/vmlinux.lds.in中定義的
@?在本head.S的末尾定義了zImag的臨時??臻g,在這里分配了4K的空間用來做堆棧。
.section?".stack",?"w"
user_stack:?.space?4096
GOT表的初值是連接器指定的,當時程序并不知道代碼在哪個地址執(zhí)行。如果當前運行的地址已經和表上的地址不一樣,還要修正GOT表。*/
.text
adr?r0,?LC0??????????????????????????????/*把地址表的起始地址放入r0中*/
ldmia?r0,?{r1,?r2,?r3,?r4,?r5,?r6,?ip,?sp}?/*加載地址表中的所有地址到相應的寄存器*/
0是運行時地址,而r1則是鏈接時地址,而它們兩都是表示LC0表的起始位置,這樣他們兩的差則是運行和鏈接的偏移量,糾正了這個偏移量才可以運行與”地址相關的代碼“
subs?r0,?r0,?r1?@?calculate?the?delta?offset?計算偏移量,并放入r0中
beq?not_relocated?@?if?delta?is?zero,?we?are?running?at?the?address?we??were?linked?at.
@?如果為0,則不用重定位了,直接跳轉到標號not_relocated處執(zhí)行
/*
*???偏移量不為零,說明運行在不同的地址,那么需要修正幾個指針
*???r5?–?zImage基地址
*???r6?–?GOT(全局偏移表)起始地址
*???ip?–?GOT結束地址
*/
add?r5,?r5,?r0?/*加上偏移量修正zImage基地址*/
add?r6,?r6,?r0?/*加上偏移量修正GOT(全局偏移表)起始地址*/
add?ip,?ip,?r0?/*加上偏移量修正GOT(全局偏移表)結束地址*/
/*
*?這時需要修正BSS區(qū)域的指針,我們平臺適用。
*???r2?–?BSS?起始地址
*???r3?–?BSS?結束地址
*???sp?–?堆棧指針
*/
add?r2,?r2,?r0?/*加上偏移量修正BSS?起始地址*/
add?r3,?r3,?r0?/*加上偏移量修正BSS?結束地址*/
add?sp,?sp,?r0?/*加上偏移量修正堆棧指針*/
/*
*?重新定位GOT表中所有的項.
*/
1:?ldr?r1,?[r6,?#0]?@?relocate?entries?in?the?GOT
add?r1,?r1,?r0?@?table.??This?fixes?up?the
str?r1,?[r6],?#4?@?C?references.
cmp?r6,?ip
blo?1b
not_relocated:?mov?r0,?#0
1:?str?r0,?[r2],?#4?@?clear?bss?清除bss段
str?r0,?[r2],?#4
str?r0,?[r2],?#4
str?r0,?[r2],?#4
cmp?r2,?r3
blo?1b
bl?cache_on????????/*?開啟指令和數據Cache?,為了加快解壓速度*/
@?這里的?r1,r2?之間的空間為解壓縮內核程序所使用,也是傳遞給?decompress_kernel?的第二和第三的參數
mov?r1,?sp?@?malloc?space?above?stack
add?r2,?sp,?#0x10000?@?64k?max解壓縮的緩沖區(qū)
@下面程序的意義就是保證解壓地址和當前程序的地址不重疊。上面分配了64KB的空間來做解壓時的數據緩存。
/*
*???檢查是否會覆蓋內核映像本身
*???r4?=?最終解壓后的內核首地址
*???r5?=?zImage?的運行時首地址,一般為?0x30008000
*???r2?=?end?of?malloc?space分配空間的結束地址(并且處于本映像的前面)
*?基本要求:r4?>=?r2?或者?r4?+?映像長度?<=?r5
(1)vmlinux?的起始地址大于?zImage?運行時所需的最大地址(?r2?)?,?那么直接將?zImage?解壓到?vmlinux?的目標地址
cmp?r4,?r2
bhs?wont_overwrite?/*如果r4大于或等于r2的話*/
(2)zImage?的起始地址大于?vmlinux?的目標起始地址加上?vmlinux?大?。?4M?)的地址,所以將?zImage?直接解壓到?vmlinux?的目標地址
add?r0,?r4,?#4096*1024?@?4MB?largest?kernel?size
cmp?r0,?r5
bls?wont_overwrite?/*如果r4?+?映像長度?<=?r5?的話*/
@?前兩種方案通常都不成立,不會跳轉到wont_overwrite標號處,會繼續(xù)走如下分支,其解壓后的內存分配示意圖如下:
Linux內核啟動流程分析(一)【轉】-LMLPHP
mov?r5,?r2?@?decompress?after?malloc?space
mov?r0,?r5??????????/*解壓程序從分配空間后面存放?*/
mov?r3,?r7
bl?decompress_kernel
/******************************進入decompress_kernel***************************************************/
@?decompress_kernel共有4個參數,解壓的內核地址、緩存區(qū)首地址、緩存區(qū)尾地址、和芯片ID,返回解壓縮代碼的長度。
decompress_kernel(ulg?output_start,?ulg?free_mem_ptr_p,?ulg?free_mem_ptr_end_p,
int?arch_id)
{
output_data?=?(uch?*)output_start;/*?Points?to?kernel?start?*/
free_mem_ptr?=?free_mem_ptr_p;?????/*保存緩存區(qū)首地址*/
free_mem_ptr_end?=?free_mem_ptr_end_p;/*保存緩沖區(qū)結束地址*/
__machine_arch_type?=?arch_id;
arch_decomp_setup();
makecrc();?????????????????????????????/*鏡像校驗*/
putstr("Uncompressing?Linux...");
gunzip();????????????????????????????/*通過free_mem_ptr來解壓縮*/
putstr("?done,?booting?the?kernel.\n");
return?output_ptr;?????????????????????/*返回鏡像的大小*/
}
/******************************從decompress_kernel函數返回*************************************************/
add?r0,?r0,?#127?+?128
bic?r0,?r0,?#127?@?align?the?kernel?length對齊內核長度
/*
*?r0?????=?解壓后內核長度
*?r1-r3??=?未使用
*?r4?????=?真正內核執(zhí)行地址??0x30008000
*?r5?????=?臨時解壓內核Image的起始地址
*?r6?????=?處理器ID
*?r7?????=?體系結構ID
*?r8?????=?參數列表???????????????0x30000100
*?r9-r14?=?未使用
*/
@?完成了解壓縮之后,由于內核沒有解壓到正確的地址,最后必須通過代碼搬移來搬到指定的地址0x30008000。搬運過程中有
@?可能會覆蓋掉現在運行的重定位代碼,所以必須將這段代碼搬運到安全的地方,
@?這里搬運到的地址是解壓縮了的代碼的后面r5+r0的位置。
add?r1,?r5,?r0?@?end?of?decompressed?kernel?解壓內核的結束地址
adr?r2,?reloc_start
ldr?r3,?LC1?????????????@?LC1:?.word?reloc_end?-?reloc_start?表示reloc_start段代碼的大小
add?r3,?r2,?r3
1:?ldmia?r2!,?{r9?-?r14}?????@?copy?relocation?code
stmia?r1!,?{r9?-?r14}
ldmia?r2!,?{r9?-?r14}
stmia?r1!,?{r9?-?r14}
cmp?r2,?r3
blo?1b
bl?cache_clean_flush??@清?cache
ARM(add?pc,?r5,?r0)?????????????????????@?call?relocation?code?跳轉到重定位代碼開始執(zhí)行
@?在此處會調用重定位代碼reloc_start來將Image?的代碼從緩沖區(qū)r5幫運到最終的目的地r4:0x30008000處
reloc_start:?add?r9,?r5,?r0????????? 9中存放的是臨時解壓內核的末尾地址
sub?r9,?r9,?#128??????@?不拷貝堆棧
mov?r1,?r4?????? 1中存放的是目的地址0x30008000
1:
.rept?4
ldmia?r5!,?{r0,?r2,?r3,?r10?-?r14}?@?relocate?kernel
stmia?r1!,?{r0,?r2,?r3,?r10?-?r14}?/*搬運內核Image的過程*/
.endr
cmp?r5,?r9
blo?1b
mov?sp,?r1????????????????????????????/*留出堆棧的位置*/
add?sp,?sp,?#128??????????????@?relocate?the?stack
call_kernel:?bl?cache_clean_flush????@清除cache
bl?cache_off????????????@關閉cache
mov?r0,?#0?@?must?be?zero
mov?r1,?r7?@?restore?architecture?number
mov?r2,?r8?@?restore?atags?pointer
@?這里就是最終我們從zImage跳轉到Image的偉大一跳了,跳之前準備好r0,r1,r2
mov?pc,?r4?@?call?kernel
到此kernel的第一階段zImage 解壓縮階段已經執(zhí)行完。
開始第二階段
__HEAD??/*該宏定義了下面的代碼位于".head.text"段內*/
.type?stext,?%function???????????????????????????/*聲明stext為函數*/
ENTRY(stext)??????????????????????????????????????/*第二階段的入口地址*/
setmode?PSR_F_BIT?|?PSR_I_BIT?|?SVC_MODE,?r9??@?ensure?svc?mode?and?irqs?disabled?進入超級權限模式,關中斷
/*從協處理器CP15,C0讀取CPU?ID,然后在__proc_info_begin開始的段中進行查找,如果找到,則返回對應處理器相關結構體在物理地址空間的首地址到r5,最后保存在r10中*/
mrc?p15,?0,?r9,?c0,?c0??????????????????@?get?processor?id?取出cpu?id
bl?__lookup_processor_type???????????@?r5=procinfo?r9=cpuid
/**********************************************************************/?
__lookup_processor_type函數的具體解析開始(\arch\arm\kernel\?head-common.S)
/**********************************************************************/?
在講解該程序段之前先來看一些相關知識,內核所支持的每一種CPU?類型都由結構體proc_info_list來描述。?
該結構體在文件arch/arm/include/asm/procinfo.h?中定義:?
struct?proc_info_list?{?
unsigned?int?cpu_val;?
unsigned?int?cpu_mask;?
unsigned?long?__cpu_mm_mmu_flags;?/*?used?by?head.S?*/?
unsigned?long?__cpu_io_mmu_flags;?/*?used?by?head.S?*/?
unsigned?long?__cpu_flush;????????/*?used?by?head.S?*/?
const?char?*arch_name;?
const?char?*elf_name;?
unsigned?int?elf_hwcap;?
const?char?*cpu_name;?
struct?processor?*proc;?
struct?cpu_tlb_fns?*tlb;?
struct?cpu_user_fns?*user;?
struct?cpu_cache_fns?*cache;?
};?
對于?arm920?來說,其對應結構體在文件?linux/arch/arm/mm/proc-arm920.S?中初始化。?
.section?".proc.info.init",?#alloc,?#execinstr?/*定義了一個段,下面的結構體存放在該段中*/
.type?__arm920_proc_info,#object??????????????/*聲明一個結構體對象*/
__arm920_proc_info:????????????????????????????/*為該結構體賦值*/
.long?0x41009200
.long?0xff00fff0
.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?__arm920_setup
…………………………………
.section?".proc.info.init"表明了該結構在編譯后存放的位置。在鏈接文件?arch/arm/kernel/vmlinux.lds?中:?
SECTIONS?
{?
#ifdef?CONFIG_XIP_KERNEL?
.?=?XIP_VIRT_ADDR(CONFIG_XIP_PHYS_ADDR);?
#else?
.?=?PAGE_OFFSET?+?TEXT_OFFSET;?
#endif?
.text.head?:?{?
_stext?=?.;?
_sinittext?=?.;?
*(.text.head)?
}
.init?:?{?/*?Init?code?and?data?*/?
INIT_TEXT?
_einittext?=?.;?
__proc_info_begin?=?.;?
*(.proc.info.init)?
__proc_info_end?=?.;?
__arch_info_begin?=?.;?
*(.arch.info.init)?
__arch_info_end?=?.;?
__tagtable_begin?=?.;?
*(.taglist.init)?
__tagtable_end?=?.;?
………………………………?
}?
所有CPU類型對應的被初始化的?proc_info_list結構體都放在?__proc_info_begin和__proc_info_end之間。?
/?*
*?r9?=?cpuid
*??Returns:
*?r5?=?proc_info?pointer?in?physical?address?space
*?r9?=?cpuid?(preserved)
*/
__lookup_processor_type:
adr?r3,?3f?????????????????????@r3存儲的是標號?3?的物理地址(由于沒有啟用?mmu?,所以當前肯定是物理地址)?
ldmia?r3,?{r5?-?r7}??????????????@?R5=__proc_info_begin,r6=__proc_info_end,r7=標號4處的虛擬地址,即4:?.long?.?處的地址
add?r3,?r3,?#8?????????????????@?得到4處的物理地址,剛好是跳過兩條指令
sub?r3,?r3,?r7???????@?get?offset?between?virt&phys得到虛擬地址和物理地址之間的offset
???????/*利用offset?,將?r5?和?r6?中保存的虛擬地址轉變?yōu)槲锢淼刂?/
add?r5,?r5,?r3?@?convert?virt?addresses?to
add?r6,?r6,?r3?@?physical?address?space
1:?ldmia?r5,?{r3,?r4}?@?value,?mask??r3=?cpu_val?,?r4=?cpu_mask
and?r4,?r4,?r9?@?mask?wanted?bits;r9?中存放的是先前讀出的?processor?ID?,此處屏蔽不需要的位
teq?r3,?r4??????????????????????@?查看代碼和CPU?硬件是否匹配(?比如想在arm920t上運行為cortex-a8編譯的內核?不讓)
beq?2f??????????????????????????@?如果相等則跳轉到標號2處,執(zhí)行返回指令
add?r5,?r5,?#PROC_INFO_SZ?@?sizeof(proc_info_list結構的長度,在這等于48)如果沒找到,?跳到下一個proc_info_list?處
cmp?r5,?r6?????????????????????????????@?判斷是不是到了該段的結尾
blo?1b?????????????????????????????????@?如果沒有,繼續(xù)跳到標號1處,查找下一個
mov?r5,?#0????????@?unknown?processor?,如果到了結尾,沒找到匹配的,就把0賦值給r5,然后返回
2:?mov?pc,?lr?????????????????????????????@?找到后返回,r5指向找到的結構體
ENDPROC(__lookup_processor_type)
.align?2
3:?.long?__proc_info_begin
.long?__proc_info_end
4:?.long?.??????????????????????????????????@“.”表示當前這行代碼編譯連接后的虛擬地址
.long?__arch_info_begin
.long?__arch_info_end
/**********************************************************************/?
__lookup_processor_type函數的具體解析結束(\arch\arm\kernel\?head-common.S)
/**********************************************************************/?
movs?r10,?r5?????????????????????@?invalid?processor?(r5=0)?
beq?__error_p?@?yes,?error?'p'
/*機器?ID是由u-boot引導內核是通過thekernel第二個參數傳遞進來的,現在保存在r1中,在__arch_info_begin開始的段中進行查找,如果找到,則返回machine對應相關結構體在物理地址空間的首地址到r5,最后保存在r8中。
bl?__lookup_machine_type?@?r5=machinfo
/**********************************************************************/?
__lookup_machine_type函數的具體解析開始(\arch\arm\kernel\?head-common.S)
/**********************************************************************/?
每一個CPU?平臺都可能有其不一樣的結構體,描述這個平臺的結構體是?machine_desc?。?
這個結構體在文件arch/arm/include/asm/mach/arch.h?中定義:?
struct?machine_desc?{?
unsigned?int?nr;??????????/*?architecture?number?*/?
unsigned?int?phys_io;?/*?start?of?physical?io?*/?
………………………………?
};?
對于平臺smdk2410?來說其對應?machine_desc?結構在文件linux/arch/arm/mach-s3c2410/mach-smdk2410.c中初始化:?
MACHINE_START(SMDK2410,?"SMDK2410")??
.phys_io?=?S3C2410_PA_UART,?
.io_pg_offst?=?(((u32)S3C24XX_VA_UART)?>>?18)?&?0xfffc,?
.boot_params?=?S3C2410_SDRAM_PA?+?0x100,?
.map_io?=?smdk2410_map_io,?
.init_irq?=?s3c24xx_init_irq,?
.init_machine?=?smdk2410_init,?
.timer?=?&s3c24xx_timer,?
MACHINE_END?
對于宏MACHINE_START?在文件?arch/arm/include/asm/mach/arch.h?中定義:?
#define?MACHINE_START(_type,_name)?/?
static?const?struct?machine_desc?__mach_desc_##_type?/?
?__used?/?
?__attribute__((__section__(".arch.info.init")))?=?{?/?
.nr?=?MACH_TYPE_##_type,?/?
.name?=?_name,?
#define?MACHINE_END?/?
};?
__attribute__((__section__(".arch.info.init")))表明該結構體在并以后存放的位置。?
在鏈接文件?鏈接腳本文件?arch/arm/kernel/vmlinux.lds?中?
SECTIONS?
{?
#ifdef?CONFIG_XIP_KERNEL?
.?=?XIP_VIRT_ADDR(CONFIG_XIP_PHYS_ADDR);?
#else?
.?=?PAGE_OFFSET?+?TEXT_OFFSET;?
#endif?
.text.head?:?{?
_stext?=?.;?
_sinittext?=?.;?
*(.text.head)?
}
.init?:?{?/*?Init?code?and?data?*/?
INIT_TEXT?
_einittext?=?.;?
__proc_info_begin?=?.;?
*(.proc.info.init)?
__proc_info_end?=?.;?
__arch_info_begin?=?.;?
*(.arch.info.init)?
__arch_info_end?=?.;?
………………………………?
}?
在__arch_info_begin和?__arch_info_end之間存放了linux內核所支持的所有平臺對應的?machine_desc?結構體。?
/*
*??r1?=?machine?architecture?number
?*?Returns:
*??r5?=?mach_info?pointer?in?physical?address?space
?*/
__lookup_machine_type:
adr?r3,?4b??????????????????????@?把標號4處的地址放到r3寄存器里面
ldmia?r3,?{r4,?r5,?r6}????????????@?R?4?=?標號4處的虛擬地址?,r?5?=?__arch_info_begin?,r?6=?__arch_info_end
sub?r3,?r3,?r4?@?get?offset?between?virt&phys?計算出虛擬地址與物理地址的偏移
/*利用offset?,將?r5?和?r6?中保存的虛擬地址轉變?yōu)槲锢淼刂?/
add?r5,?r5,?r3?@?convert?virt?addresses?to
add?r6,?r6,?r3?@?physical?address?space
/*讀取machine_desc結構的?nr?參數,對于smdk2410?來說該值是?MACH_TYPE_SMDK2410,這個值在文件linux/arch/arm/tools/mach-types?中:
smdk2410????ARCH_SMDK2410?SMDK2410??193?*/
1:?ldr?r3,?[r5,?#MACHINFO_TYPE]?@?get?machine?type
teq?r3,?r1?@?matches?loader?number?把取到的machine?id和從uboot中傳過來的machine?id(存放r1中)相比較
beq?2f?@?found?如果相等,則跳到標號2處,返回
add?r5,?r5,?#SIZEOF_MACHINE_DESC@?next?machine_desc?沒有找到,則繼續(xù)找下一個,加上該結構體的長度
cmp?r5,?r6??????????????????????@?判斷是否已經到該段的末尾
blo?1b??????????????????????????@?如果沒有,則跳轉到標號1處,繼續(xù)查找
mov?r5,?#0?@?unknown?machine?如果已經到末尾,并且沒找到,則返回值r5寄存器賦值為0
2:?mov?pc,?lr??????????????????????@?返回原函數,且r5作為返回值
ENDPROC(__lookup_machine_type)
.align?2
3:?.long?__proc_info_begin
.long?__proc_info_end
4:?.long?.??????????????????????????????????@“.”表示當前這行代碼編譯連接后的虛擬地址
.long?__arch_info_begin
.long?__arch_info_end
/**********************************************************************/?
__lookup_machine_type函數的具體解析結束(\arch\arm\kernel\?head-common.S)
/**********************************************************************/?
movs?r8,?r5?@?invalid?machine?(r5=0)?
beq?__error_a?@?yes,?error?'a'
/*檢查?bootloader傳入的參數列表?atags?的?合法性*/
bl?__vet_atags
/**********************************************************************/?
__vet_atags函數的具體解析開始(\arch\arm\kernel\?head-common.S)
/**********************************************************************/?
關于參數鏈表:?
內核參數鏈表的格式和說明可以從內核源代碼目錄樹中的\arch\arm\include\asm\setup.h中找到,參數鏈表必須以ATAG_CORE開始,以ATAG_NONE結束。這里的?ATAG_CORE,ATAG_NONE是各個參數的標記,本身是一個32?位值,例如:?ATAG_CORE=0x54410001?。?其它的參數標記還包括:?ATAG_MEM32??,??ATAG_INITRD??,??ATAG_RAMDISK??,?ATAG_COMDLINE?等。每個參數標記就代表一個參數結構體,由各個參數結構體構成了參數鏈表。參數結構體的定義如下:???
struct?tag?{?
??????struct??tag_header??hdr;?
??????union?{?
?????????????struct?tag_core??core;?
?????????????struct?tag_mem32??mem;?
??????????struct?tag_videotext?videotext;?
??????????struct?tag_ramdisk???ramdisk;?
??????????struct?tag_initrd????initrd;?
??????????struct?tag_serialnr??serialnr;?
??????????struct?tag_revision??revision;?
??????????struct?tag_videolfb??videolfb;?
??????????struct?tag_cmdline???cmdline;?
??????????struct?tag_acorn?????acorn;?
??????????struct?tag_memclk????memclk;?
????????}?u;?
};?
參數結構體包括兩個部分,一個是?tag_header?結構體?,?一個是?u?聯合體。?
tag_header結構體的定義如下:??
struct?tag_header?{??
?????????????????u32?size;????
?????????????????u32?tag;??
};??
其中?size?:表示整個??tag??結構體的大小?(?用字的個數來表示,而不是字節(jié)的個數?)?,等于tag_header的大小加上??u?聯合體的大小,例如,參數結構體??ATAG_CORE??的size=(sizeof(tag->tag_header)+sizeof(tag->u.core))>>2,一般通過函數??tag_size(struct?*?tag_xxx)?來獲得每個參數結構體的?size?。其中??tag?:表示整個??tag??結構體的標記,如:?ATAG_CORE?等。
/*?r8??=?machinfo
*?Returns:
?*??r2?either?valid?atags?pointer,?or?zero
*/
__vet_atags:
tst?r2,?#0x3?@?aligned??r2指向該參數鏈表的起始位置,此處判斷它是否字對齊
bne?1f??????????????????????????@?如果沒有對齊,跳到標號1處直接返回,并且把r2的值賦值為0,作為返回值
ldr?r5,?[r2,?#0]?@?is?first?tag?ATAG_CORE??獲取第一個?tag?結構的?size
cmp?r5,?#ATAG_CORE_SIZE?????????@?判斷該?tag?的長度是否合法
cmpne?r5,?#ATAG_CORE_SIZE_EMPTY???
bne?1f??????????????????????????@?如果不合法,異常返回
ldr?r5,?[r2,?#4]????????????????@?獲取第一個?tag?結構體的標記
ldr?r6,?=ATAG_CORE??????????????@?取出標記ATAG_CORE的內容
cmp?r5,?r6??????????????????????@?判斷該標記是否等于ATAG_CORE
bne?1f??????????????????????????@?如果不等,異常返回
mov?pc,?lr?@?atag?pointer?is?ok,如果都相等,則正常返回
1:?mov?r2,?#0??????????????????????@?異常返回值
mov?pc,?lr?@?異常返回
ENDPROC(__vet_atags)
/**********************************************************************/?
__vet_atags函數的具體解析結束(\arch\arm\kernel\?head-common.S)
/**********************************************************************/?
/*創(chuàng)建內核初始化頁表*/
bl?__create_page_tables
/**********************************************************************/?
__create_page_tables函數的具體解析開始(\arch\arm\kernel\?head.S)
/**********************************************************************/?
/*
*?r8??=?machinfo
?*?r9??=?cpuid
?*?r10?=?procinfo
*?Returns:
*??r4?=?physical?page?table?address
?*/
/*在該文件的開頭有如下宏定義*/
#define?KERNEL_RAM_PADDR?(PHYS_OFFSET?+?TEXT_OFFSET)
.macro?pgtbl,?rd
ldr\rd,?=(KERNEL_RAM_PADDR?-?0x4000)
.endm
其中:PHYS_OFFSET在arch/arm/mach-s3c2410/include/mach/memory.h定義,為UL(0x30000000),而TEXT_OFFSET在arch/arm/Makefile中定義,為內核鏡像在內存中到內存開始位置的偏移(字節(jié)),為$(textofs-y)?textofs-y也在文件arch/arm/Makefile中定義,為textofs-y???:=?0x00008000,r4?=?30004000為臨時頁表的起始地址,首先即是初始化16K的頁表,高12位虛擬地址為頁表索引,每個頁表索引占4個字節(jié),所以為4K*4?=?16K,大頁表,每一個頁表項,映射1MB虛擬地址.
__create_page_tables:
/*為內核代碼存儲區(qū)域創(chuàng)建頁表,首先將內核起始地址-0x4000到內核起始地址之間的16K存儲器清0?,將創(chuàng)建的頁表存于此處*/?
pgtbl?r4?@?r4中存放的為頁表的基地址,最終該地址會寫入cp15的寄存器c2,這個值必須是?16K?對齊的
mov?r0,?r4??????????????????????@?把頁表的基地址存放到r0中
mov?r3,?#0??????????????????????@?把r3清0
add?r6,?r0,?#0x4000?????????????@?r6指向16K的末尾
1:?str?r3,?[r0],?#4????????????????@?把16K的頁表空間清0
str?r3,?[r0],?#4
str?r3,?[r0],?#4
str?r3,?[r0],?#4
teq?r0,?r6
bne?1b
/*從proc_info_list結構中獲取字段?__cpu_mm_mmu_flags?,該字段包含了存儲空間訪問權限等,?此處指令執(zhí)行之后r7=0x00000c1e*/
ldr?r7,?[r10,?#PROCINFO_MM_MMUFLAGS]?@?mm_mmuflags
/*為內核的第一MB創(chuàng)建一致的映射,以為打開MMU做準備,這個映射將會被paging_init()移除,這里使用程序計數器來獲得相應的段的基地址*/
mov?r6,?pc
mov?r6,?r6,?lsr?#20?@?start?of?kernel?section
orr?r3,?r7,?r6,?lsl?#20?@?flags?+?kernel?base
str?r3,?[r4,?r6,?lsl?#2]?@?identity?mapping
/*?MMU是通過?C2?中基地址(高?18?位)與虛擬地址的高?12?位組合成物理地址,在轉換表中查找地址條目。?R4?中存放的就是這個基地址?0x30004000*/?
add?r0,?r4,??#(KERNEL_START?&?0xff000000)?>>?18???@?r0?=?0x30007000?r0存放的是轉換表的起始位置
str?r3,?[r0,?#(KERNEL_START?&?0x00f00000)?>>?18]!?@?r3存放的是內核鏡像代碼段的起始地址
ldr?r6,?=(KERNEL_END?-?1)?????????????????????????@?獲取內核的尾部虛擬地址存于r6中
add?r0,?r0,?#4????????????????????????????????????@?第一個地址條目存放在?0x30007004?處,以后依次遞增
add?r6,?r4,?r6,?lsr?#18???????????????????????????@?計算最后一個地址條目存放的位置
1:?cmp?r0,?r6????????????????????????????????????????@?填充這之間的地址條目
/*每一個地址條目代表了?1MB?空間的地址映射。物理地址將從0x30100000開始映射。0X30000000?開始的?1MB?空間將在下面映射*/
add?r3,?r3,?#1?<<?20??????????????????????????????
strls?r3,?[r0],?#4
bls?1b
…………………………………
…………………………………………
/*為了使用啟動參數,將物理內存的第一MB映射到內核虛擬地址空間的第一個MB,r4存放的是頁表的地址。映射0X30000000開始的?1MB?空間PAGE_OFFSET?=?0XC0000000,PHYS_OFFSET?=?0X30000000,?r0?=??0x30007000,?上面是從?0x30007004開始存放地址條目的*/
add?r0,?r4,?#PAGE_OFFSET?>>?18
orr?r6,?r7,?#(PHYS_OFFSET?&?0xff000000)??@?r6=?0x30000c1e
.if?(PHYS_OFFSET?&?0x00f00000)
orr?r6,?r6,?#(PHYS_OFFSET?&?0x00f00000)
.endif
str?r6,?[r0]????????????????????????????@?將0x30000c1e?存于0x30007000處。
………………………
………………………………
mov?pc,?lr??????????????????????????????@子程序返回
ENDPROC(__create_page_tables)
/**********************************************************************/?
__create_page_tables函數的具體解析結束(\arch\arm\kernel\?head.S)
/**********************************************************************/?
/*把__switch_data標號處的地址放入r13寄存器,當執(zhí)行完__enable_mmu函數時會把r13寄存器的值賦值給pc,跳轉到__switch_data?處執(zhí)行*/
ldr?r13,?__switch_data?@?address?to?jump?to?after?mmu?has?been?enabled
/*把__enable_mmu函數的地址值,賦值給lr寄存器,當執(zhí)行完__arm920_setup時,返回后執(zhí)行__enable_mmu?*/
adr?lr,?BSYM(__enable_mmu)?@?return?(PIC)?address
/**********************************************************************/?
__enable_mmu函數的具體解析開始(\arch\arm\kernel\?head.S)
/**********************************************************************/?
__enable_mmu:?
#ifdef?CONFIG_ALIGNMENT_TRAP?
orr?r0,?r0,?#CR_A???//使能地址對齊錯誤檢測?
#else?
bic?r0,?r0,?#CR_A?
#endif?
#ifdef?CONFIG_CPU_DCACHE_DISABLE?
bic?r0,?r0,?#CR_C???//禁止數據?cache?
#endif?
#ifdef?CONFIG_CPU_BPREDICT_DISABLE?
bic?r0,?r0,?#CR_Z?
#endif?
#ifdef?CONFIG_CPU_ICACHE_DISABLE?
bic?r0,?r0,?#CR_I??//禁止指令?cache?
#endif?????????????//配置相應的訪問權限并存入?r5?中?
mov?r5,?#(domain_val(DOMAIN_USER,?DOMAIN_MANAGER)?|?/?
??????domain_val(DOMAIN_KERNEL,?DOMAIN_MANAGER)?|?/?
??????domain_val(DOMAIN_TABLE,?DOMAIN_MANAGER)?|?/?
??????domain_val(DOMAIN_IO,?DOMAIN_CLIENT))?
mcr?p15,?0,?r5,?c3,?c0,?0?//將訪問權限寫入協處理器?
mcr?p15,?0,?r4,?c2,?c0,?0?//將頁表基地址寫入基址寄存器?C2?,?0X30004000?
b?__turn_mmu_on??????????//跳轉到程序段去打開?MMU?
ENDPROC(__enable_mmu)?
文件linux/arch/arm/kernel/head.S?中?
__turn_mmu_on:?
mov?r0,?r0?
mcr?p15,?0,?r0,?c1,?c0,?0?//打開?MMU?同時打開?cache?等。?
mrc?p15,?0,?r3,?c0,?c0,?0?@?read?id?reg?讀取?id?寄存器?
mov?r3,?r3?
mov?r3,?r3????//兩個空操作,等待前面所取的指令得以執(zhí)行。?
mov?pc,?r13??//程序跳轉?
ENDPROC(__turn_mmu_on)?
/**********************************************************************/?
__enable_mmu函數的具體解析結束(\arch\arm\kernel\?head.S)
/**********************************************************************/?
/*執(zhí)行__arm920_setup函數(\arch\arm\mm\?proc-arm920.S),該函數完成對數據cache,指令cache,write?buffer等初始化操作*/
??ARM(?add?pc,?r10,?#PROCINFO_INITFUNC?)
/**********************************************************************/?
__arm920_setup函數的具體解析開始(\arch\arm\mm\?proc-arm920.S)
/**********************************************************************/?
在上面程序段.section?".text.head",?"ax"?的最后有這樣幾行:?
add?pc,?r10,?#PROCINFO_INITFUNC?
R10中存放的是在函數?__lookup_processor_type?中成功匹配的結構體?proc_info_list。對于arm920?來說在文件?linux/arch/arm/mm/proc-arm920.S?中有:?
.section?".proc.info.init",?#alloc,?#execinstr?
.type??__arm920_proc_info,#object?
__arm920_proc_info:?
.long?0x41009200?
.long?0xff00fff0?
.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?__arm920_setup?
………………………………?
add?pc,?r10,?#PROCINFO_INITFUNC的意思跳到函數?__arm920_setup去執(zhí)行。?
.type?__arm920_setup,?#function??//表明這是一個函數?
__arm920_setup:?
mov?r0,?#0??????????????????????//設置?r0?為?0?。?
mcr?p15,?0,?r0,?c7,?c7??????????//使數據?cahche,??指令?cache?無效。?
mcr?p15,?0,?r0,?c7,?c10,?4??????//使?write?buffer?無效。?
#ifdef?CONFIG_MMU?
mcr?p15,?0,?r0,?c8,?c7??????????//使數據?TLB,?指令?TLB?無效。?
#endif?
adr?r5,?arm920_crval????????????//獲取?arm920_crval?的地址,并存入?r5?。?
ldmia?r5,?{r5,?r6}??????????????//獲取?arm920_crval?地址處的連續(xù)?8?字節(jié)分別存入?r5,r6?。?
mrc?p15,?0,?r0,?c1,?c0??????????//獲取?CP15?下控制寄存器的值,并存入?r0?。?
bic?r0,?r0,?r5??????????????????//通過查看?arm920_crval?的值可知該行是清除?r0?中相關位,為以后對這些位的賦值做準備?
orr?r0,?r0,?r6??????????????????//設置?r0?中的相關位,即為?mmu?做相應設置。?
mov?pc,?lr??????????????????????//上面有操作?adr?lr,?__enable_mmu?,此處將跳到程序段?__enable_mmu?處。?
.size?__arm920_setup,?.?-?__arm920_setup?
.type?arm920_crval,?#object?
arm920_crval:?
crval?clear=0x00003f3f,?mmuset=0x00003135,?ucset=0x00001130?
/**********************************************************************/?
__arm920_setup函數的具體解析結束(\arch\arm\mm\?proc-arm920.S)
/**********************************************************************/?
ENDPROC(stext)
接著往下分析linux/arch/arm/kernel/head-common.S中:
.type?__switch_data,?%object??????@定義__switch_data為一個對象
__switch_data:
.long?__mmap_switched
.long?__data_loc?@?r4
.long?_data?@?r5
.long?__bss_start?@?r6
.long?_end?@?r7
.long?processor_id?@?r4
.long?__machine_arch_type?@?r5
.long?__atags_pointer?@?r6
.long?cr_alignment?@?r7
.long?init_thread_union?+?THREAD_START_SP?@?sp
/*
?*?The?following?fragment?of?code?is?executed?with?the?MMU?on?in?MMU?mode,
?*?and?uses?absolute?addresses;?this?is?not?position?independent.
*??r0??=?cp#15?control?register
?*??r1??=?machine?ID
?*??r2??=?atags?pointer
?*??r9??=?processor?ID
?*/
?/*其中上面的幾個段的定義是在文件arch/arm/kernel/vmlinux.lds?中指定*/
**********************************?vmlinux.lds開始*******************************************
?SECTIONS?
?{?
?……………………?
?#ifdef?CONFIG_XIP_KERNEL?
?__data_loc?=?ALIGN(4);?/*?location?in?binary?*/?
?.?=?PAGE_OFFSET?+?TEXT_OFFSET;?
?#else?
?.?=?ALIGN(THREAD_SIZE);?
??__data_loc?=?.;?
?#endif?
?.data?:?AT(__data_loc)?{??//此處數據存儲在上面__data_loc處。?
??_data?=?.;?/*?address?in?memory?*/??
??*(.data.init_task)?
…………………………?
.bss?:?{?
__bss_start?=?.;?/*?BSS?*/?
*(.bss)?
*(COMMON)?
_end?=?.;?
}?
………………………………?
}?
init_thread_union?是?init進程的基地址.在?arch/arm/kernel/init_task.c?中:?
union?thread_union?init_thread_union?__attribute__((__section__(".init.task")))?=?{?INIT_THREAD_INFO(init_task)?};?????????
對照?vmlnux.lds.S?中,我們可以知道init?task是存放在?.data?段的開始8k,?并且是THREAD_SIZE(8k)對齊的?*/?
**********************************?vmlinux.lds結束*******************************************
__mmap_switched:
adr?r3,?__switch_data?+?4
ldmia?r3!,?{r4,?r5,?r6,?r7}
……………………
………………………………
mov?fp,?#0?@?清除bss段
1:?cmp?r6,?r7
strcc?fp,?[r6],#4
bcc?1b
?ARM(?ldmia?r3,?{r4,?r5,?r6,?r7,?sp})??/*把__machine_arch_type變量值放入r5中,把__atags_pointer變量的值放入r6中*/
str?r9,?[r4]?@?Save?processor?ID?保存處理器id到processor_id所在的地址中
str?r1,?[r5]?@?Save?machine?type?保存machine??id到__machine_arch_type中
str?r2,?[r6]?@?Save?atags?pointer?保存參數列表首地址到__atags_pointer中
bic?r4,?r0,?#CR_A?@?Clear?'A'?bit
stmia?r7,?{r0,?r4}?@?Save?control?register?values
b?start_kernel????????????????@程序跳轉到函數?start_kernel?進入?C?語言部分。
ENDPROC(__mmap_switched)
到處我們的啟動的第二階段分析完畢。
第三階段完全是C語言代碼,從start_kernel函數開始,就先到這里吧。
注意:kernel鏡像(zImage)的前部分代碼是未經壓縮直接可以使用的,后半部分代碼由前面一小部分未壓縮的代碼解壓縮后再使用。后面會介紹解壓縮程序。
參考資料:
《深入理解嵌入式Linux設備驅動程序》
《嵌入式Linux開發(fā)實用教程》
https://blog.csdn.net/li_wen01/article/details/103353627
https://blog.csdn.net/baidu_38601468/article/details/126445751
http://blog.chinaunix.net/uid-25909619-id-3380535.html
http://blog.chinaunix.net/uid-25909619-id-3380544.html