轉自:https://blog.csdn.net/shichaog/article/details/40218763
Linux系統啟動那些事—基於Linux 3.10內核
csdn 我的空間的下載地址 ,有些做的效果網頁上沒了,感興趣建議去上面地址下載pdf版的
下載地址 http://download.csdn.net/detail/shichaog/8054005 shichaog@126.com
啟動流程概述
也許你會好奇Linux是如何啟動的?本文圍繞Linux如何啟動起來展開一些介紹。本文涉及grub、uboot、initrd、根文件系統、設備樹、以及Linux內核編譯等內容。
對那些好奇系統是如何啟動的人本文非常適合,當然對於由於涉及操作系統的方方面面,bsp的開發人員也有點價值,但是這里沒有對應用做介紹;本文討論兩種平台下的啟動方式,因為它們均是對應體系架構下的典型。
1、 通用的PC平台架構(X86)
2、 嵌入式平台架構(ARM,S3C2440)
本文可能一些實驗性的操作,如grub、內核編譯等,建議這些動手也做一下,能夠加深對這一過程的理解。
對於存儲器、磁盤、CPU具有一定的了解會比較好,不過這不是這里的重點,並且會稍微提及一下,所以不必擔心成為障礙。 對於uboot、BIOS、grub、vmlinuz、initrd有點認識會更好,但是對操作系統是需要有個概念性的認識,很多基本概念性的沒有在這里講解了。當然,如果你還不會C語言,那就別向下看了,趕緊學學C吧,有點匯編、編譯、鏈接的知識會更好,對Makefile、Kconfig、shell如果也知道那就更好。
PC下我的Linux系統:
Fedora9, 桌面版 內核版本2.6.25;VMware9下虛擬機。
www.kerenel.org下載Linux-3.10.0,這里將使用Linux-3.10.0升級Fedora 9內核。
所有的故事從升級Linux內核開始,首先,讓我們來配置Linux內核,解壓Linux內核,我解壓的目錄為/usr/src/下,不過我可不建議你解壓到該目錄,Linux內核的Header文件鏈接到該目錄下,盡管該版本下不會帶來大的問題,但是建議不要“污染”了這個目錄。
步驟一:然后使用menuconfig,生成配置文件。
cd/usr/src/linux-3.X
步驟二:編譯內核目標文件。
make {O=/home/name/build/kernel}menuconfig
步驟三:編譯module和內核。
make {O=/home/name/build/kernel}
步驟四:安裝module和內核以及啟動必備文件
sudo make {O=/home/name/build/kernel}modules_install install
經過如上四個步驟,以root權限reboot系統,進入系統,如果在編譯和鏈接時想看看詳細的執行過程可以在步驟三中使用makeV=1 all該命令讓編譯時輸出更多的過程信息。
后面會將啟動代碼和啟動輸出的信息進行關聯分析,這樣的認識會更深刻些;如果您手頭資源有限或者時間等原因沒能完成這個步驟,那么還有一個補救措施,筆者將上述的2.6和3.10內核的兩個版本的啟動顯示信息錄制了一個視頻,該視頻下載網址:XXX
如果您沒有自己升級,那么我強烈建議您看一下,總時間不到十分鍾,您就可以看完2.6和3.10兩個版本啟動的過程了。編譯命令中大括號里的內容可選,這里就是輸出路徑設置,可以不關心,這和啟動沒有關系,升級完該放下它,后面會再提及它的,bios可能大家都聽說過,PC啟動的最先代碼執行的就是這里的代碼,第二個執行的代碼是grub或者lilo,grub會將initrd和內核拷貝到內存中,然后進行解壓並執行內核。
首先看一下在2.6內核版本下啟動時和Linux相關的一些文件。
[root@shichaog boot]# ls -lt
total 6282
drwxr-xr-x 2 root root 1024 2012-09-18 04:34 grub
drwxr-xr-x 3 root root 1024 2012-09-18 04:06 efi
drwx------ 2 root root 12288 2012-09-18 03:55 lost+found
-rw------- 1 root root 3312447 2012-09-1720:52 initrd-2.6.25-14.fc9.i686.img
-rw-r--r-- 1 root root 86348 2008-05-01 18:34config-2.6.25-14.fc9.i686
-rw-r--r-- 1 root root 892575 2008-05-01 18:34System.map-2.6.25-14.fc9.i686
-rwxr-xr-x 1 root root 2088288 2008-05-0118:34 vmlinuz-2.6.25-14.fc9.i686
grub啟動加載程序,當系統按下電源鍵開機時,第一條指令對應的邏輯地址(段:偏移)FFFF:0000,也就是物理地址的FFFF0H,這個在8086時代就這么決定了,這個地址一般存放第一條BIOS的指令,這條指令一般又是個長跳轉指令,因為8086時代地址線只有20根,所以尋址只能是1M范圍內的空間,而到了酷睿或者i5系列早已是32位或者64位了,所以Bios的大小和物理地址都進行了擴充,所以該指令一般跳轉到bios執行指令。Bios存放在ROM中掉電不會失去。
efi bios的升級方案, 您可以將其理解為功能和bios差不多,但是運行的是32為指令並且地址有了突破,此外和pc操作系統的接口也有一點區別就可以了。這里還是以bios為主。
lost+found存放修復或者損壞的文件,正常的啟動過程用不到。
Initrd 這個是initial RAM disk的簡稱,系統在運行時建立在一個存儲系統上的,initrd使用軟件模擬磁盤系統,它就是個文件系統,一般的嵌入式系統的文件系統就是它,但是在pc環境下initrd只在啟動過程中起作用。
config文件內核的配置,和make menuconfig生成的文件一樣,是關於當前系統的配置情況,如處理器類型、mmu、cgroup功能是否支持等。
system.map,這個文件是內核映像文件vmlinux使用nm導出的符號表,vmlinux是ELF文件格式,含有一些信息,nm命令導出該文件的信息,該文件對於分析內核映像文件還是有一定幫助的。vmlinuz,加工過的vmlinux,加工內容包括壓縮、去除ELF文件信息,並且添加了bst(bootstrap)部分代碼。
這里也看一下升級后boot目錄下的文件內容,注意黃色加深部分,至於沒有着色的部分也許你也看到了系統有兩個,是的,沒看錯,這個之前我也編譯並安裝過另一個2.6.25版本的內核了。
drwxr-xr-x 2 root root 1024 2014-08-17 14:43 grub
-rw------- 1 root root 3377030 2014-08-17 14:43 initrd-3.10.0.img
lrwxrwxrwx 1 root root 232014-08-17 14:40 System.map -> /boot/System.map-3.10.0
-rw-r--r-- 1 root root 1398825 2014-08-17 14:40 System.map-3.10.0
lrwxrwxrwx 1 root root 202014-08-17 14:40 vmlinuz -> /boot/vmlinuz-3.10.0
-rw-r--r-- 1 root root 3012160 2014-08-17 14:40 vmlinuz-3.10.0
drwxr-xr-x 3 root root 1024 2014-07-23 04:53 efi
drwx------ 2 root root 12288 2014-07-23 04:45 lost+found
-rw------- 1 root root 3311461 2014-07-2221:55 initrd-2.6.25-14.fc9.i686.img
-rw-r--r-- 1 root root 86348 2008-05-01 18:34config-2.6.25-14.fc9.i686
-rw-r--r-- 1 root root 892575 2008-05-01 18:34System.map-2.6.25-14.fc9.i686
-rwxr-xr-x 1 root root 2088288 2008-05-0118:34 vmlinuz-2.6.25-14.fc9.i686
-rwxr-xr-x 1 root root 822228 2008-04-26 01:25xen-syms-2.6.25-2.fc9.i686.xen
-rw-r--r-- 1 root root 86316 2008-04-26 01:18config-2.6.25-2.fc9.i686.xen
-rw-r--r-- 1 root root 907057 2008-04-26 01:18System.map-2.6.25-2.fc9.i686.xen
-rwxr-xr-x 1 root root 2495757 2008-04-2601:18 vmlinuz-2.6.25-2.fc9.i686.xen
-rw-r--r-- 1 root root 373850 2008-04-26 01:10xen.gz-2.6.25-2.fc9.i686.xen
PC啟動流程簡介
對於X86PC系統上電后,會執行特定地址上的一條指令,而目前這條指令就是一個長跳轉指令,該指令跳轉至真正的bios入口地址執行bios,bios程序會進行加電自檢(POST),其次會本地設備初始化,並按照啟動順序搜索可以引導的設備,這里我們從硬盤引導我們的系統,這是bios將硬盤的0磁道0柱面1扇區的內容拷貝至內存中運行,之所以拷貝至內存中,因為內存的存儲速率遠遠高於ROM或者硬盤,不過最近的固態盤速率提高的很快,好多pc開始采用固態盤(SSD)作為PC的主存儲器了。一個扇區是512字節,當這些512字節拷貝至內存后,bios會將跳轉至該內存里的程序(grub)繼續執行,grub將負責實際的內核加載,即將內核加載到內存中,然后將控制權交給內核,內核會解壓縮自身的內核映像到特定的地址,然后運行該內核,內核會啟動分頁機制,內存管理、設備初始化等一些的任務,執行init進程,創建三個內核線程,其中一個線程會創建內核守護進程如swap進程,還有一個線程會啟動shell進程,提供操作系統登錄這登錄。
Bios會將0磁道0柱面1扇區(sector)的512字節內容(grub stage1)拷貝至0x7c00所在的物理內存處,並且將控制權交給grub,該512B中只有446B是引導代碼,剩下的是分區表信息。grub的第二個階段代碼可以可以跨越一個或者多個sector。所以實際上grub大部分的工作是由stage2來完成的,其會被stage1 階段的代碼拷貝至內存運行。
圖1.1.1 Linux PC(X86)啟動流程
先來看看grub的配置文件的內容(/boot/grub/grub.conf)【該版本的grub是0.97,有stage1.5,grub2部分見2.1節】:
…
title Fedora (3.10.0)
root (hd0,0)
kernel /vmlinuz-3.10.0 ro root=/dev/VolGroup00/LogVol00 rhgb quiet
initrd /initrd-3.10.0.img
title Fedora (2.6.25-14.fc9.i686)
root (hd0,0)
kernel /vmlinuz-2.6.25-14.fc9.i686 ro root=UUID=aeca4624-c3e9-45af-b01b-125b2bcbec7arhgb quiet
initrd /initrd-2.6.25-14.fc9.i686.img
root (hd0,0) root系統文件目錄,第一塊硬盤的第一個分區(主分區)。
kernel /vmlinuz-3.10.0 內核映像文件
ro只讀
root=/dev/VolGroup00/LogVol00 根分區用戶
rhgb quiet 圖形redhat graphic boot ,不顯示dmesg信息
initrd 啟動時的文件系統。
device.map文件(boot/grub/):
(hd0) /dev/sda
PC的1M內容如下:Documentation/x86/boot.txt
圖1.1.2內存布局
grub執行完畢后就會將cpu轉交給Linux內核,內核的bzImage格式是經過壓縮的內核格式,首先進行解壓縮,然后才是真正意義上的啟動內核。這一解壓縮和啟動的過程對應於下圖所示。
這一段信息中Decompressing Linux… done意味着內核剛解壓完畢,緊接着啟動內核,即圖中顯示的Booting the kernel。
圖1.1.3內核解壓縮
在Linux內核booting后期,會調用start_kernel()函數,該函數會創建內核守護進程, init進程,內核守護進程用於例行性管理類任務,如swapd等, /sbin/init會讀取/etc/inittab,根據不同的運行級別去初始化相關的服務。
</etc/inittab>
# Default runlevel. The runlevels used are:
# 0- halt (Do NOT set initdefault to this)
# 1- Single user mode
# 2- Multiuser, without NFS (The same as 3, if you do not have networking)
# 3- Full multiuser mode
# 4- unused
# 5- X11
# 6- reboot (Do NOT set initdefault to this)
#
id:5:initdefault:
接下來init進程會調用/etc/rc.sysinit腳本初始化很多換環境參數,如PATH、網絡的設定等。加載內核模塊。同樣是依賴於inittab中的運行級別,執行對應運行級別初始化腳本(/etc/rc0.d~/etc/rc6.d目錄下),最后還有一個/etc/rc.local目錄下腳本也會運行,這里用戶可以定制一些自己的服務在里面,這樣每次系統啟動就可以運行一些自己定制化的東東。至此所有的准備工作已經做好了,就是讓用戶登錄了,調用腳本/bin/login,給出用戶登錄界面。登錄進shell。
這一過程直觀上的感覺就是啟動時下面會出現的啟動過程顯示1.1.4圖,這是一張啟動截屏圖。
圖1.1.4:啟動過程截圖
1.1.5運行腳本rc5.d目錄下內容。
圖中划橫線的部分就是在啟動過程顯示的服務的由來。至此整個過程有了清楚的認識了。
http://www.linuxhomenetworking.com/wiki/index.php/Quick_HOWTO_:_Ch07_:_The_Linux_Boot_Process#.VCa1mOOSwU0
ARM啟動流程簡介
對於嵌入式平台ARM平台,說說其NANDFlash的啟動過程,請先看圖2.2描述的NAND flash中的程序布局,上電時,首先cpu會自動將自動從NAND flash中拷貝一定代碼到內存中執行,這是任何支持nand方式啟動必須支持的,一般我見到的有2K還有4K的,這部分的代碼我們將其稱為bootstrap,這個有點類似MBR中的執行代碼,那部分代碼是grub的stage1代碼,bootstrap然后會拷貝bootloader到內存中,這個就類似PC中的grub的stage2,這部分代碼大小是有限的,遇到過32K,128K的bootloade。Bootstrap會將32K或者128K的空間里的東西拷貝到內存中而不管實際的bootloader大小,然后將控制權移交給bootloader,相比bootstrap而言,bootloader發揮的空間較大了,它會讀取一個叫ptb里的內容,這里存的是分區信息,根據nand的存儲特點記錄app,kernel…bootstrap的大小,其實各部分的block,page的信息,然后bootloader將這些內容統統拷貝到內存中,至於拷貝到內存中的地址各個平台的差異性就比較大了,不像PC加載內核到內存中的地址是固定的。
圖1.2.1嵌入式ARM啟動流程
Uboot主要還是為kernel服務的,所以它准備好外部環境后將控制權交給kernel,其實轉交過程就是jump,並且會將相關的信息傳遞給內核,比如設備樹表的地址,kernel部分的代碼然后開始執行,主要系統初始化工作還是在startkernel中完成的,例如會解析命令,然后還有一個重要動作,那就是解析設備樹,並使用一個鏈表將其串接在一塊,在后續驅動注冊的probe方法中會用到這個鏈表,3.10的內核是這么處理設備的。接着一系列的初始化,初始化的工作交給一個內核線程完成,該線程會執行/sbin/init的內容,這個內容就涉及到根文件系統了,在PC情況下就是initrd了,ARM下initramfs了,這里需要說明的是init並不屬於內核,為了保持內核的精簡,底層服務搭建好了以后,初始化任務還是交給了用戶空間去完成了,最后啟動shell,至此PC下的任務完成了,嵌入式下會把需要的應用,即app里的內容放在init里去加載,或者init下調用另一個去加載。至此整個加載過程就完畢了。
圖1.2.2NAND Flash中所有的布局
本文會講解兩種架構下的啟動流程,各個階段對用的概念是一樣的,如grub等價於uboot,PC下的initrd對應於嵌入式的根文件系統等,到Linux啟動時,他們的代碼概念上都是一樣的,如建立頁表、中斷等。
MBR& grub
Linux內核啟動時的一些信息就是MBR和bootloader給的,那么就先將講的MBR的分區信息吧,bootloader和內核都會依賴分區,因為分區信息指明了代碼在存儲介質上的分布信息,一些操作依賴於該分區信息,比如,內核在哪里,一般在/boot分區,這些分區信息在安裝操作系統格式化磁盤時就會存在,或者新的硬盤的格式化,也會有分區信息存在里面。這里使用的兩個命令查看了硬盤的分區情況,在MBR中有該分區信息。
圖2.1.1 分區信息
由fdisk命令可以知道系統共兩個分區一個硬盤,通常第一塊硬盤被稱為sda,第二塊硬盤被稱為sdb,表示我的系統有一塊硬盤,兩個分區,/sda1和/sda2,其中/sda1是boot分區(星號),/sda2是LVM類型的分區,一個邏輯管理,適用於需要動態調整大小的場合。從df命令可以看出/boot分區的確掛在/dev/sda1下。前面的device.map文件(boot/grub/):下的(hd0) /dev/sda,也就不難理解了,/sda表示整個第一塊硬盤。如果有興趣可以使用dd if=/dev/sda1 of=XXX bs=512 count=1, od-x XXX 查看第一個扇區的內容。最后一個必然是aa55,因為這是一個合法的mbr的必然信息,前面是grub的stage1,后面是分區信息,每個分區16個字節,由於我的盤只有兩個分區,所以aa55的前面會有那么多零是正常的,關於mbr的分析,如果感興趣自己查查,這里講篇幅留給其它更有意義的部分吧。
2.1.2MBR內容
好了,接下來正式進入grub2,grub在2014年就一直沒有跟新過了,但uboot更新可真是勤快啊,建議按照這個步驟先做一下,有點成就感,這樣會有信心往下走,而且PC應該不難找到,找個虛擬機就可以試,在弄個snapshot就不用擔心系統壞掉了。先來一個grub2的啟動界面:
2.1.3 grub啟動界面
1、 首先去gnu的官網下載一個grub-1.97.2.tar.gz源碼包,地址
ftp://alpha.gnu.org/gnu/grub/
2、 解壓 tar xvzfgrub.1.97.2.tar.gz
3、 進入解壓后目錄 cdgrub-1.97.2
4、 配置grub, ./configure
5、 編譯 make
6、 安裝grub-install/dev/sda
7、 更新grub grub2-mkconfig-o /boot/grub/grub.cfg,這里的grub.cfg和原來grub.conf的作用是一樣的,該命令會根據/boot下的kernel image和initrd信息生成啟動信息。如果是Ubuntu下,需要使用update-grub/dev/sda命令
8、 reboot重啟可以看到該系統啟動grub的信息。
下面就來說說grub的啟動步驟吧:
1、Bios將MBR中的512B東西拷貝到0x7c00處,然后跳至該處執行,然后裝載start模塊。這部分代碼參看/boot/i386/pc/boot.S
2、start模塊加載剩余的grub stage2部分,參看boot/i386/pc/diskboot.S。
3、 grub stage引導CPU保護模式,自解壓並釋放到0x10000開始內存處,解壓完成后再拷貝回原來位置,然后調用grub_main,見kern/main.c,grub_main初始化系統,加載模塊,並進入normal或者rescue模式。GRUB將根據配置文件grub.cfg或者用戶輸入,加載操作系統並執行操作系統。
在這里就贅述Makefile以及鏡像文件的鏈接關系等相關的內容了,這方面的知識會在內核映像生成過程中體現出來。針對越來越大的硬盤存儲容量,開始出現了GTP取代MBR的趨勢,GTP特性需要EFI特性的支持。
uboot
一個bootloader應當提供一下功能,
1. Setup and initialise the RAM.
2. Initialise one serial port.
3. Detect the machine type.
4. Setup the kernel tagged list or bdt.
5. Call the kernel image.
對其的介紹和使用這里就不寫了,網上針對2440的文章可謂泛濫了,自己找找動手實踐一下。
Linux內核映像生成過程
鏈接的語法可以參看:http://sourceware.org/binutils/docs-2.21/ld/
也可以查看精簡版的鏈接腳本的書寫格式。
http://www.slac.stanford.edu/comp/unix/package/rtems/doc/html/ld/ld.info.Scripts.html
內核鏡像一般包括兩個部分,一個部分是針對特定處理器、特定平台的啟動代碼,這部分通常被稱為boot代碼,另一部分就是Linux系統本身,這包括內存管理、進程調度、文件系統、進程間通訊、網絡子系統等部分。
如果你查下Linux的代碼會發現,就arch/(x86,arm)/boot下的內容差別就很大,arch/arm/boot目錄下的內容如下:
圖2.3.1 ARM啟動代碼目錄
arch/x86/boot下的內容如下:
圖2.3.2 X86啟動代碼目錄
從上面就可以看出X86的啟動代碼會比arm的啟動多一些,但是上述關於arm的boot代碼在arch/arm下還有一個板級的初始化,如s3c2440板級一些工作被放在了,arch/arm/mach-s3c24xx下了。一個內核映像的組成至少包括boot部分和kernel部分,我們暫且這么定,並且后面無特殊說明,也會boot指cpu和板級的初始化代碼,kernel指脫離了硬件差別的啟動代碼。
兩種平台啟動時,其中一個重要的差別是文件系統,因為arm通常用於嵌入式系統,其內存和Flash相對PC而受限,比較出名的嵌入式文件系統有yaffs2、jffs2、cramfs,由於工作的關系這里我們就說ubifs文件系統了,該文件系統是新一代為NAND Flash設計的文件系統,但其本身相對也較大,8M左右。PC架構采用ext3文件系統。
另一差別在對硬件的處理上,arm采用了設備樹,arch/arm/boot/dts,並且uboot現在也可以采用設備方法來解析硬件了。嵌入式設備對硬件的處理(依賴設備樹)較PC差異較大。這里先綜述PC下的啟動流程。
首先來看一個縮略的內核鏡像vmlinux(kernel部分,非boot)輸入文件編譯過程,使用的鏈接腳本是內核源碼文件/arch/x86/kernel/vmlinux.lds。
[root@ge linux-3.10]# make vmlinux
HOSTCC scripts/basic/fixdep
HOSTCC arch/x86/tools/relocs_32.o
HOSTCC arch/x86/tools/relocs_64.o
HOSTCC arch/x86/tools/relocs_common.o
HOSTLD arch/x86/tools/relocs
CHK include/generated/uapi/linux/version.h
CHK include/generated/utsrelease.h
CC kernel/bounds.s
GEN include/generated/bounds.h
CC arch/x86/kernel/asm-offsets.s
GEN include/generated/asm-offsets.h
CALL scripts/checksyscalls.sh
CC scripts/mod/empty.o
HOSTCC scripts/mod/mk_elfconfig
MKELF scripts/mod/elfconfig.h
CC scripts/mod/devicetable-offsets.s
GEN scripts/mod/devicetable-offsets.h
HOSTCC scripts/mod/file2alias.o
HOSTCC scripts/mod/modpost.o
HOSTCC scripts/mod/sumversion.o
HOSTLD scripts/mod/modpost
HOSTCC scripts/selinux/genheaders/genheaders
HOSTCC scripts/selinux/mdp/mdp
HOSTCC scripts/kallsyms
HOSTCC scripts/pnmtologo
HOSTCC scripts/conmakehash
HOSTCC scripts/sortextable
CC init/main.o
CHK include/generated/compile.h
CC init/version.o
CC init/do_mounts.o
CC init/do_mounts_initrd.o
LD init/mounts.o
CC init/initramfs.o
CC init/calibrate.o
CC init/init_task.o
LD init/built-in.o
HOSTCC usr/gen_init_cpio
GEN usr/initramfs_data.cpio
AS usr/initramfs_data.o
LD usr/built-in.o
LD arch/x86/crypto/built-in.o
CC arch/x86/kernel/process_32.o
CC arch/x86/kernel/signal.o
AS arch/x86/kernel/entry_32.o
CC arch/x86/kernel/traps.o
…
CHK include/generated/uapi/linux/version.h
UPD include/generated/uapi/linux/version.h
CHK include/generated/utsrelease.h
UPD include/generated/utsrelease.h
CC kernel/bounds.s
GEN include/generated/bounds.h
CC arch/x86/kernel/asm-offsets.s
GEN include/generated/asm-offsets.h
CALL scripts/checksyscalls.sh
CC scripts/mod/empty.o
HOSTCC scripts/mod/mk_elfconfig
MKELF scripts/mod/elfconfig.h
CC scripts/mod/devicetable-offsets.s
GEN scripts/mod/devicetable-offsets.h
HOSTCC scripts/mod/file2alias.o
HOSTCC scripts/mod/modpost.o
HOSTCC scripts/mod/sumversion.o
HOSTLD scripts/mod/modpost
HOSTCC scripts/selinux/genheaders/genheaders
HOSTCC scripts/selinux/mdp/mdp
HOSTCC scripts/kallsyms
HOSTCC scripts/pnmtologo
HOSTCC scripts/conmakehash
HOSTCC scripts/sortextable
CC init/main.o
CHK include/generated/compile.h
UPD include/generated/compile.h
CC init/version.o
CC init/do_mounts.o
CC init/do_mounts_initrd.o
CC init/do_mounts_md.o
LD init/mounts.o
CC init/initramfs.o
CC init/calibrate.o
CC init/init_task.o
LD init/built-in.o
HOSTCC usr/gen_init_cpio
GEN usr/initramfs_data.cpio
AS usr/initramfs_data.o
LD usr/built-in.o
CC arch/x86/kernel/process_32.o
CC arch/x86/kernel/signal.o
AS arch/x86/kernel/entry_32.o
CC arch/x86/kernel/traps.o
CC arch/x86/kernel/irq.o
CC arch/x86/kernel/irq_32.o
CC arch/x86/kernel/dumpstack_32.o
CC arch/x86/kernel/time.o
CC arch/x86/kernel/ioport.o
CC arch/x86/kernel/ldt.o
CC arch/x86/kernel/dumpstack.o
CC arch/x86/kernel/nmi.o
CC arch/x86/kernel/setup.o
CC arch/x86/kernel/x86_init.o
CC arch/x86/kernel/i8259.o
CC arch/x86/kernel/irqinit.o
CC arch/x86/kernel/jump_label.o
CC arch/x86/kernel/irq_work.o
CC arch/x86/kernel/probe_roms.o
CC arch/x86/kernel/i386_ksyms_32.o
CC arch/x86/kernel/syscall_32.o
CC arch/x86/kernel/bootflag.o
CC arch/x86/kernel/e820.o
CC arch/x86/kernel/pci-dma.o
CC arch/x86/kernel/quirks.o
CC arch/x86/kernel/topology.o
CC arch/x86/kernel/kdebugfs.o
CC arch/x86/kernel/alternative.o
CC arch/x86/kernel/i8253.o
CC arch/x86/kernel/pci-nommu.o
CC arch/x86/kernel/hw_breakpoint.o
CC arch/x86/kernel/tsc.o
CC arch/x86/kernel/io_delay.o
CC arch/x86/kernel/rtc.o
CC arch/x86/kernel/pci-iommu_table.o
CC arch/x86/kernel/process.o
CC arch/x86/kernel/i387.o
CC arch/x86/kernel/xsave.o
CC arch/x86/kernel/ptrace.o
CC arch/x86/kernel/tls.o
CC arch/x86/kernel/step.o
CC arch/x86/kernel/stacktrace.o
CC arch/x86/kernel/acpi/boot.o
CC arch/x86/kernel/acpi/sleep.o
CC arch/x86/kernel/acpi/cstate.o
LD arch/x86/kernel/acpi/built-in.o
CC arch/x86/kernel/apic/apic.o
CC arch/x86/kernel/apic/apic_noop.o
CC arch/x86/kernel/apic/ipi.o
CC arch/x86/kernel/apic/hw_nmi.o
CC arch/x86/kernel/apic/io_apic.o
CC arch/x86/kernel/apic/probe_32.o
LD arch/x86/kernel/apic/built-in.o
CC arch/x86/kernel/cpu/intel_cacheinfo.o
MKCAP arch/x86/kernel/cpu/capflags.c
CC arch/x86/kernel/cpu/capflags.o
CC arch/x86/kernel/cpu/common.o
CC arch/x86/kernel/cpu/match.o
CC arch/x86/kernel/cpu/bugs.o
CC arch/x86/kernel/cpu/intel.o
CC arch/x86/kernel/cpu/amd.o
CC arch/x86/kernel/cpu/transmeta.o
CC arch/x86/kernel/cpu/perf_event.o
CC arch/x86/kernel/cpu/perf_event_amd.o
CC arch/x86/kernel/cpu/perf_event_amd_uncore.o
CC arch/x86/kernel/cpu/perf_event_p6.o
CC arch/x86/kernel/cpu/perf_event_knc.o
CC arch/x86/kernel/cpu/perf_event_p4.o
CC arch/x86/kernel/cpu/perf_event_intel_lbr.o
CC arch/x86/kernel/cpu/perf_event_intel_ds.o
CC arch/x86/kernel/cpu/perf_event_intel.o
CC arch/x86/kernel/cpu/perf_event_intel_uncore.o
CC arch/x86/kernel/cpu/mcheck/mce.o
CC arch/x86/kernel/cpu/mcheck/mce_intel.o
CC arch/x86/kernel/cpu/mcheck/mce_amd.o
CC arch/x86/kernel/cpu/mcheck/threshold.o
CC arch/x86/kernel/cpu/mcheck/therm_throt.o
LD arch/x86/kernel/cpu/mcheck/built-in.o
CC arch/x86/kernel/cpu/mtrr/main.o
CC arch/x86/kernel/cpu/mtrr/if.o
CC arch/x86/kernel/cpu/mtrr/generic.o
CC arch/x86/kernel/cpu/mtrr/cleanup.o
CC arch/x86/kernel/cpu/mtrr/amd.o
CC arch/x86/kernel/cpu/mtrr/cyrix.o
CC arch/x86/kernel/cpu/mtrr/centaur.o
LD arch/x86/kernel/cpu/mtrr/built-in.o
CC arch/x86/kernel/cpu/perfctr-watchdog.o
CC arch/x86/kernel/cpu/perf_event_amd_ibs.o
LD arch/x86/kernel/cpu/built-in.o
CC arch/x86/kernel/reboot.o
CC arch/x86/kernel/msr.o
CC arch/x86/kernel/cpuid.o
CC arch/x86/kernel/early-quirks.o
CC arch/x86/kernel/smp.o
CC arch/x86/kernel/smpboot.o
CC arch/x86/kernel/tsc_sync.o
CC arch/x86/kernel/setup_percpu.o
CC arch/x86/kernel/mpparse.o
CC arch/x86/kernel/machine_kexec_32.o
CC arch/x86/kernel/crash.o
CC arch/x86/kernel/crash_dump_32.o
CC arch/x86/kernel/module.o
CC arch/x86/kernel/doublefault_32.o
CC arch/x86/kernel/vm86_32.o
CC arch/x86/kernel/early_printk.o
CC arch/x86/kernel/amd_nb.o
CC arch/x86/kernel/microcode_core_early.o
CC arch/x86/kernel/microcode_intel_early.o
CC arch/x86/kernel/microcode_intel_lib.o
CC arch/x86/kernel/microcode_core.o
CC arch/x86/kernel/microcode_intel.o
CC arch/x86/kernel/microcode_amd.o
LD arch/x86/kernel/microcode.o
CC arch/x86/kernel/check.o
CC arch/x86/kernel/perf_regs.o
LD arch/x86/kernel/built-in.o
AS arch/x86/kernel/head_32.o
CC arch/x86/kernel/head32.o
CC arch/x86/kernel/head.o
LD arch/x86/built-in.o
….
CC kernel/fork.o
CC kernel/exec_domain.o
CC kernel/panic.o
CC kernel/printk.o
CC kernel/cpu.o
…
CC mm/filemap.o
CC mm/mempool.o
CC mm/oom_kill.o
CC mm/fadvise.o
…
CC mm/migrate.o
LD mm/built-in.o
CC fs/open.o
CC fs/read_write.o
CC fs/file_table.o
CC fs/super.o
…
CC fs/drop_caches.o
LD fs/built-in.o
CC ipc/util.o
CC ipc/msgutil.o
CC ipc/msg.o
CC ipc/sem.o
CC ipc/shm.o
CC ipc/ipcns_notifier.o
CC ipc/syscall.o
CC ipc/ipc_sysctl.o
CC ipc/mqueue.o
CC ipc/namespace.o
CC ipc/mq_sysctl.o
LD ipc/built-in.o
CC security/keys/gc.o
…
CC security/capability.o
CC security/lsm_audit.o
LD security/built-in.o
CC crypto/api.o
CC crypto/cipher.o
CC crypto/compress.o
…
CC arch/x86/lib/usercopy_32.o
AR arch/x86/lib/lib.a
LINK vmlinux
LD vmlinux.o
MODPOST vmlinux.o
GEN .version
CHK include/generated/compile.h
UPD include/generated/compile.h
CC init/version.o
LD init/built-in.o
KSYM .tmp_kallsyms1.o
KSYM .tmp_kallsyms2.o
LD vmlinux
SORTEX vmlinux
SYSMAP System.map
對於boot部分,依賴於arch/x86/boot/setup.ld,其編譯過程如下,
CC arch/x86/boot/a20.o
AS arch/x86/boot/bioscall.o
CC arch/x86/boot/cmdline.o
AS arch/x86/boot/copy.o
HOSTCC arch/x86/boot/mkcpustr
CPUSTR arch/x86/boot/cpustr.h
CC arch/x86/boot/cpu.o
CC arch/x86/boot/cpucheck.o
CC arch/x86/boot/early_serial_console.o
CC arch/x86/boot/edd.o
VOFFSET arch/x86/boot/voffset.h
LDS arch/x86/boot/compressed/vmlinux.lds
AS arch/x86/boot/compressed/head_32.o
CC arch/x86/boot/compressed/misc.o
CC arch/x86/boot/compressed/string.o
CC arch/x86/boot/compressed/cmdline.o
CC arch/x86/boot/compressed/early_serial_console.o
OBJCOPY arch/x86/boot/compressed/vmlinux.bin
RELOCS arch/x86/boot/compressed/vmlinux.relocs
GZIP arch/x86/boot/compressed/vmlinux.bin.gz
HOSTCC arch/x86/boot/compressed/mkpiggy
MKPIGGY arch/x86/boot/compressed/piggy.S
AS arch/x86/boot/compressed/piggy.o
LD arch/x86/boot/compressed/vmlinux
ZOFFSET arch/x86/boot/zoffset.h
AS arch/x86/boot/header.o
CC arch/x86/boot/main.o
CC arch/x86/boot/mca.o
CC arch/x86/boot/memory.o
CC arch/x86/boot/pm.o
AS arch/x86/boot/pmjump.o
CC arch/x86/boot/printf.o
CC arch/x86/boot/regs.o
CC arch/x86/boot/string.o
CC arch/x86/boot/tty.o
CC arch/x86/boot/video.o
CC arch/x86/boot/video-mode.o
CC arch/x86/boot/version.o
CC arch/x86/boot/video-vga.o
CC arch/x86/boot/video-vesa.o
CC arch/x86/boot/video-bios.o
LD arch/x86/boot/setup.elf
OBJCOPY arch/x86/boot/setup.bin
OBJCOPY arch/x86/boot/vmlinux.bin
HOSTCC arch/x86/boot/tools/build
BUILD arch/x86/boot/bzImage
上述文件中總共着色了紅、橙、綠、藍、紫,(紫色的是和根文件系統相關的部分,暫時先不談。)
剩下的着色部分在啟動時會依上述順序執行,好了到這里了,該稍微總結一下了,根據Makefile規則,可以生成上述的啟動和內核文件。這基本上是一個能夠啟動的內核了,但是還確實根文件系統,也就是紫色部分會用到的,這部分會留到后面講述,這里仍以內核鏡像的生成和使用過程為主剖析。先來看一張圖,然后再將映像文件是如何鏈接起來的。
圖2.4.3 啟動過程圖
這張圖的有半部分在1.1節就見過了,是內存中代碼的架構,其中X = 0x001000 + grub的大小,因為grub大小在編譯時才確定,grub將上圖中紅色的512字節內容代碼拷貝到右圖紅色箭頭所指的地址處並將控制權較給這部分代碼,至此Linux內核代碼正式開始登場接管后續操作系統的啟動工作。然后將實模式代碼拷貝到墨綠色箭頭處執行,這部分代碼被稱為setup代碼,開始的代碼是上圖中黃色和綠色的兩個代碼,實模式跳轉到保護模式開始執行,是藍色箭頭表示的。實際的過程和上面講的還請有點區別,因為內核時經過壓縮的,由/arch/x86/boot/compressed/head_32.S調用misc.c中的decompress_kernel()函數解壓到0x100000地址處的,所以是整個一次性有grub拷到內存中的,然后會進行自解壓等,這里就簡化了這一處理過程,不過壓縮的內核代碼會被grub加載1M+的地方,這些地址的在鏈接時就確定了的。
當內核跳到0x100000處時,控制權由bst轉交給了真正意義上的kernel,這就是vmlinux的入口(可使用make vmlinux生成的),這時/arch/x86/kernel/head32.c中的i386_start_kerne()將會被調用,該函數會調用start_kernel()函數。該函數調用若干函數和腳本建立Linux的核心環境,start_kernel()之后調用init(),創建系統級線程,比較重要的如do_basic_setup()完成外設及驅動的加載,很多設備和驅動的源頭就來自這里,同時這里也會完成根文件系統的掛載工作。
完成上述工作后,init()會打開/dev/console設備,重定向stdin、stdout、stderr,最后使用execve()系統調用執行init程序。到這里可以說引導工作結束了。
init()進程開啟后,系統核心環境已經准備好了,接着init()讀取其配置文件/etc/inittab。
上述中setup大小是18K,這表示的是我的機器上是這樣的,也許你的會有差異,具體查看和計算方法如下:
[root@ge boot]# od -j 0x01f1 -N1/boot/vmlinuz-3.10.0
0000761 000036
0000762
0000036*512/1024=18K
鏈接過程就是將若干個輸入文件鏈接成一個文件,這編寫經典的helloword程序時,你可能使用了printf或者printk,這里就涉及到了庫文件和動態還是靜態編譯了,暫時先整個內核的鏈接過程按照上述也分為boot和kernel兩個部分,接下來就來看看這兩個部分,如果細心,就會發現有很多built-in.o,如ipc/built-in.o,並且它們的獲得方式不是cc而是ld,就是將對應目錄下文件鏈接成一個文件,如:
CC security/capability.o
CC security/lsm_audit.o
LD security/built-in.o
最后這些built-in.o文件和鏈接腳本一起作為輸入連接器的文件,最終將生成vmlinuz。
setup鏈接腳本,相關的注釋使用了華文隸書:
1 /*
2 * setup.ld
3 *
4 * Linker script for the i386setup code
5 */
6OUTPUT_FORMAT("elf32-i386", "elf32-i38 6","elf32-i386")
7OUTPUT_ARCH(i386)
8ENTRY(_start)
9
ENTRY表示程序的入口,在head.S文件的274行定義如下。
272 # offset 512, entry point
273
274 .globl _start
275 _start:
276 # Explicitly enter this asbytes, or the assembler
277 # tries to generate a 3-bytejump here, which causes
278 # everything else to push offto the wrong offset.
279 .byte 0xeb # short (2-byte) jump
280 .byte start_of_setup-1f
10SECTIONS
11 {
12 . = 0;
13 .bstext : { *(.bstext) }
下面顯示了為什么是bstext段的內容放在這里了,在header.S文件中已經制定的生成的段類型了。.o類型文件的所有段的段類型使用對應平台的readelf命令可以查看到。
./boot/setup.ld: .bstext : {*(.bstext) }
Binary file ./boot/header.o matches
./boot/header.S: .section ".bstext", "ax"
下面同理
14 .bsdata : { *(.bsdata) }
15
16 . = 495;
17 .header : { *(.header) }
18 .entrytext : { *(.entrytext)}
19 .inittext : { *(.inittext) }
./boot/setup.ld: .inittext : { *(.inittext)}
./boot/tty.c: * These functions are in.inittext so they can be used to signal
./boot/tty.c:static void__attribute__((section(".inittext"))) serial_putchar(int ch)
./boot/tty.c:static void__attribute__((section(".inittext"))) bios_putchar(int ch)
./boot/tty.c:void__attribute__((section(".inittext"))) putchar(int ch)
./boot/tty.c:void __attribute__((section(".inittext")))puts(const char *str)
20 .initdata : { *(.initdata) }
21 __end_init = .;
22
23 .text : { *(.text) }
./kernel/entry_32.S: .section.entry.text, "ax"
24 .text32 : { *(.text32) }
./boot/pmjump.S: .section ".text32","ax"
25
26 . = ALIGN(16);
27 .rodata : { *(.rodata*) }
28
29 .videocards : {
30 video_cards = .;
31 *(.videocards)
./boot/video.h:#define __videocard structcard_info __attribute__((section(".videocards")))
32 video_cards_end = .;
33 }
34
35 . = ALIGN(16);
36 .data : { *(.data*) }
37
38 .signature : {
39 setup_sig = .;
40 LONG(0x5a5aaa55)
41 }
42
43
44 . = ALIGN(16);
45 .bss :
46 {
47 __bss_start = .;
48 *(.bss)
49 __bss_end = .;
50 }
51 . = ALIGN(16);
52 _end = .;
53
54 /DISCARD/ : { *(.note*) }
DISCARD 該段不會出現在輸出文件中,通常用於檢查映像生成的正確性
55
56 /*
57 * The ASSERT() sink to . is intentional, for binutils 2.14compatibility:
58 */
59 . = ASSERT(_end <= 0x8000, "Setup too big!");
Setup部分的實模式代碼,結尾處不能超過0x8000,這樣圖2.4.1中的x+0x08000才有意義。
60 . = ASSERT(hdr == 0x1f1, "The setup header has the wrongoffset!");
在前面技術計算setup代碼大小時用到的這個數字0x1f1,該處存放的是setup的大小信息。其里存放的內容為buf[0x1f1] = setup_sectors-1;這就不能理解512存在的原因了。
61 /* Necessary for the very-old-loader check to work... */
62 . = ASSERT(__end_init <= 5*512, "init sections too big!");
判斷init段大小,應在2.5K以下,才正確
63
64 }
好了上面就是圖2.4.1中關於bootsector和setup的內容了。下面看看kernel的鏈接腳本,有點長,這里略去了64位總線PC情況,只留下32的SECTIONS定義。注釋方法同上。
首先來看看大體的鏈接成什么樣子,
# vmlinux
# ^
# |
# +-< $(KBUILD_VMLINUX_INIT)
# | +--< init/version.o + more
# |
# +--< $(KBUILD_VMLINUX_MAIN)
# | +--< drivers/built-in.omm/built-in.o + more
# |
# +-< ${kallsymso} (see description in KALLSYMS section)
上面深紅色KBUILD_VMLINUX_INIT表示的內容就是kernel image最開始存放的代碼了,接下來存放kernel的主要內容,一些核心、庫文件、驅動以及網絡代碼統統放在這個部分,想知道這里KBUILD_VMLINUX_INIT的內容存放什么內容, 看下面的Makefile腳本。
<Makefile>
755 # Externally visible symbols (used by link-vmlinux.sh)
756export KBUILD_VMLINUX_INIT:= $(head-y)$(init-y)
757export KBUILD_VMLINUX_MAIN := $(core-y) $(libs-y) $(drivers-y) $(net-y)
758export KBUILD_LDS :=arch/$(SRCARCH)/kernel/vmlinux.lds
763 vmlinux-deps := $(KBUILD_LDS) $(KBUILD_VMLINUX_INIT)$(KBUILD_VMLINUX_MAIN)
其中756-758行很關鍵,前兩個指定了在kernel前兩部分放置的內容,后一個則指定了鏈接的腳本文件的存放路徑,該鏈接腳本指導鏈接的整個過程,后面會簡單注釋以下。
771 vmlinux: scripts/link-vmlinux.sh $(vmlinux-deps) FORCE
從上述的Makefile文件可以看出導出符號KBUILD_VMLINUX_INIT 依賴於head-y和init-y符號表示的內容, :=是腳本一種賦值方法。再看head-y表示的是什么。
<arch/x86/Makefile>
160 # Kernel objects
161
162 head-y := arch/x86/kernel/head_$(BITS).o
163 head-y += arch/x86/kernel/head$(BITS).o
164 head-y += arch/x86/kernel/head.o
$(BITS)=32
可以看到依賴三個文件,這里BITS直接給出32,因為我們的系統是X86_32的,將所有符號帶入可得到:
head-y := head_32.S head32.c head.c
關於init-y 的內容類似,並且init-y部分非常中要,很多系統開機的設置均由這里代碼完成的,所以這部分會放在Linux系統啟動來講,如果想搶先看,情況init/main.c 下的start_kernel()函數,該函數做了非常多的系統初始化工作。下面就給出arch/X86/kernel/vmlinux.lds的注釋。
程序加載地址和運行地址是對應的兩種地址,鏈接腳本使用兩種地址來表示(虛擬/運行地址VMA,加載地址LMA),LMA地址由 AT指定。
#include <asm/boot.h>
ENTRY(phys_startup_32)
SECTIONS
{
. = LOAD_OFFSET + LOAD_PHYSICAL_ADDR;
//.是一個位置計數符號,記錄當前位置在目標文件中的虛擬地址(VMA),自增; 初始值為LOAD_OFFSET + LOAD_PHYSICAL_ADDR,前者是我們熟知的內核虛擬地址空間起始地址0xC0000000,LOAD_PHYSICAL_ADDR是內核image加載的物理地址,由CONFIG_PHYSICAL_START計算得到。
./kernel/vmlinux.lds.S: #define LOAD_OFFSET __PAGE_OFFSET
#define __PAGE_OFFSET _AC(CONFIG_PAGE_OFFSET,UL)
.config CONFIG_PAGE_OFFSET=0xC0000000
這里LOAD_OFFSET就是3G,
<asm/boot.h>
/* Physical address where kernel should be loaded. */
#define LOAD_PHYSICAL_ADDR ((CONFIG_PHYSICAL_START \
+(CONFIG_PHYSICAL_ALIGN - 1)) \
&~(CONFIG_PHYSICAL_ALIGN - 1))
# CONFIG_KEXEC_JUMP is not set
CONFIG_PHYSICAL_START=0x1000000
CONFIG_PHYSICAL_ALIGN=0x1000000
上面是做了邊界對齊。綜合上面可得到 .= 0xC0000000 + 0x1000000 ;對於0x1000000在圖2.4.1中找找看看在內存中的那個位置,這樣就可以知道上述虛擬地址3G~4G部分是留給內核的。
phys_startup_32 = startup_32 - LOAD_OFFSET;
這里暫且知道startup_32地址等於__HEAD,就是獲得物理地址,就是程序的加載地址。
arch/x86/kernel/head_32.S
__HEAD
ENTRY(startup_32)
/* Text and read-only data */
.text : AT(ADDR(.text) -LOAD_OFFSET) {
return the absolute address (the VMA) of the named section.。
創建第一段,其它段類推,段名.text, 加載地址AT指定的地址,加載地址地址為LOAD_PHYSICAL_ADDR
_text = .;
/* bootstrapping code */
HEAD_TEXT //bootsector對應的字段
./include/asm-generic/vmlinux.lds.h:#defineHEAD_TEXT *(.head.text)
. = ALIGN(8);
_stext = .;
TEXT_TEXT
#define TEXT_TEXT \
ALIGN_FUNCTION(); \
*(.text.hot) \
*(.text) \
*(.ref.text) \
DEV_KEEP(init.text) \
DEV_KEEP(exit.text) \
CPU_KEEP(init.text) \
CPU_KEEP(exit.text) \
MEM_KEEP(init.text) \
MEM_KEEP(exit.text) \
*(.text.unlikely)
其它的就不一一列舉了,init段的內容在啟動和驅動加載時可能會用到,這部分比較特殊一點。后續段內容參看./include/asm-generic/vmlinux.lds.h
SCHED_TEXT
LOCK_TEXT
KPROBES_TEXT
ENTRY_TEXT
IRQENTRY_TEXT
*(.fixup)
*(.gnu.warning)
/* End of text section */
_etext = .;
} :text = 0x9090
對section中的空隙用0x9090進行填充。0x90是匯編指令NOP的機器碼,故相當於在不連續代碼間填充空操作。
NOTES :text :note
EXCEPTION_TABLE(16) :text = 0x9090
/* Data */
.data : AT(ADDR(.data) - LOAD_OFFSET) {
/* Start of data section */
_sdata = .;
/* init_task */
INIT_TASK_DATA(THREAD_SIZE)
/* 32 bit has nosave before_edata */
NOSAVE_DATA
PAGE_ALIGNED_DATA(PAGE_SIZE)
CACHELINE_ALIGNED_DATA(L1_CACHE_BYTES)
DATA_DATA
CONSTRUCTORS
/* rarely changed data like cpumaps */
READ_MOSTLY_DATA(INTERNODE_CACHE_BYTES)
/* End of data section */
_edata = .;
} :data
/* Init code and data - will be freedafter init */
. = ALIGN(PAGE_SIZE);
.init.begin : AT(ADDR(.init.begin) - LOAD_OFFSET) {
__init_begin = .; /* pairedwith __init_end */
}
INIT_TEXT_SECTION(PAGE_SIZE)
INIT_DATA_SECTION(16)
.x86_cpu_dev.init : AT(ADDR(.x86_cpu_dev.init) - LOAD_OFFSET) {
__x86_cpu_dev_start = .;
*(.x86_cpu_dev.init)
__x86_cpu_dev_end = .;
}
/*
* start address and size of operations which during runtime
* can be patched with virtualization friendly instructions or
* baremetal native ones. Think page table operations.
* Details in paravirt_types.h
*/
. = ALIGN(8);
.parainstructions : AT(ADDR(.parainstructions) - LOAD_OFFSET) {
__parainstructions = .;
*(.parainstructions)
__parainstructions_end = .;
}
/*
* struct alt_inst entries. From the header (alternative.h):
* "Alternative instructions for different CPU types orcapabilities"
* Think locking instructions on spinlocks.
*/
. = ALIGN(8);
.altinstructions : AT(ADDR(.altinstructions) - LOAD_OFFSET) {
__alt_instructions = .;
*(.altinstructions)
__alt_instructions_end = .;
}
/*
* struct iommu_table_entry entries are injected in this section.
* It is an array of IOMMUs which during run time gets sorted depending
* on its dependency order. After rootfs_initcall is complete
* this section can be safely removed.
*/
.iommu_table : AT(ADDR(.iommu_table) - LOAD_OFFSET) {
__iommu_table = .;
*(.iommu_table)
__iommu_table_end = .;
}
. = ALIGN(8);
.apicdrivers : AT(ADDR(.apicdrivers) - LOAD_OFFSET) {
__apicdrivers = .;
*(.apicdrivers);
__apicdrivers_end = .;
}
. = ALIGN(8);
/*
* .exit.text is discard at runtime, not link time, to deal with
* references from.altinstructions and .eh_frame
*/
.exit.text : AT(ADDR(.exit.text) - LOAD_OFFSET) {
EXIT_TEXT
}
.exit.data : AT(ADDR(.exit.data) - LOAD_OFFSET) {
EXIT_DATA
}
#if !defined(CONFIG_X86_64) ||!defined(CONFIG_SMP)
PERCPU_SECTION(INTERNODE_CACHE_BYTES)
#endif
. = ALIGN(PAGE_SIZE);
/* freed after init ends here */
.init.end : AT(ADDR(.init.end) - LOAD_OFFSET) {
__init_end = .;
}
/*
* smp_locks might be freed after init
* start/end must be page aligned
*/
. = ALIGN(PAGE_SIZE);
.smp_locks : AT(ADDR(.smp_locks) - LOAD_OFFSET) {
__smp_locks = .;
*(.smp_locks)
. = ALIGN(PAGE_SIZE);
__smp_locks_end = .;
}
/* BSS */
. = ALIGN(PAGE_SIZE);
.bss : AT(ADDR(.bss) - LOAD_OFFSET) {
__bss_start = .;
*(.bss..page_aligned)
*(.bss)
. = ALIGN(PAGE_SIZE);
__bss_stop = .;
}
. = ALIGN(PAGE_SIZE);
.brk : AT(ADDR(.brk) - LOAD_OFFSET) {
__brk_base = .;
. += 64 * 1024; /* 64k alignment slop space */
*(.brk_reservation) /* areas brk users have reserved */
__brk_limit = .;
}
_end = .;
STABS_DEBUG
DWARF_DEBUG
/* Sections to be discarded */
DISCARDS
/DISCARD/ : { *(.eh_frame) }
}
從上面的鏈接腳本你會發現圖2.4.3左邊的鏡像缺少一部分,正確的形式如下,之所以上下沒有對齊,是因為地址啦,內核的VMA地址不是從零開始的,而是0xC0000000 + 0x1000000:
圖2.4.4內核鏡像和啟動內存布局
initramfs
本節對PC下的根文件系統做個簡單的過程瀏覽,深入的文件系統的部分放在嵌入式根文件系統的制作上。Initrd是initial ramdisk的簡稱,initramfs是initial ram filesystem的簡稱,是一種cpio格式的內存文件系統,在pc下init-3.10.0.img就是一個cpio格式的,目前initrd類型的啟動方式少見了。
PC下的initramfs
先來看看/boot/grub/grub.cfg文件里的內容,下圖只截圖了3.10內核啟動用到的部分,有這么一行:
Initrd /init-3.10.0.img
如果其在啟動配置文件grub.cfg里,那么可以說該文件比較重要了,因為系統啟動需要使用它,實際上也可以不使用initrd,只需要將上面那行改成no initrd就可以了,大多數情況下系統仍然可以正常啟動,但是對於服務器多半會啟動不了,這取決於具體的硬件配置。
圖2.4.1.1 grub配置信息
早期的Linux內核中是沒有initrd的,在Linux 0.11版本中,系統啟動時直接掛載floppy(軟盤),然后啟動系統的init進程,該進程在任何Linux系統中都存在,到目前是必不可少的,0.11將init存儲在floppy中,但是隨着時代的變遷新的存儲器也出現了,比如scsi硬盤,sd、usb存儲設備等相繼出現,這時如果將這些存儲設備的驅動編譯進內核,可以想象,內核的將會非常大,Linux內核的一個宗旨就是簡單,為了保持內核代碼簡潔,就將這些設備的驅動程序放在了文件系統里。這就意味着要想獲得init程序需要先掛載設備的驅動,但是驅動代碼在文件系統里,所以系統要先掛載設備的驅動,然后從設備中找到對應的init,這個加載驅動和啟動init的工作就放在了initrd中來完成了,下面就來揭開initrd的神秘面紗,首先我們看看initrd-3.10.0.img的內容,注意該文件為cpio格式的,請按照下述方式操作。
cp /boot/initrd-3.10.0.img initrd.gz
lsinitrd initrd.gz
到這已經可以看到initrd的內容,也可以使用下述命令去詳細的看看該文件的組織結構。
gzip –d initrd.gz
cpio –ivdm < initrd.gz
cat init
文件輸出如下:
#!/bin/nash
mount -t proc /proc /proc
setquiet
echo Mounting proc filesystem
echo Mounting sysfs filesystem
mount -t sysfs /sys /sys
echo Creating /dev
mount -o mode=0755 -t tmpfs /dev /dev
mkdir /dev/pts
mount -t devpts -o gid=5,mode=620 /dev/pts /dev/pts
mkdir /dev/shm
mkdir /dev/mapper
echo Creating initial device nodes
mknod /dev/null c 1 3
mknod /dev/zero c 1 5
mknod /dev/systty c 4 0
mknod /dev/tty c 5 0
mknod /dev/console c 5 1
mknod /dev/ptmx c 5 2
mknod /dev/tty0 c 4 0
mknod /dev/tty1 c 4 1
mknod /dev/tty2 c 4 2
mknod /dev/tty3 c 4 3
mknod /dev/tty4 c 4 4
mknod /dev/tty5 c 4 5
mknod /dev/tty6 c 4 6
mknod /dev/tty7 c 4 7
mknod /dev/tty8 c 4 8
mknod /dev/tty9 c 4 9
mknod /dev/tty10 c 4 10
mknod /dev/tty11 c 4 11
mknod /dev/tty12 c 4 12
mknod /dev/ttyS0 c 4 64
mknod /dev/ttyS1 c 4 65
mknod /dev/ttyS2 c 4 66
mknod /dev/ttyS3 c 4 67
echo Setting up hotplug.
hotplug
echo Creating block device nodes.
mkblkdevs
echo "Loading ehci-hcd module"
modprobe -q ehci-hcd
echo "Loading ohci-hcd module"
modprobe -q ohci-hcd
echo "Loading uhci-hcd module"
modprobe -q uhci-hcd
mount -t usbfs /proc/bus/usb /proc/bus/usb
echo "Loading ext3 module"
modprobe -q ext3
echo "Loading scsi_mod module"
modprobe -q scsi_mod
echo "Loading sd_mod module"
modprobe -q sd_mod
echo "Loading scsi_transport_spi module"
modprobe -q scsi_transport_spi
echo "Loading mptbase module"
modprobe -q mptbase
echo "Loading mptscsih module"
modprobe -q mptscsih
echo "Loading mptspi module"
modprobe -q mptspi
echo "Loading dm-mod module"
modprobe -q dm-mod
echo "Loading dm-mirror module"
modprobe -q dm-mirror
echo "Loading dm-zero module"
modprobe -q dm-zero
echo "Loading dm-snapshot module"
modprobe -q dm-snapshot
echo Making device-mapper control node
mkdmnod
mkblkdevs
echo Scanning logical volumes
lvm vgscan --ignorelockingfailure
echo Activating logical volumes
lvm vgchange -ay --ignorelockingfailure VolGroup00
resume /dev/VolGroup00/LogVol01
echo Creating root device.
mkrootdev -text3 -o defaults,ro /dev/VolGroup00/LogVol00
echo Mounting root filesystem.
mount /sysroot
echo Setting up other filesystems.
setuproot
loadpolicy
echo Switching to new root and running init.
switchroot
echo Booting has failed.
sleep -1
上述文件可以看出,該文件的作用就是創建一些系統運行的依賴目標,然后掛載了幾個存儲設備的驅動,掛載了根文件系統,這里的文件系統是真正操作系統起來后使用的文件系統,最后啟動init進程就結束了。
如果看cpio命令后的目錄下內容,黑色的那兩個文件不是cpio解壓出來的。主要包括啟動的程序、啟動工具和初始化服務,如網絡、終端、設備驅動的加載以及加載其它文件系統。
圖2.4.1.2 cpio文件解壓內容
在來看看bin目錄下的內容:
圖2.4.1.3 PC根文件系統bin內容
這里可以看到好多命令都在這里了,但是對於嵌入式環境看到的也許有點不同哦,由於嵌入式環境的存儲空間是非常珍貴的,多以這里看到的這么多命令在嵌入式環境就變成busybox了,所有的命令均是鏈接到busybox,由busybox解析命令。
這里在總結一下這個過程,首先grub根據配置文件grub.cfg解析是否時能initrd,如果時能將內核鏡像和initrd拷貝到物理內存中,然后grub將控制權交給內核,內核加載initrd,解壓並將內容拷貝到/dev/ram0中,解壓后的內容就是一個文件系統了,該文件系統在RAM上,這就是說內存盤(ramdisk)中存在一個文件系統了,內核這是會將其掛載為rootfs(root file system)根文件系統,然后內核創建線程執linuxrc(嵌入式系統),完成后會寫在initrd並進行最后的啟動,進程這是使用initrd去初始化一些系統資源,掛載根文件系統,調用init進程初始化系統資源並啟動shell。至此對用戶而言系統可用了。
對於嵌入式設備,如果實際的根設備(非易失性存儲器)存在/initrd目錄,Linux掛載並將其做為最終的根文件系統,如果不存在initrd的鏡像將被丟棄。但是如果在內核命令行中指定了root=/dev/ram0之類,linuxrc的執行將被跳過並且也不會嘗試掛載其它文件系統做為最終的根文件系統。
接下來會講嵌入式系統下根文件系統的制作,不再講述PC下initrd的制作,有興趣可以自己嘗試制作一個,嵌入式下的根文件系統制作基於busybox,並且個人覺得嵌入式下的根文件系統的制作更能讓人理解根文件系統的方方面面。
busybox嵌入式根文件系統的制作:
http://busybox.net/
1、 解壓busybox-1.22.1.tar.gz
2、 配置源碼 makemenuconfig ARCH=arm CROSS_COMPILE=arm-linux-
3、 make
4、 make install
圖2.4.2.1 安裝busybox過程
5、 進入上一步生成的_install目錄,該目錄下會生成以下幾個文件
圖2.4.2.2 安裝生成內容
6、 添加其它目錄mkdirdev etc mnt proc var tmp sys root lib
至於根文件系統的各個目錄的作用可以參看FHS,《filesystem Hierarchy Standard》
/dev 設備文件目錄,應用程序和驅動程序的橋梁,udev支持的熱插拔設備自動創建。
/etc 配置文件,許多系統啟動的配置選項均在此,PC下和嵌入式下配置文件的內容差別比較大,有興趣自己比較。
/mnt 掛載目錄點,可掛載其它文件系統類型的存儲設備。
/proc 虛擬設備文件系統,一些內核參數由此導出,一般不允許更改權限,驅動程序的一些輔助調試接口通常導出到該目錄下。
/lib 存放動態、靜態庫等文件,命令以及應用程序會使用該庫文件,glibc庫必須有,否則基本的ls、cd命令可能無法運行。
/sys虛擬文件系統,用於設備和驅動的管理,驅動的class接口位於此,udev自動創建設備節點的功能依賴於該文件系統。
/root根用戶登錄目錄
7、 添加動態庫cp –a$(TOOLCHIAN)arm-none-linux-gnueabi/sys-root/lib/*so* ./lib/
該目錄下還有靜態庫,系統命令會調用該庫里的文件。
8、 添加系統文件
[root@ge _install]# cd etc/
[root@ge etc]# vim inittab
[root@ge etc]# vim fstab
[root@ge etc]# Vim passwd
[root@ge etc]# mkdir init.d
[root@ge etc]# cd init.d/
[root@ge init.d]# vim rcS
[root@ge init.d]# chmod +x rcS
[root@ge init.d]# vim profile
這里的文件都是在/etc目錄下的文件,主要用於Linux系統啟動階段,inittab屬於內核標准文件,有其編寫標准,該文件是Linux系統啟動時該目錄下第一個使用到的文件。
Inittab內容如下:
#this is run first except when bootinginsingle-user mode.
::sysinit:/etc/init.d/rcS
# /bin/sh invocations on selected ttys
# Start an "askfirst" shell ontheconsole (whatever that may be)
ttySAC0::askfirst:-/bin/sh
# Stuff to do when restarting the initprocess
::restart:/sbin/init
#Stuff to do before rebooting
::ctrlaltdel:/sbin/reboot
/sbin/init --mount the root file system
可以參看相關的語法講解,sysinit具有最高優先級,該行表示,運行/etc/init.d/rcS,關於該文件后面再說,ttySAC0表示終端運行的shell,嵌入式一般是/bin/sh,前面的-表示當該終端打開時會調用/etc/profile腳本,如果沒有-,則會運行對應用戶目錄下的~/.profile,ctrlaltdel表示當ctrl + Alt + Del 三鍵同時按下時的動作,運行/sbin/reboot命令即重啟,restart行表示重啟時運行/sbin/init,這里可以看出和冷啟動(斷電再上電)的區別。
Fstab文件內容如下:
#device mount-point type options dump fsch order
proc /proc proc defaults 0 0
sysfs /sys sysfs defaults 0 0
該文件同樣是內核標准文件,可以沒有,在rcS下創建上述設備目錄。上述表示文件系統的掛載相關信息,如掛載點、文件系統類型等。
rcS的內容如下:
#!/bin/sh
#This is the firstscript called by initprocess
/bin/mount –a
通常這里的內容會比較多,一般會調用其它的腳本啟動一些常駐的應用程序,為shell腳本,內容差別可能非常大。
Profile文件內容
#!/bin/sh
exportHOSTNAME=ge
export USER=root
export HOME=root
#exportPS1="[$USER@$HOSTNAME\W]#"
PATH=/bin:/sbin:/usr/bin:/usr/sbin
LD_LIBRARY_PATH=lib:/usr/lib:$LD_LIBRARY_PATH
export PATHLD_LIBRARY_PATH
該文件設置一些環境參數。
在passwd文件中添加如下行:
root::0:0:root:/root:/bin/sh,即只保存與root相關項
9、 創建設備節點
mknod dev/console c 5 1
mknod dev/null c 1 3
通常必須有console這個目錄,null目錄也常常需要,如果沒有console目錄,啟動會導致失敗。到此busybox部分工作完成了,因為選擇的是動態編譯方式,所以ls等命令依賴的glibc庫文件需要添加到/lib目錄下,但是真正的根文件系統制作完畢還有/lib沒有完成,實際使用中通常會進行壓縮,一般兩種方式,一種.tar.gz,還有一種一種就是cpio格式。在根目錄下生成cpio格式的壓縮文件,使用的如下命令:
Find . | cpio–quiet –o –H newc | gzip > ./rootfs.cpio.gz
嵌入式文件系統深入
上面已經構建了一個可以使用的根文件系統了,ls、cd命令已經可以正確運行,現在給兩個題目,接下來就這兩個題目想闡述如何針對項目需求去定制一個根文件系統。
題目一:物聯網發展趨勢下,以后的嵌入式設備會聯網,如何添加防火牆的功能呢?
題目二:聽說具有加密功能的ssh能夠為在網絡上傳輸提供一層保障,該如何實現該功能呢?
對於題目一使用開源軟件包iptables,交叉編譯iptables-1.4.18.tar.gz軟件包,
./configure CC=arm-linux-gcc --enable-static –disable-shared–host=arm-linux –prefix=$(YOUR INSTALL DIRECTORY)
Make
Make install
這樣就編譯好了,可以把install目錄下的相關內容拷貝到上述對應的目錄中,可執行文件到/sbin 、庫文件到/lib中,這樣在shell下就可以使用iptables命令了。
題目二比題目一稍微復雜一點,ssh功能的支持包是dropbear,但是呢編譯該時它還依賴zlib這個壓縮包提供的功能,所以呢,需要首先編譯zlib包,而后才是編譯dropbear包。
zlib-1.2.8.tar.gz編譯如下:
export CC=arm-linux-gcc
./configure –prefix=/$(YOUR INSTALLDIRECTORY)
Make && make install
Dropbear-2013.5.58 的編譯命令如下
./configrure –with-zlib=$(YOUR zlib INSTALLDIRECTORY) cc=arm-linux –host=arm-linux
Make && make install
同樣將生成的可執行文件以及庫文件復制到對應的根文件系統的目錄中,這樣就可以使用該功能,如果對configure不太懂,./configure –-help命令可以查看。
通過上面的兩個例子,來總結一下如何向根文件系統添加新功能的思路,首先根據要提供的功能,(一般可能是/bin、/sbin之類下的命令,可能是/lib下的庫(應用程序使用)文件,抑或兩者兼有),查找有沒有現成的開源包,如果有,那么交叉編譯,拷貝,如果沒有那就自己寫,然后交叉編譯,拷貝。一切都是那么的順利成章啊~!
最后需要提醒一下上文提到的udev機制在也是需要編安裝和拷貝的。
根文件系統的加載
這里的加載指的是操作系統啟動時的加載過程,在系統啟動時start_kernel()->vfs_cache_init()->mnt_init()->init_rootfs(),該函數會掛載rootfs作為根文件系統,可以使用cat /proc/mounts查看,然后會解壓用gzip壓縮的歸檔文件到rootfs目錄,接着執行解壓后的init程序,一旦該程序運行,那么就稱其為init進程,並將控制權移交給init進程,這也是系統由內核到用戶態的標志,如果在解壓的rootfs目錄下找不到init程序,那么會根據解析的cmdline參數,找“root=XXX”,並掛載,該參數有devicetree的chosen節點提供。在Linux內核編譯時CONFIG_INITRAMFS_SOURCE指明intramfs的位置。通常一個系統可用的文件系統在/etc/fstab文件中,但是嵌入式環境下通常不用。
設備樹
對於Linux系統,通常啟動分為bld(bootloader),bst(bootstrap),vmlinux(這里的vmlinux不包括bst,但是通常Linux內核源碼包,編譯出來的最終vmlinux是包含bst,這里將其分開講解了)。當你在Linux-3.10根目錄下使用make vmlinux時,你會發現確實有該目標,可是這里所說的vmlinux是用來生成vmlinuz的,所以概念上還是有點區別的,區別在於前一個vmlinux並不包括bst部分,而最終的vmlinuz是包括bst的。這個關於Makefile的分析就略過了,關鍵的執行步驟會在下面展現出來的。
這里先提一下,由於當/sbin/init進程運行起來以后,其讀取/etc/inittab配置文件,根據配置信息確定運行級別,筆者對應的運行級別為5,即圖形界面方式,在啟動時的視頻中就顯示了該信息。Init進程會執行系統初始化腳本/etc/rc.d/rc.sysinit,對系統進行配置,以讀寫方式掛載根文件系統,至此,系統基本運行起來了,接着運行/etc/rc.d/rc,該文件會判斷配置文件中的啟動運行級別,並定義了服務啟動的順序,並啟動/etc/rc.d/rc5.d,該目錄下的內容鏈接至/etc/init.d目錄下,后面會啟動虛擬終端/sbin/mingetty,在運行級別5上啟動xwindow,這是就是大家看到的登錄界面了,用戶輸入用戶名和密碼,然后/etc/passwd完成密碼驗證,登錄了。這一過程的文檔確實很多,所以這里打算深入兩個細節分析一下,這也是筆者在設備驅動的道路上吃過苦頭才認認真真看了源碼的。
網上分析Linux內核啟動的文章很多了,就算不是針對3.10版本的內核,也可以參考,並結合3.10版的代碼進行分析,這里打算重點講下嵌入式下的device tree的解析和設備的構建,未來嵌入式系統解析設備信息將會采用設備樹的方式,這也是為什么這里會詳細討論該解析過程了,但是這是和嵌入式處理器息息相關的,不同的處理器的設備樹的構建和解析也是有區別的,所以Linux內核會將這部分的解析代碼放在setup_arch()函數中完成。
devicetree設備樹
由於3.10版本的嵌入式環境下大多數使用的是device tree的方法來描述設備,不僅如此,uboot也可以並且也正在采用device tree的方法,所以這里會花點篇幅介紹它,然后才是Linux系統的啟動。
為什么先介紹device tree:
在2.3節中bootloader功能的提到如下兩個步驟:
4. Setup the kernel tagged list or dtb.
5. Call the kernel image.
對於3.10版本的內核使用dtb(device tree block)方法來描述設備拓撲,所以上面的4里的tagged list並沒有被使用,而是使用了dtb,對於上面的5,call內核前將前三個寄存器按如下方式設定。
- CPU register settings
r0= 0,
r1= machine type number.
r2= physical address of tagged list in system RAM, or
physical address of device tree block (dtb) in system RAM
對於r2寄存器的值為RAM中dtb的地址。
內核在啟動時會查r2地址為的4偏移處的值,如果r2地址偏移零處的值為零,那么表示沒有taglist或者dtb被傳遞過來,如果4的偏移處為0xd00dfeed,那么表示傳遞過來的是dtb。
同時也會傳遞系統內存和根文件系統。這是通過device tree的chosen節點實現的。關於device tree的語法參看:
http://www.devicetree.org/Device_Tree_Usage
關於device tree 的用法可以參看:
https://wiki.freebsd.org/FlattenedDeviceTree
在操作系統源碼中/arch/arm/boot/dts/目錄下常見以.dts和.dtsi為后綴的文件,通常來說.dtsi一般是cpu級別的描述,而.dts則是板級的描述,在編譯生成dtb是通常.dts會包含.dtsi,以構成完整的設備樹,當還有一個skeleton.dts的通常會被包含。
這里給出一個
/proc/device-tree 是device tree的一個調試入口,
Documentation/devicetree/booting-without-of.txt.
devicetree的信息獲取
先來看看devicetree的解析過程:
首先看arch/arm/kernel/head.S,注意和arch/arm/boot/compressed下的head.S區別,后者屬於bst的部分, arch/arm/boot/compressed下的內容,最終會生成bst部分的代碼,這里從內核的head.S開始講起。該函數調用需要滿足時各個寄存器存儲的內容如下:
R0:內核所在地址 R1 : machine number
R2: dtb or atags R8: phys_offset R9:cupid R10:procinfo
head.S
79 ENTRY(stext)
119 bl __vet_atags
135 ldr, r13=__mmap_switched
142 b __enable_mmu
415 __enable_mmu:
441 b __turn_mmu_on
460 ENTRY(__turn_mmu_on)
467 mov r3, r13
468 mov pc, r3
上述119行跳轉地址定義位於/arch/arm/kernel/head-common.S
18 #ifdef CONFIG_CPU_BIG_ENDIAN
19#define OF_DT_MAGIC 0xd00dfeed
20#else
21#define OF_DT_MAGIC 0xedfe0dd0 /* 0xd00dfeed in big-endian */
22#endif
46__vet_atags:
47 tst r2, #0x3 @ aligned?
48 bne 1f
49
50 ldr r5, [r2, #0]
51#ifdef CONFIG_OF_FLATTREE
52 ldr r6, =OF_DT_MAGIC @ is it a DTB?
53 cmp r5, r6
54 beq 2f
55#endif
56 cmp r5, #ATAG_CORE_SIZE @ is first tag ATAG_CORE?
57 cmpne r5, #ATAG_CORE_SIZE_EMPTY
58 bne 1f
59 ldr r5, [r2, #4]
60 ldr r6, =ATAG_CORE
61 cmp r5, r6
62 bne 1f
63
642: mov pc, lr @ atag/dtb pointer isok
65
661: mov r2, #0
67 mov pc, lr
68ENDPROC(__vet_atags)
46行判斷r2寄存器內容(dtb或者atags的地址)是否四字對齊,如果不是那么就是atags的地址可以直接返回,將首地址內容賦值為零,返回head.S函數的調用處。如果是四字對齊的,那么可能是dtb了,需要進一步判斷。50行獲取四字對齊的地址的起始內容,如果是dtb,那么這里應該放的就是devicetree的魔數了。52行存儲設備樹的魔數,定義見18~21行。56-62處理ATAG_CORE的情況。
arch/arm/kernel/head.S的135~468行,跳轉到arch/arm/kernel/head-common.S執行,代碼如下:
80__mmap_switched:
81 adr r3, __mmap_switched_data
82
83 ldmia r3!, {r4, r5, r6, r7}
84 cmp r4, r5 @ Copy datasegment if needed
85 1: cmpne r5, r6
86 ldrne fp, [r4], #4
87 strne fp, [r5], #4
88 bne 1b
89
90 mov fp, #0 @Clear BSS (and zero fp)
91 1: cmp r6, r7
92 strcc fp, [r6],#4
93 bcc 1b
94
95 ARM( ldmia r3, {r4, r5, r6, r7, sp})
96 THUMB( ldmia r3, {r4, r5, r6, r7} )
97 THUMB( ldr sp, [r3, #16] )
98 str r9, [r4] @Save processor ID
99 str r1, [r5] @Save machine type
100 str r2, [r6] @ Save atags pointer
101 cmp r7, #0
102 bicne r4, r0, #CR_A @ Clear 'A' bit
103 stmneia r7, {r0, r4} @Save control register values
104 b start_kernel
105 ENDPROC(__mmap_switched)
106
107 .align 2
108 .type __mmap_switched_data,%object
109 __mmap_switched_data:
110 .long __data_loc @ r4
111 .long _sdata @ r5
112 .long __bss_start @ r6
113 .long _end @ r7
114 .long processor_id @ r4
115 .long __machine_arch_type @ r5
116 .long __atags_pointer @ r6
117 #ifdef CONFIG_CPU_CP15
118 .long cr_alignment @ r7
119 #else
120 .long 0 @ r7
121 #endif
122 .long init_thread_union +THREAD_START_SP @ sp
123 .size __mmap_switched_data, . -__mmap_switched_data
其中100行完成將__atags_pointer設置為dtb的地址。104行跳轉到start_kernel函數執行。該函數定義init/main.c中,網上對start_kernel()的分析非常多,該函數會間接調用很多子系統的代碼該start_kernel()函數的501行如下:
setup_arch(&command_line);
上述函數會解析dtb。
該函數最終將解析的根節點存放於of_allnodes中,在后續設備注冊時,會掃描該節點,和該節點匹配上才進行實際的注冊,這里設備分為兩種情況,舉個例子如果U盤想要工作,一般嵌入式Soc會集成usb控制器,這里usb控制器和u盤都會用到of_allnodes,所不同的是usb控制器的初始化會在start_kernel()中的do_basic_setup()中完成,而U盤的初始化則會延遲到U盤插入,熱插拔機制會啟動相應的支持。
在setup_arch配置完成之后還會有一個board的初始化,arch/arm/match-XXX/目錄下的一些平台相關的代碼在這里會被調用,比如該目錄下的init_irq、init_machine函數就會被調用。
在init_machine函數中會調用of_platform_populate()注冊平台總線和平台設備。對於像i2c、spi之類設備則會在early_platform_add_devices()完成注冊。
開發中的Linux內核源碼,git.kernel.org/cgit/linux/kernel/git/Torvalds/linux.git
上述的vmlinux是沒壓縮過的內核映像,真正使用時會將內核壓縮省以減小內核映像的大小和以及節約映像從Flash或磁盤拷貝到內存的時間。去了壓縮鏈接的過程,這一過程在ARM的嵌入式平台和PC平台均存在,但是由上面的基礎來看壓縮部分的應該不難了。X86部分的壓縮實現見arch/x86/boot/compressed/。
最后來點收尾的,你可能發現在1.1節中提及Linux內核升級的時候講到了make modules 和make modules_install,這里一直未提到該內容,從該命令可以看出來是和module有關系的,config時會選擇編譯成module還是編譯進內核抑或不編譯,如果選擇編譯成module,這里兩個命令就是用來處理那部分代碼的,簡單點說就是做如下的工作(注意在現有ext3文件系統上操作,系統啟動時只有根文件系統可有):
cp -f/opt/linux-3.10/modules.order /lib/modules/3.10.0/
cp -f/opt/linux-3.10/modules.builtin /lib/modules/3.10.0/
…
該文件夾下的modules.dep,描述模塊的依賴關系,當有熱插拔事件發生時,內核會將處理過程推到用戶態處理,主要關系udev實現機制,該機制會創建必要的設備節點和加載對應的驅動需要的模塊,模塊的依賴關系就在modules.dep里描述了。這里就可以看出來為什么和make vmlinux分開處理了,因為一個編譯生成可執行文件,一個是在現有文件系統中插入一部分“module”。
devicetree函數的解析
在為了讓上面setup_arch()函數所表示的啟動過程更具有真實性,這里我們舉個devicetree的例子,首先知道通常嵌入式下使用的處理器被稱為SOC 而不是cpu,因為該芯片通常集成了許多控制器,所以假設我們的cpu是cotex-A9的,並且假設其集成有apb總線,在ahb總線上掛有以太網控制器,apb總線掛有i2c控制器,在實際的電路板上還有一個i2c控制的4951的音頻芯片。
這里講Soc具有的東西寫成.dtsi的文件,將板上其它的外設寫成.dts文件,這樣在.dts文件中include該文件就可以了,這樣可具有良好的可移植性。
ABCD.dtsi文件,文件在內核中的位置,arch/arm/boot/dts文件下,
/{
compatible = “ABCD,exp”;
cpus {
#address-cells=<1>;
#size-cells =<0>;
cpu0{
device_type=”cpu”;
compatible=“ABCD,cortex-a9”;
reg = <0>;
};
ahb@e0000000{
compatible=”simple-bus”;
mac0:ethernet@e000d0000{
compatible=” ABCD,eth”;
#address-cells=<1>;
#size-cells=<0>;
reg =<0xe0000000 0x3000>;
};
};
};
dts文件為:
Include ABCD.dtsi
/ {
mode = “ABCD corp Board”;
compatible = “ABCD,exp”;
chosen = {
bootargs = “console=ttySA0init=/linuxrc root=…”
};
apb@d0000000{
i2c0:i2cd0004000{
ak4951:code@5{
compatible=”A,ak4951”;
reg =<0x12>;
};
};
};
好了,設備樹構建完畢了,這里省去內存信息,中斷等很多其它內容,關於devicetree內容參看,http://www.devicetree.org/Main_Page,接着向下看,可以說devicetree的三分之一的內容體現在這里。
圖2.7.1 setup_arch()函數調用過程
Setup_arch函數里調用的第一個函數,也就是這里唯一出現等號的地方,這里mdesc,這就是和devicetree相關的一個非常重要的東西,
DT_MACHINE_START(exp_DT, “Aexp(Flattened Device Tree)”)
.restart_mode = ABCD _map_io,
.init_early= ABCD _init_early,
.init_irq=irqchip_init,
.init_machine=ABCD_init_machine,
.dt_compat= exp_dt_board_compat;
首先使用DT_MACHINE_START定義一個體系結構相關的函數,后續的的一些初始化函數將調用這里,這就有一個問題,Linux如何知道是這個呢,根據dt_compat,其指向一個compatible屬性的字段,結合上面dts文件的定義,定義如下的exp_dt_board_compat;
Static const char *const exp_dt_board_compat= {
“ABCD,exp”,
NULL,
}
直覺告訴我們,這兩者匹配上了,但是具體的匹配過程是什么呢?耐心往下看,
ach/arm/include/asm/mach/arch.h
#defineDT_MACHINE_START(_name, _namestr) \
static conststruct machine_desc __mach_desc_##_name \
__used \
__attribute__((__section__(".arch.info.init")))= { \
.nr =~0, \
.name =_namestr,
#endif
可以看到該宏定義的,將放在.arch.info.init段中,再來看setup_arch中獲得的對應代碼,這里我們使用該宏定義一個和上述devicetree相關的結構體出來,
for_each_machine_desc(mdesc){
score= of_flat_dt_match(dt_root, mdesc->dt_compat);
if(score > 0 && score < mdesc_score) {
mdesc_best= mdesc;
mdesc_score= score;
}
}
一看到到for就該笑了,就是從上面那個段,遍歷一遍查看字段是否相等就可以了,為了驗證我們的想法,向下看:
#define for_each_machine_desc(p) \
for(p = __arch_info_begin; p < __arch_info_end; p++)
這里用了一個非常巧妙的方法,__arch_info_begin和__arch_info_end在lds腳本里定義的,在鏈接時確定的,參看腳本arch/arm/kernel/vmlinux.lds.S的180行。
到此我們獲得了體系結構相關的初始化代碼,接着看of_get_flat_dt_root(),該函數獲得設備樹的根節點,這很好辦,因為前面話了很多篇幅說的r2就是設備樹的地址,但是如果看代碼,可能會覺得很復雜,這是因為設備樹在編譯時會加上頭信息,此外還要進行align操作。
setup_machine_fdt里其它函數的意義從字面上就可以看出來,這里就是針對該machine解析一些devicetree的字段,因為它們的優先級比較高,unflatten_device_tree()函數,中解析設備樹,並且前面提到的ofallnode的信息來自於此。那我們看看那些優先級不是很高的設備樹字段是如何使用的,這也是個初始化的過程哦,看前面設備樹中的mac字段,do_basic_setup()中調用驅動初始化相關函數,mac字段是以太網信息的表示,所以這里會進行以太網相關的初始化的工作了,整個devicetree對設備的處理都是按下面的方式處理的。
設備驅動的框架一般是,寫一個driver結構體,里面包括了設備的操作方法集,然后注冊該結構,該結構調用driver的probe方法。
module_platform_driver(ABCD_drvier);
struct staticplatform_drver ABCD_drvier={
.probe = ABCD_drv_probe;
.remove =
.drver ={
.name = “ABCD-eth”,
.own = THIS_MODULE,
.of_match_table=ABCD_eth_dt_ids,
},
};
static conststrut of_device_id ABCD_eth_dt_ids[]={
.compatible=”ABCD,eth”,
{ }
};
ABCD_probe(struct platform_device *pdev),該函數的參數中的pdev,該函數中有一個獲得devicetree的方法,如pdev->dev.of_node,這是匹配上,那就獲得上了該設備相關的信息,后面的過程類似以前的處理了,注冊向下進行了。
linux310啟動
由2.5.3知道了初始化主要在start_kernel中完成,
圖2.6.1 start_kernel初始化
上圖中可以知道,內存子系統已經初始化完畢了,后面初始化其它子系統時就可以直接申請內存了。
圖2.6.2 rest_init初始化流程
rest_init首先調用kernel_thread創建連個進程,Linux內核規定init進程的ID等於1,所以先創建kernel_init,但是進程號等於一的進程,但是ID號等於1的進程在結束時會創建內核守護進程,守護進程相關的初始化會由后面的kernel_thread(kthread…)創建,這里使用了competition機制在kernel_init獲得ID號1后,該進程處於等待狀態,等待kernel_thread(kthread…)初始化完畢。
接下來的do_basic_setup()中非常重要的兩個函數driver_init()和do_initcalls(),
前者完成總線等子系統的初始化,而后這則是根據如下initcall的優先級調用。
#define pure_initcall(fn) __define_initcall(fn,0)
#define core_initcall(fn) __define_initcall(fn,1)
#define core_initcall_sync(fn) __define_initcall(fn,1s)
#define postcore_initcall(fn) __define_initcall(fn,2)
#define postcore_initcall_sync(fn) __define_initcall(fn,2s)
#define arch_initcall(fn) __define_initcall(fn,3)
#define arch_initcall_sync(fn) __define_initcall(fn,3s)
#define subsys_initcall(fn) __define_initcall(fn,4)
#define subsys_initcall_sync(fn) __define_initcall(fn,4s)
#define fs_initcall(fn) __define_initcall(fn,5)
#define fs_initcall_sync(fn) __define_initcall(fn,5s)
#define rootfs_initcall(fn) __define_initcall(fn,rootfs)
#define device_initcall(fn) __define_initcall(fn,6)
#define device_initcall_sync(fn) __define_initcall(fn,6s)
#define late_initcall(fn) __define_initcall(fn,7)
#define late_initcall_sync(fn) __define_initcall(fn,7s)
根據initcall級別,調用相應的函數,如下:
后面調用2.4介紹的文件系統執行/sbin/init,該/init會執行/etc下的配置文件,內容在2.4節介紹過了,init最終啟動shell給用戶。
最后一行的init_idle_bootup_task(current);是創建ideal進程,可以知道該函數的ID等於0。
初始化中內存相關,請移步《內存管理-之啟動》
---------------------
作者:shichaog
來源:CSDN
原文:https://blog.csdn.net/shichaog/article/details/40218763
版權聲明:本文為博主原創文章,轉載請附上博文鏈接!
