在Linux環境下,我們想運行一個應用程序,在shell交互環境下直接敲命令就可以了,操作系統給程序提供了運行環境和進程管理。那Linux操作系統本身是如何運行和啟動的呢?在分析之前,我們先做一個Linux內核啟動的實驗:通過u-boot加載Linux內核鏡像uImage到內存不同地址,觀察Linux內核啟動流程。
實驗環境:
實驗過程:
- 編譯內核鏡像,將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
-
Linux
+關注
關注
87文章
11345瀏覽量
210401 -
應用程序
+關注
關注
38文章
3292瀏覽量
57917 -
Shell
+關注
關注
1文章
366瀏覽量
23448
發布評論請先 登錄
相關推薦
評論