跟濤哥一起學習嵌入式 32:Linux內核編譯和啓動分析

在Linux環境下,我們想運行一個應用程序,在shell交互環境下直接敲命令就可以了,操作系統給程序提供了運行環境和進程管理。那Linux操作系統本身是如何運行和啓動的呢?在分析之前,我們先做一個Linux內核啓動的實驗:通過u-boot加載Linux內核鏡像uImage到內存不同地址,觀察Linux內核啓動流程。

實驗環境:

  • 硬件平臺:使用 QEMU 仿真ARM vexpress A9 開發板
  • RAM大小配置:512 MB
  • RAM內存地址:0x60000000 ~ 0x7FFFFFFF

實驗過程:

  • 編譯內核鏡像,將uImage加載地址設置爲0x60003000,編譯生成uImage
  • 將內核加載到0x60003000地址,然後bootm 0x60003000
  • 將內核加載到0x60004000地址 ,然後bootm 0x60004000

通過實驗我們可以看到:雖然 uImage 被U-boot加載到了內存 0x60003000 和 0x60004000 內存不同地址,但是通過U-boot的bootm命令都可以正常引導和啓動運行。bootm到底有什麼魔法,即使我們把鏡像文件加載到了未指定的內存地址,也能讓Linux神奇般地啓動起來呢?要想一探究竟,還得溯本求源:從Linux內核的編譯鏈接說起。我們從編譯Linux內核鏡像 uImage 的Log信息爲切入點分析:

$ make uImage LOADADDR=0x60003000
  CC arch/arm/mm/mmu.o //上面省略的是編譯過程:將.c編譯爲.o文件
  …                    //前方高能預警
  LD      vmlinux
  SYSMAP  System.map
  OBJCOPY arch/arm/boot/Image
  Kernel: arch/arm/boot/Image is ready
  Kernel: arch/arm/boot/Image is ready
  LDS     arch/arm/boot/compressed/vmlinux.lds
  AS      arch/arm/boot/compressed/head.o
  GZIP    arch/arm/boot/compressed/piggy.gzip
  AS      arch/arm/boot/compressed/piggy.gzip.o
  CC      arch/arm/boot/compressed/misc.o
  CC      arch/arm/boot/compressed/decompress.o
  LD      arch/arm/boot/compressed/vmlinux
  OBJCOPY arch/arm/boot/zImage
  Kernel: arch/arm/boot/zImage is ready
  Kernel: arch/arm/boot/Image is ready
  Kernel: arch/arm/boot/zImage is ready
  UIMAGE  arch/arm/boot/uImage
Image Name:   Linux-4.4.0+
Created:      Fri Apr 24 19:11:09 2020
Image Type:   ARM Linux Kernel Image (uncompressed)
Data Size:    3460776 Bytes = 3379.66 kB = 3.30 MB
Load Address: 60003000
Entry Point:  60003000
Image arch/arm/boot/uImage is ready

編譯Linux內核鏡像整個過程比較漫長,大概需要5分鐘左右,並有大量的編譯信息打印出來。前期的打印信息比較簡單,就是分別使用編譯器和彙編器將對應的.c文件、.S文件編譯成 .o 格式可重定位目標文件。真正高能核心的過程在最後的鏈接和鏡像文件格式處理部分,編譯信息已經截取如上。

結合編譯信息和上面的編譯流程圖我們可以看到,編譯器將所有的源文件編譯成對應的目標文件後,接下來就是鏈接過程:將所有的目標文件鏈接成ELF格式的可執行文件:vmlinux。ELF文件格式是Linux環境下的可執行文件格式,無論是 gcc 還是 arm-linux-gcc 編譯器,生成的都是ELF這種格式的文件。在Linux環境下,加載器根據ELF文件裏的地址信息,就可以把它加載到內存指定的地址運行,但是系統啓動過程中並沒有ELF文件的執行環境,需要將ELF文件轉換爲二進制純指令文件。編譯器接着會調用objdump命令刪除不必要的section,只保留代碼段、數據段等必要的section,將ELF格式的vmlinux文件轉換爲原始的二進制內核鏡像Image。Image可以在裸機環境下運行,體積也比較大,我們可以使用gzip工具對其進行壓縮,生成piggz.gzip壓縮的二進制內核鏡像。這樣做的好處是可提高程序的啓動速度:因爲內核加載運行時,從Flash 上讀取鏡像的速度是很慢的,我們通過先壓縮,加載到內存後再解壓這種操作,不僅可以節省Flash存儲空間(尤其是Nor Flash還是很貴的),還可以節省了鏡像的加載時間。

