在Linux環(huán)境下,我們想運(yùn)行一個應(yīng)用程序,在shell交互環(huán)境下直接敲命令就可以了,操作系統(tǒng)給程序提供了運(yùn)行環(huán)境和進(jìn)程管理。那Linux操作系統(tǒng)本身是如何運(yùn)行和啟動的呢?在分析之前,我們先做一個Linux內(nèi)核啟動的實(shí)驗(yàn):通過u-boot加載Linux內(nèi)核鏡像uImage到內(nèi)存不同地址,觀察Linux內(nèi)核啟動流程。
實(shí)驗(yàn)環(huán)境:
實(shí)驗(yàn)過程:
- 編譯內(nèi)核鏡像,將uImage加載地址設(shè)置為0x60003000,編譯生成uImage
- 將內(nèi)核加載到0x60003000地址,然后bootm 0x60003000
- 將內(nèi)核加載到0x60004000地址 ,然后bootm 0x60004000
通過實(shí)驗(yàn)我們可以看到:雖然 uImage 被U-boot加載到了內(nèi)存 0x60003000 和 0x60004000 內(nèi)存不同地址,但是通過U-boot的bootm命令都可以正常引導(dǎo)和啟動運(yùn)行。bootm到底有什么魔法,即使我們把鏡像文件加載到了未指定的內(nèi)存地址,也能讓Linux神奇般地啟動起來呢?要想一探究竟,還得溯本求源:從Linux內(nèi)核的編譯鏈接說起。我們從編譯Linux內(nèi)核鏡像 uImage 的Log信息為切入點(diǎn)分析:
$ make uImage LOADADDR=0x60003000
CC arch/arm/mm/mmu.o //上面省略的是編譯過程:將.c編譯為.o文件
… //前方高能預(yù)警
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內(nèi)核鏡像整個過程比較漫長,大概需要5分鐘左右,并有大量的編譯信息打印出來。前期的打印信息比較簡單,就是分別使用編譯器和匯編器將對應(yīng)的.c文件、.S文件編譯成 .o 格式可重定位目標(biāo)文件。真正高能核心的過程在最后的鏈接和鏡像文件格式處理部分,編譯信息已經(jīng)截取如上。
結(jié)合編譯信息和上面的編譯流程圖我們可以看到,編譯器將所有的源文件編譯成對應(yīng)的目標(biāo)文件后,接下來就是鏈接過程:將所有的目標(biāo)文件鏈接成ELF格式的可執(zhí)行文件:vmlinux。ELF文件格式是Linux環(huán)境下的可執(zhí)行文件格式,無論是 gcc 還是 arm-linux-gcc 編譯器,生成的都是ELF這種格式的文件。在Linux環(huán)境下,加載器根據(jù)ELF文件里的地址信息,就可以把它加載到內(nèi)存指定的地址運(yùn)行,但是系統(tǒng)啟動過程中并沒有ELF文件的執(zhí)行環(huán)境,需要將ELF文件轉(zhuǎn)換為二進(jìn)制純指令文件。編譯器接著會調(diào)用objdump命令刪除不必要的section,只保留代碼段、數(shù)據(jù)段等必要的section,將ELF格式的vmlinux文件轉(zhuǎn)換為原始的二進(jìn)制內(nèi)核鏡像Image。Image可以在裸機(jī)環(huán)境下運(yùn)行,體積也比較大,我們可以使用gzip工具對其進(jìn)行壓縮,生成piggz.gzip壓縮的二進(jìn)制內(nèi)核鏡像。這樣做的好處是可提高程序的啟動速度:因?yàn)閮?nèi)核加載運(yùn)行時,從Flash 上讀取鏡像的速度是很慢的,我們通過先壓縮,加載到內(nèi)存后再解壓這種操作,不僅可以節(jié)省Flash存儲空間(尤其是Nor Flash還是很貴的),還可以節(jié)省了鏡像的加載時間。
因?yàn)閜iggz.gzip是壓縮文件無法運(yùn)行,所以我們還需要給它鏈接上一段解壓縮代碼。鏈接器只能處理ELF格式的目標(biāo)文件,因此在鏈接之前,要先將壓縮文件piggz.gzip轉(zhuǎn)換為可重定位的目標(biāo)文件:piggy.gzip.o。在ARM平臺下,解壓縮代碼是在arch/arm/boot/compressed/目錄下面的head.o、misc.o、 decompress.o,這部分使用 -fpic 參數(shù)編譯生成的指令是與位置無關(guān)的,放到哪里都可以執(zhí)行,它們通過鏈接器與piggy.gzip.o一起組裝成新的ELF文件vmlinux,然后再使用objcopy工具轉(zhuǎn)換為純二進(jìn)制鏡像zImage,就可以直接燒寫到Nor或nand flash上,隨系統(tǒng)啟動后加載到內(nèi)存運(yùn)行了。
不同的嵌入式系統(tǒng)平臺可能會使用不同的BootLoader來加載Linux內(nèi)核鏡像的運(yùn)行,常見的BootLoader有U-boot、vivi、g-bios等。使用U-boot的嵌入式平臺通常會對zImage進(jìn)一步轉(zhuǎn)換,給它添加一個64字節(jié)的數(shù)據(jù)頭,用來記錄鏡像文件的加載地址、入口地址、文件大小、CPU架構(gòu)等信息。我們可以使用U-boot提供的mkimage工具將zImage鏡像轉(zhuǎn)換為uImage:
$ mkimage –A arm -O linux –T kernel –C none –a 0x60003000
–e 0x60003000 -d zImage uImage
mkimage工具常見的參數(shù)說明如下:
- -A:指定CPU架構(gòu)類型
- -O:指定操作系統(tǒng)類型
- -T:指定image類型
- -C:采用的壓縮方式:none、gzip、bzip2等
- -a:內(nèi)核加載地址
- -e:內(nèi)核鏡像入口地址
走到這一步,U-boot可以引導(dǎo)的uImage內(nèi)核鏡像生成,這個Linux內(nèi)核鏡像編譯就完美結(jié)束了。接下來我們繼續(xù)分析U-boot是如何加載uImage運(yùn)行的:
U-boot加載的 dtb 文件和 bootargs 這里暫不考慮,我們重點(diǎn)關(guān)注uImage:當(dāng)uImage被加載到內(nèi)存不同的位置時,為什么都可以正常啟動。我們先考慮上面的第一種情況,當(dāng)加載到內(nèi)存中的地址等于編譯時指定的地址時:
U-boot提供的bootm機(jī)制用來啟動內(nèi)核的運(yùn)行。bootm會解析uImage文件64字節(jié)的數(shù)據(jù)頭,解析出指定的加載地址,并跟自己的參數(shù)進(jìn)行對比:若發(fā)現(xiàn)bootm參數(shù)地址和編譯時-a指定的加載地址0x60003000相同,就會直接跳過數(shù)據(jù)頭,跳到zImage的入口地址0x60003040執(zhí)行。
如果bootm發(fā)現(xiàn)自己的參數(shù)地址跟-a指定的加載地址0x60003000不同時,它會將去掉64個字節(jié)數(shù)據(jù)頭的內(nèi)核鏡像zImage復(fù)制到編譯時 -a 指定的加載地址處,然后再跳到該地址處執(zhí)行。如上圖所示,zImage鏡像被加載到了編譯時指定的0x60003000地址處,然后跳過來,就可以直接執(zhí)行zImage了。
zImage是一個壓縮文件,在運(yùn)行之前要先解出真正要執(zhí)行的內(nèi)核鏡像Image,然后才能跳到內(nèi)核鏡像真正的入口處去啟動Linux內(nèi)核。解壓縮代碼head.o、decompress.o是一段與位置無關(guān)的代碼,放到內(nèi)存的任何位置都可以運(yùn)行。大家有興趣可以做一個實(shí)驗(yàn),使用U-boot的bootz命令直接引導(dǎo)內(nèi)核鏡像zImage運(yùn)行:將zImage加載到內(nèi)存的不同地址,你會發(fā)現(xiàn)zImage都可以正常啟動。
解壓縮代碼的主要作用就是將從zImage文件出解壓出真正的內(nèi)核鏡像Image,并將其重定位到Image內(nèi)核編譯時指定的鏈接地址0x80008000上。Linux運(yùn)行使用的是虛擬地址,需要CPU硬件管理單元MMU的支持,MMU會將虛擬地址轉(zhuǎn)換為對應(yīng)的物理地址。在ARM vexpress平臺上,內(nèi)核的鏈接地址0x80008000會映射到物理內(nèi)存0x60008000的地方。zImage的解壓縮代碼會將Image解壓到0x60008000處,然后跳過去就可以直接啟動Linux內(nèi)核了。
在zImage運(yùn)行解壓縮代碼的過程中會遇到這么一種情況:zImage自身剛好占據(jù)了0x60008000這片地址空間,那么當(dāng)zImage的重定位代碼將解壓出來的Image拷貝到指定的0x60008000處時,可能就會沖掉自身正在運(yùn)行的代碼。為了避免這種情況發(fā)生,zImage會將這部分重定位拷貝到一個安全的地方,比如Image的后面,然后再跳到這片重定位代碼處執(zhí)行,這樣就可以將Image鏡像安全地拷貝到0x60008000地址上了。
拷貝成功后,就可以直接跳到 0x60008000 地址去運(yùn)行Linux內(nèi)核真正的代碼了。因?yàn)镮mage鏡像鏈接時使用的是虛擬地址,所以在運(yùn)行Linux內(nèi)核的C語言函數(shù)之前,首先會有一段匯編代碼用來初始化堆棧環(huán)境,使能MMU。代碼跟蹤就不具體分析了,有興趣大家可以去看視頻教程:《C語言嵌入式Linux高級編程》第3期:程序的編譯、鏈接和運(yùn)行,或者參考下面的提示自行分析:
- 運(yùn)行入口:arch/arm/kernel/head.S
- 使能MMU:__create_page_tables
- 跳入C語言函數(shù):__mmap_switched/start_kernel
-
Linux
+關(guān)注
關(guān)注
87文章
11329瀏覽量
209969 -
應(yīng)用程序
+關(guān)注
關(guān)注
37文章
3285瀏覽量
57785 -
Shell
+關(guān)注
關(guān)注
1文章
366瀏覽量
23428
發(fā)布評論請先 登錄
相關(guān)推薦
評論