因爲piggz.gzip是壓縮文件無法運行,所以我們還需要給它鏈接上一段解壓縮代碼。鏈接器只能處理ELF格式的目標文件,因此在鏈接之前,要先將壓縮文件piggz.gzip轉換爲可重定位的目標文件:piggy.gzip.o。在ARM平臺下,解壓縮代碼是在arch/arm/boot/compressed/目錄下面的head.o、misc.o、 decompress.o,這部分使用 -fpic 參數編譯生成的指令是與位置無關的,放到哪裏都可以執行,它們通過鏈接器與piggy.gzip.o一起組裝成新的ELF文件vmlinux,然後再使用objcopy工具轉換爲純二進制鏡像zImage,就可以直接燒寫到Nor或nand flash上,隨系統啓動後加載到內存運行了。

不同的嵌入式系統平臺可能會使用不同的BootLoader來加載Linux內核鏡像的運行,常見的BootLoader有U-boot、vivi、g-bios等。使用U-boot的嵌入式平臺通常會對zImage進一步轉換,給它添加一個64字節的數據頭,用來記錄鏡像文件的加載地址、入口地址、文件大小、CPU架構等信息。我們可以使用U-boot提供的mkimage工具將zImage鏡像轉換爲uImage:

$ mkimage –A arm  -O linux –T kernel –C none –a 0x60003000  –e 0x60003000  -d zImage   uImage

mkimage工具常見的參數說明如下:

  • -A:指定CPU架構類型
  • -O:指定操作系統類型
  • -T:指定image類型
  • -C:採用的壓縮方式:none、gzip、bzip2等
  • -a:內核加載地址
  • -e:內核鏡像入口地址

走到這一步,U-boot可以引導的uImage內核鏡像生成,這個Linux內核鏡像編譯就完美結束了。接下來我們繼續分析U-boot是如何加載uImage運行的:

U-boot加載的 dtb 文件和 bootargs 這裏暫不考慮,我們重點關注uImage:當uImage被加載到內存不同的位置時,爲什麼都可以正常啓動。我們先考慮上面的第一種情況,當加載到內存中的地址等於編譯時指定的地址時:

U-boot提供的bootm機制用來啓動內核的運行。bootm會解析uImage文件64字節的數據頭,解析出指定的加載地址,並跟自己的參數進行對比:若發現bootm參數地址和編譯時-a指定的加載地址0x60003000相同,就會直接跳過數據頭,跳到zImage的入口地址0x60003040執行。

如果bootm發現自己的參數地址跟-a指定的加載地址0x60003000不同時,它會將去掉64個字節數據頭的內核鏡像zImage複製到編譯時 -a 指定的加載地址處,然後再跳到該地址處執行。如上圖所示,zImage鏡像被加載到了編譯時指定的0x60003000地址處,然後跳過來,就可以直接執行zImage了。

zImage是一個壓縮文件,在運行之前要先解出真正要執行的內核鏡像Image,然後才能跳到內核鏡像真正的入口處去啓動Linux內核。解壓縮代碼head.o、decompress.o是一段與位置無關的代碼,放到內存的任何位置都可以運行。大家有興趣可以做一個實驗,使用U-boot的bootz命令直接引導內核鏡像zImage運行:將zImage加載到內存的不同地址,你會發現zImage都可以正常啓動。

解壓縮代碼的主要作用就是將從zImage文件出解壓出真正的內核鏡像Image,並將其重定位到Image內核編譯時指定的鏈接地址0x80008000上。Linux運行使用的是虛擬地址,需要CPU硬件管理單元MMU的支持,MMU會將虛擬地址轉換爲對應的物理地址。在ARM vexpress平臺上,內核的鏈接地址0x80008000會映射到物理內存0x60008000的地方。zImage的解壓縮代碼會將Image解壓到0x60008000處,然後跳過去就可以直接啓動Linux內核了。

在zImage運行解壓縮代碼的過程中會遇到這麼一種情況:zImage自身剛好佔據了0x60008000這片地址空間,那麼當zImage的重定位代碼將解壓出來的Image拷貝到指定的0x60008000處時,可能就會沖掉自身正在運行的代碼。爲了避免這種情況發生,zImage會將這部分重定位拷貝到一個安全的地方,比如Image的後面,然後再跳到這片重定位代碼處執行,這樣就可以將Image鏡像安全地拷貝到0x60008000地址上了。

拷貝成功後,就可以直接跳到 0x60008000 地址去運行Linux內核真正的代碼了。因爲Image鏡像鏈接時使用的是虛擬地址,所以在運行Linux內核的C語言函數之前,首先會有一段彙編代碼用來初始化堆棧環境,使能MMU。代碼跟蹤就不具體分析了,有興趣大家可以去看視頻教程:《C語言嵌入式Linux高級編程》第3期:程序的編譯、鏈接和運行

或者參考下面的提示自行分析:

  • 運行入口:arch/arm/kernel/head.S
  • 使能MMU:__create_page_tables
  • 跳入C語言函數:__mmap_switched/start_kernel

宅學部落官方微信羣開通了,嵌入式技術分享、交流。有興趣加入請先加微信:brotau,然後拉你進羣。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章