Linux Kernel 0.12 啟動簡介,除錯記錄(Ubuntu1804, Bochs, gdb)
阿新 • • 發佈:2021-03-07
PS:要轉載請註明出處,本人版權所有。
PS: 這個只是基於《我自己》的理解,
如果和你的原則及想法相沖突,請諒解,勿噴。
###### 前置說明 本文作為本人csdn blog的主站的備份。(BlogID=102) ###### 環境說明 - Ubuntu 18.04 - gcc version 7.5.0 (Ubuntu 7.5.0-3ubuntu1~18.04) - Bochs 2.6 - As86 version: 0.16.17 ### 前言 --- 自從我近段時間開始溫習一些基礎知識以來,其中覺得以前學的很淺的就是OS原理。為啥這樣說呢?因為就是淺,知道一些瑣碎的知識。以前我自負的認為OS就是硬體的抽象,然後把這些硬體資源合理的分配給使用者使用就完了,因為我覺得合理的整合這些硬體資源是非常‘簡單’的。 由於我本身對底層是非常著迷的。帶著覺得OS很簡單的想法,想著去看看LinuxKernel的原始碼。在以前,我對LinuxKernel的認知很膚淺,就知道一些驅動移植的事情。如果硬要說一件我在LinuxKernel中玩的很深的事情,那就是自己理解並實現了一個類似Anonymous Shared Memory的Linux驅動,詳見以下兩篇文章。 - 《Android匿名共享記憶體(Anonymous Shared Memory) --- 瞎折騰記錄 (驅動程式篇)》 https://blog.csdn.net/u011728480/article/details/88420467 - 《linux kernel 中程序間描述符的傳遞方法及原理》 https://blog.csdn.net/u011728480/article/details/88553602 帶著這樣的想法其實已經很久了,由於現在的LinuxKernel太大了,對新手不友好。我就想著去找一個老一點的版本核心看看。結果去網上一找,就發現了前人已經做了許多許多了,比如這個之前就有了解的《linux 0.11核心完全註釋》,還比如其他許許多多前人種的‘樹’,看到了許多,最終我決定跟著國內現在比較好和新的資料從‘遠古’開始學習它。它就是《Linux核心完全註釋(PDF) v5.0 by 趙炯.pdf》。它是基於LinuxKernel0.12 講述的,它是我在ubuntu1804上編譯通過LinuxKernel0.12的主要參考和學習資料,同時也是我在Bochs上執行成功的主要參考和學習資料。 好的多說無益,直接看執行效果。
說來也慚愧,利用斷斷續續的時間,我花了約2月,把LinuxKernel0.12在Ubuntu1804上編譯通過,並在1804上通過Bochs執行成功。而且要命的事情是我其實只加了一些列印除錯函式,和根據實際的除錯情況修改了一些程式碼,卻花了那麼久的時間,搞得我很不自信了QAQ。
我修改好的原始碼已經開源,立即想要原始碼的請直接去文末兩個rep clone即可。
本文主要還是簡單介紹LinuxKernel從上電到進入sh的中間的簡要流程。這些流程網上已經有很多了,可能我會挑選一些我覺得比較重要的來說。
本文適用於:
- 會編譯和使用bochs的人。不會可以去網上找找,很多這方面的資料。
- 對Intel AT&T 彙編有點了解的人。
- 會GDB除錯的人。
- 知道C語言常識的人。
- 對LinuxKernel感興趣的人。
### 搭環境 --- 工欲善其事必先利其器。本文主要是在Ubuntu1804上編譯生成LinuxKernel,然後用Bochs執行我們的核心。
##### Ubuntu18.04環境安裝 我們應該首先安裝make,gcc,gcc-multilib,bin86。 - sudo apt install build-essential cmake make gcc-multilib g++-multilib module-assistant bin86 然後進入原始碼目錄。 - cd my_src - make disk 更多的詳情資訊檢視開源的rep。
##### 編譯兩個bochs版本備用 我們首先就得把Linux0.12的執行環境搭建起來,方便我們除錯。我們使用的是Bochs2.6 和 GDB遠端除錯。並編譯出兩個bochs版本,一個是帶本身除錯功能(命名為:bochs),一個是和gdb聯調(命名為:bochsdbg)。bochs 主要是除錯在init/main()函式之前的內容以及檢視更多的x86暫存器。 bochsdbg主要是除錯進入init/main()函式之後到sh成功執行的事情。 - 通過 ./configure --enable-debugger 生成bochs。 - 通過 ./configure --enable-gdb-stub 生成bochsdbg。
##### 執行我們編譯的核心 通過本文介紹生成的檔案是Linux核心映象,稍微懂點行的人都知道還差一個RootFS。這個檔案系統我們在網上下載的例如: http://oldlinux.org/Linux.old/bochs/linux-0.12-080324.zip 。本文生成的Linux核心映象使用的是rootimage-0.12-hd這個檔案系統。 我建議這裡自己配置兩個.bxrc檔案,一個對應bochs,一個對應bochsdbg遠端除錯。這樣在遇到問題的時候我們可以很方便的除錯。
### LinuxKernel啟動簡介 --- 本節簡述LinuxKernel的啟動流程。根據我近段時間的學習來看,這裡包含了許多的歷史性的東西,大家不要去細究為啥是這樣,很多都是為了相容。 此外在整個學習期間,由於涉及到許多的x86 硬體體系知識,除了參考上文我說的文件以外,還必須參考以下Intel官方文件: - Intel® 64 and IA-32 architectures software developer's manual combined volumes 2A, 2B, 2C, and 2D:Instruction set reference, A-Z - Intel® 64 and IA-32 architectures software developer's manual combined volumes 3A, 3B, 3C, and 3D: System programming guide - 《Linux核心完全註釋(PDF) v5.0 by 趙炯.pdf》 第4章,全篇精華。
##### boot/bootsect.S 階段 當我們的計算機上電以後,IntelCPU進入真實模式,並且PC指向了0xfff0整個地址,如下圖。什麼意思呢?就是開機的時候執行的第一句指令放在0xffff0這個地方,通常這裡有一個很重要的東西叫做BIOS。我們可以看到下圖,cs=0xf000,base=0xffff0000,在真實模式下面,cs:pc 就是真實的指向地址0xffff0。到了這裡不知道大家發現沒有,這裡還差一個東西,那就是bios本來是放在rom裡面的,怎麼被指向了記憶體地址0xffff0的地方呢?是誰在之前自動搬運的嗎?經過查詢後發現,大部分人說開機的時候,對特殊地址的訪問會被仲裁器件指向BIOS-ROM器件。仲裁器還可以把地址翻譯並指向我們熟悉的MEM和IO。所以這裡我理解對0xffff0的訪問就是對BIOS-ROM器件的直接訪問和執行。
BIOS主要是做自檢,並且在實體地址0x0開始初始化BIOS的中斷向量,同時通過BIOS訪問儲存裝置的中斷,將可啟動裝置的第一個扇區512位元組給搬運到絕對地址0x7c00(31k)處。然後跳轉到0x7c00繼續執行,這裡被搬運的512位元組就是bootsect.S生成的指令。這一段沒啥營養,都是一些約定好的,到了CPU執行到絕對地址0x7c00的時候,才是真正的我們能控制的地方。其實這裡也能夠看到,我們的bootsect.S生成的指令最大隻能夠512位元組,超過了就會出問題。下圖為我們的0x7c00處的開始幾句指令和bootsect.S的幾句指令,同時也能夠看到BIOS初始化和自檢列印的一些內容:
在上圖的圖中,我列印了0x7c00開始的一部分反彙編程式碼。可以看到和下面的bootsect.S的程式碼是一致的。
```asm
entry start
start:
! start at 0x07c0:0
! add by sky
mov ax,#BOOTSEG
mov es,ax
mov bp,#msg2 ! sky-notes: src-str is es:bp
mov si,#15 ! sky-notes: src-str-len is cx
call pirnt_str
! add by sky
mov ax,#BOOTSEG
mov ds,ax
mov ax,#INITSEG
mov es,ax
mov cx,#256
sub si,si
sub di,di
rep
movw
jmpi go,INITSEG
```
從0x7c00開始,就是我們自己的可以程式設計的領域了,也開始有了一些我自己特有的內容。主要是各種方法實現的print語句。這種除錯方法簡直不要太好。
下面簡要說明一下bootsect.S的功能:
- 首先用rep movw把自己從0x7c00搬運到0x90000,並跳轉cs=0x9000, pc=go 標號的地址。繼續執行剩下的內容。
- 通過讀取0x1E號中斷向量位置的軟碟機引數(由BIOS初始化時候通過BIOS中斷讀取的)到記憶體,然後修改其中的最大扇區數,並重新寫回到0x1E中斷向量位置絕對地址0x78去。最後重置軟碟機,使其載入最新的引數。
- 使用BIOS INT 0x13的2號功能,將第一個軟盤第2,3,4,5扇區讀取到0x90200開始的位置。這裡讀取的就是setup.S的指令內容,最大共2k(4*512)。0x90000-0x90200存放的是bootsect.S, 0x90200-0x90A00 為setup.S。
- 使用BIOS INT 0x13的8號功能,讀取磁碟引數:每磁軌扇區數。並儲存到變數sectors中。
- 使用BIOS INT 0x13的2號功能,使用剛剛的引數,讀取system模組到0x10000,我們的bootsect.S放在0x90000,所以我們system模組最大隻能夠佔用0x10000~0x8ffff。這裡的system模組就是除了bootsect和setup模組之外的所有核心程式碼。
- 判斷bootsect模組第508,509位元組是否為0,來判斷我們是否指定根檔案系統的裝置號。我們的核心定義為0x0301,代表第一個磁碟第一個分割槽為我們的根檔案系統。
- 然後通過jmpi 0:9020跳轉到cs=0x9020,pc=0的地方去執行setup.S的程式碼。
在我的bootsect模組,我定義了一個列印字串的函式,主要是通過使用BIOS INT 0x10的0x13號功能實現。主要還是為了除錯,注意,這裡不能夠隨意新增程式碼,因為生成的程式碼超過512byte後,連結器會報錯。只能夠少量的新增我們的除錯程式碼。
至此,我們就執行完了bootsect模組。本模組的主要內容還是載入setup和system到指定位置。bootsect執行的一些除錯日誌如下圖(在0x90200下斷點):
注意:圖中話框的部分就是我們上文貼出的call pirnt_str列印的。
##### boot/setup.S 階段 首先我們還是來看一下0x90200的位置是否是setup.S,換句話來說是否載入好了setup模組。
這裡和bootsect一樣,我也弄了一個prtstr函式,這個prtstr和bootsect裡面的是一樣的,原理也是一致的。
剛剛我們提到,setup是從0x90200開始存放的。那麼0x90000~0x901ff中的bootsect已經無用了,於是我們setup中,用這裡的記憶體存放一些引數。下面簡要說明一下setup.S的功能:
- 用BIOS INT 0x15功能號0x88取系統所含擴充套件記憶體大小並儲存在記憶體0x90002~0x90003處。共兩個位元組。
- 用BIOS INT 0x10功能號0x12讀取顯示卡引數,0x9000A 視訊記憶體大小,0x9000B 顯示卡型別(單色/彩色),0x9000C顯示卡特性引數。
- 用BIOS中斷讀取螢幕的行列存放到0x9000E 0x9000F
- 用BIOS INT 0x10功能號0x03讀取當前游標位置存放到0x90000 0x90001
- 用BIOS INT 0x10功能號0x0f讀取當前顯示頁,顯示模式,字元列數。 0x90004~0x90005 存放當前顯示頁。 0x90006 顯示模式, 0x90007 字元列數。
- 讀取第一個硬碟引數表和第二個硬碟引數表,並放到0x90080 0x90090。每個表共16byte。注意,這裡和之前的軟盤引數一樣,在BIOS自檢過程中,就被放到了中斷向量0x41 0x46 的位置。
- 用BIOS INT 0x13功能號0x15讀取當前硬碟裝置情況,如果硬碟2不存在,則把0x90090之後的16byte清零。
下面我們將使CPU從真實模式變更為保護模式,下面繼續說明一下setup.S的功能:
- 禁用中斷。
- 然後我們把system模組0x10000~0x8ffff整體下移到0x0開始的位置。就是把最大0x80000(512k)的system模組向下移動0x10000(64k)。
- 首先載入LDT和GDT。
- 開啟A20地址線,支援1M以上的記憶體。
- 初始化兩個8259A中斷控制器。
- 通過lmsw 設定cr0最低位位1,進入保護模式。
- 通過jmpi 0:0x8跳轉到絕對地址0x0開始執行system的程式碼。system是從boot/head.s開始的。
這裡需要說明幾個事情:
- 我們在下移system模組的時候,覆蓋了BIOS中斷向量表。所以通過BIOS中斷列印字串是行不通的。
- 在真實模式中,cs:pc就是真實執行的地址。但是在保護模式中,cs是一個選擇符號,根據選擇符號值不同,分表在GDT或者LDT中查詢對應的CS段描述符,其中最重要的就是base地址,當未開啟分頁的時候,這裡的base+pc就是我們真實的執行地址。上面我們載入了LDT和GDT。這裡的LDT是空,GDT有3項,第零項是空,第一項是程式碼段描述符,第二項是資料段描述符,他們的基地址都是0x0。當cs=0x08,ds=0x10時,分別指向這裡的第一項和第二項。
剛剛說了,system下移導致BIOS中斷向量表被沖掉了,於是我們不能夠通過BIOS列印字串,於是這裡我們使用的是直接操作視訊記憶體記憶體地址顯示字元,這個原理和LinuxKernel tty顯示原理差別不是很大。
這裡我們設計了print_str函式,通過直接操控視訊記憶體然後寫入字元進行顯示,這裡還使用到了剛剛我們儲存的當前游標位置(0x90000 0x90001)。寫這個主要還是為了除錯。
到此,我們已經開始去執行system的內容,其中head.s是入口。下圖是在0x0下斷點得到的setup模組的一些列印日誌。
這裡我們可以看到,紅框還是BIOS中斷列印的,黃框是通過直接操縱視訊記憶體顯示的。注意,我這裡設計的直接操作視訊記憶體的函式,是通過迴圈在當前視訊記憶體頁顯示的,並不是我們常見的整頁上移的方式。
##### boot/head.s 階段 首先我們還是來看一下0x0的位置是否是head.s,換句話來說是否載入好了system模組。並且,從這裡開始,我們就是進入了真正的LinuxKernel的世界,前面都是做一些環境初始化,都是一些固定的內容。
這裡我們需要說明的是,bootsect.S和setup.S用的是intel彙編,而從head.s開始,我們用的都是AT&T彙編。同理,這裡我也弄了一個safe_mode_print_str_no_page,列印字串,為了除錯,還是用的直接操作視訊記憶體的方式。
從這裡開始,CPU開始工作於保護模式,下面簡要介紹一下工作流程:
- 剛剛我們通過jmpi切換到0x0開始執行,這時cs=0x8,根據setup設定好的GDT,base為0x0,同理我們設定其他段暫存器。
- 設定堆疊為stack_start,這個就是核心堆疊。此符號定義於kernel/sched.c中,如下文。
```c
long user_stack [ PAGE_SIZE>>2 ] ;
struct {
long * a;
short b;
// } stack_start = { & user_stack [PAGE_SIZE>>2] , 0x10 };
} stack_start = { & user_stack , 0x10 };
```
- 設定IDT,所有的中斷向量指向ignore_int,一個預定義的中斷服務程式。共256項,每項8byte。
- 重新設定GDT。共256項,每項8byte。重新設定GDT的原因是setup的GDT可能會被沖掉,於是把GDT設定到合理的記憶體位置。這裡設定好的GDT有4個。和setup中類似。第0,3個為0.第1,2項為cs和ds的段描述符。
- 檢查A20是否開通,主要是通過判斷0x100000 和 0x0值是否相等。
- 檢查數學協處理器是否存在。
到這裡,我們就開始準備正式進入到init/main.c中的main函數了,但是還差最後一個重要的事情,那就是啟用分頁機制,下面繼續介紹其工作流程:
```asm
after_page_tables:
# sky print
push %ebp
lea msg5, %ebp
call safe_mode_print_str_no_page
pop %ebp
#
pushl $0 # These are the parameters to main :-)
pushl $0
pushl $0
pushl $L6 # return address for main, if it decides to.
pushl $main
jmp setup_paging
```
- 從上面的程式碼我們可只,我們在啟用分頁前,把init/main.c中的main函式地址設定到了堆疊中。
- 首先我們把從0x0開始的5頁記憶體清零。每頁4096位元組。其中第一頁為頁表目錄,第2-5頁為頁表。
- 設定頁表目錄的前4項為第2-5頁頁表地址。注意頁表目錄為1024項,每項4位元組。
- 倒序設定每一個頁表的每一項內容,第5頁最後一項為0xfff000。對映之後,2-5頁分別對映好了16MB記憶體的空間。
- 操作cr0,開啟分頁
- 通過ret指令,從堆疊中把main地址彈出去執行。
到這裡,我們正式進入到init/main.c中的main函式中,進入c語言相關程式碼的地界。下面是進入main之前的一些日誌輸出。
##### init/main.c 到進入shell 這裡我們進入了init/main.c中的main函式,可從下圖看到。從這裡開始,也是我們大家都熟知的Linux核心部分。
```c
void main(void) /* This really IS void, no error here. */
{ /* The startup routine assumes (well, ...) this */
/*
* Interrupts are still disabled. Do necessary setups, then
* enable them
*/
char _my_msg_buf[100];
sprintf(_my_msg_buf, "kernel main() start, root_dev=%x, swap_dev=%x ... ...\0", ORIG_ROOT_DEV, ORIG_SWAP_DEV);
__asm__ (
"push %%ebp\n\t"
"mov %0, %%ebp\n\t"
"call safe_mode_print_str_after_page\n\t"
"pop %%ebp\n\t"
:
:"p"((char *)&_my_msg_buf)
:);
ROOT_DEV = ORIG_ROOT_DEV;
SWAP_DEV = ORIG_SWAP_DEV;
sprintf(term, "TERM=con%dx%d", CON_COLS, CON_ROWS);
envp[1] = term;
envp_rc[1] = term;
drive_info = DRIVE_INFO;
memory_end = (1<<20) + (EXT_MEM_K<<10);
memory_end &= 0xfffff000;//align 4k
if (memory_end > 16*1024*1024)//if memory_end > 16MB, set it to be 16 MB
memory_end = 16*1024*1024;
if (memory_end > 12*1024*1024)
buffer_memory_end = 4*1024*1024;
else if (memory_end > 6*1024*1024)
buffer_memory_end = 2*1024*1024;
else
buffer_memory_end = 1*1024*1024;
main_memory_start = buffer_memory_end;
sprintf(_my_msg_buf, "Mem size is %x, buf-mem size is %x, main-mem start %x ... ...\0", memory_end, main_memory_start, buffer_memory_end);
__asm__ (
"push %%ebp\n\t"
"mov %0, %%ebp\n\t"
"call safe_mode_print_str_after_page\n\t"
"pop %%ebp\n\t"
:
:"p"((char *)&_my_msg_buf)
:);
#ifdef RAMDISK
sprintf(_my_msg_buf, "ramdisk init ... ...\0");
__asm__ (
"push %%ebp\n\t"
"mov %0, %%ebp\n\t"
"call safe_mode_print_str_after_page\n\t"
"pop %%ebp\n\t"
:
:"p"((char *)&_my_msg_buf)
:);
main_memory_start += rd_init(main_memory_start, RAMDISK*1024);
#endif
sprintf(_my_msg_buf, "memory init ... ...\0");
__asm__ (
"push %%ebp\n\t"
"mov %0, %%ebp\n\t"
"call safe_mode_print_str_after_page\n\t"
"pop %%ebp\n\t"
:
:"p"((char *)&_my_msg_buf)
:);
mem_init(main_memory_start,memory_end);
sprintf(_my_msg_buf, "trap init ... ...\0");
__asm__ (
"push %%ebp\n\t"
"mov %0, %%ebp\n\t"
"call safe_mode_print_str_after_page\n\t"
"pop %%ebp\n\t"
:
:"p"((char *)&_my_msg_buf)
:);
trap_init();
sprintf(_my_msg_buf, "blk init ... ...\0");
__asm__ (
"push %%ebp\n\t"
"mov %0, %%ebp\n\t"
"call safe_mode_print_str_after_page\n\t"
"pop %%ebp\n\t"
:
:"p"((char *)&_my_msg_buf)
:);
blk_dev_init();
sprintf(_my_msg_buf, "chr init ... ...\0");
__asm__ (
"push %%ebp\n\t"
"mov %0, %%ebp\n\t"
"call safe_mode_print_str_after_page\n\t"
"pop %%ebp\n\t"
:
:"p"((char *)&_my_msg_buf)
:);
chr_dev_init();
sprintf(_my_msg_buf, "tty init ... ...\0");
__asm__ (
"push %%ebp\n\t"
"mov %0, %%ebp\n\t"
"call safe_mode_print_str_after_page\n\t"
"pop %%ebp\n\t"
:
:"p"((char *)&_my_msg_buf)
:);
tty_init();
printk("time init ... ...\n\r");
time_init();
printk("sched init ... ...\n\r");
sched_init();
/*
After sched_init()
gdt[0] = NULL
gdt[1] = kernel cs
gdt[2] = kernel ds
gdt[3] = NULL
gdt[4] = task0.tss
gdt[5] = task0.ldt
tr=task0.tss
ldtr=task0.ldt
*/
printk("buffer init ... ...\n\r");
buffer_init(buffer_memory_end);
printk("hd init ... ...\n\r");
hd_init();
printk("floppy init ... ...\n\r");
floppy_init();
printk("enable interrupts ... ...\n\r");
sti();
printk("go to user mode ... ...\n\r");
/*
movl %%esp,%%eax
pushl $0x17
pushl %%eax
pushfl
pushl $0x0f
pushl $1f
iret
1:
movl $0x17,%%eax
mov %%ax,%%ds
mov %%ax,%%es
mov %%ax,%%fs
mov %%ax,%%gs
iret instruction will do follow op:
popl eip
popl cs
popl eflag
popl esp
popl ss
*/
move_to_user_mode();
printf("user_mode: fork() task0 ... ...");
if (!fork()) { /* we count on this going ok */
printf("user_mode: task1 call init ... ...");
init();
}
/*
* NOTE!! For any other task 'pause()' would mean we have to get a
* signal to awaken, but task0 is the sole exception (see 'schedule()')
* as task 0 gets activated at every idle moment (when no other tasks
* can run). For task0 'pause()' just means we go check if some other
* task can run, and if not we return here.
*/
printf("user_mode: task0 call sys_pause() in while ... ...");
for(;;)
__asm__("int $0x80"::"a" (__NR_pause):);
}
```
注意,這裡我們仍然設計了一個函式為safe_mode_print_str_after_page,通過直接操作視訊記憶體進行顯示字串,知道tty_init之後,我們才能夠呼叫printk類似的函式進行列印。
下面簡要介紹一下main函式主要做的事情:
- 根據我們在setup中儲存到記憶體中的記憶體引數初始化高速緩衝區和主存的位置。
- 然後就是我們常見的初始化mm模組。
- 初始化中斷向量。
- 初始化塊裝置。
- 初始化字串裝置。
- 初始化tty裝置。
- 初始化時間。
- 初始化排程模組。
- 初始化緩衝區。
- 初始化硬碟。
- 初始化軟盤。
- 開啟中斷。
- 把當前任務切換到使用者態。
當我們切換到使用者態之後,並且當前我們的程序是0號程序,我們核心的一些重要初始化基本設定完畢。然後就像我們常見的linux程式設計那樣,通過fork,建立我們的1號程序。然後我們繼續進行下面的事情:
- task0在fork出task1之後,就迴圈呼叫sys_pause, 這裡主要還是執行schedule()開始執行程序排程。
- task1成功建立後,呼叫setup,開始載入根檔案系統。然後task1 通過fork建立了task2。
- task2通過execve開始執行/bin/sh,進入shell。後續就是一些其他的事情。
到這裡,我們已經把kernel跑起來了。在我除錯的過程中,主要還是mm模組和schedule模組有些問題,可能和編譯器版本有關係,反正我生成的程式碼,總會報錯。哪怕到現在,我開源出來的我修改的核心,也非常的不穩定,經常崩潰。但是好在正常工作了。
下面給出兩種不同列印的日誌:
##### tool/build.c 此工具是生成LinuxKernel映象的手段。但是我們在Ubuntu上生成的核心,由於gcc版本變更的原因,需要做一些變更。主要還是把生成的elf格式system模組通過objcopy 生成二進位制記憶體映象。主要原因就是elf格式需要一個elf載入器進行各個段的重定位,但是由於我們是核心,所以沒有。詳情,請檢視tool/build.c 及 Makefile。
### 開源 --- https://github.com/flyinskyin2013/LinuxKernel-src0.12 https://gitee.com/sky-X/LinuxKernel-src0.12 (映象)
### 後記 --- 為啥想要在ubuntu1804環境下弄這個東西呢?一方面是想學習一下,通過踩坑的方式加深自己的理解。另一方面還是太懶了,我只想在我的ubuntu1804上編譯核心,不想安裝其他虛擬機器了,我的電腦太卡了(畢竟8年的電腦了QAQ)。 經過了這一波除錯,我對LinuxKernel有了更深的認知,我覺得很不錯,如果以後有必要,我還可以分別對這些模組進行詳細的檢視,在這裡,我只是簡單的說明了init/main中的內容,其實,還有許多其他的內容是執行在背後的。比如system_call,sys_table等等內容。還有do_fork do_execve等等內容都是我在除錯過程中踩過的坑。 這裡還是要說明,深入除錯學習這個的原因還是想看看OS是怎麼執行起來,雖然不能說已經100%的熟知,但是也可管中窺豹。 注意,這個版本的核心和現代的2.0,4.0,5.0還缺了一些主要的知識,比如網路棧,VFS等。但是其他的一些內容,在現在的最新核心中,多多少少都能夠看到這個版本的一些影子。這也是學習這個核心的原因之一。
打賞、訂閱、收藏、丟香蕉、硬幣,請關注公眾號(攻城獅的搬磚之路)
PS: 請尊重原創,不喜勿噴。
PS: 要轉載請註明出處,本人版權所有。
PS: 有問題請留言,看到後我會第一時間回
PS: 這個只是基於《我自己》的理解,
如果和你的原則及想法相沖突,請諒解,勿噴。
###### 前置說明 本文作為本人csdn blog的主站的備份。(BlogID=102) ###### 環境說明 - Ubuntu 18.04 - gcc version 7.5.0 (Ubuntu 7.5.0-3ubuntu1~18.04) - Bochs 2.6 - As86 version: 0.16.17 ### 前言 --- 自從我近段時間開始溫習一些基礎知識以來,其中覺得以前學的很淺的就是OS原理。為啥這樣說呢?因為就是淺,知道一些瑣碎的知識。以前我自負的認為OS就是硬體的抽象,然後把這些硬體資源合理的分配給使用者使用就完了,因為我覺得合理的整合這些硬體資源是非常‘簡單’的。 由於我本身對底層是非常著迷的。帶著覺得OS很簡單的想法,想著去看看LinuxKernel的原始碼。在以前,我對LinuxKernel的認知很膚淺,就知道一些驅動移植的事情。如果硬要說一件我在LinuxKernel中玩的很深的事情,那就是自己理解並實現了一個類似Anonymous Shared Memory的Linux驅動,詳見以下兩篇文章。 - 《Android匿名共享記憶體(Anonymous Shared Memory) --- 瞎折騰記錄 (驅動程式篇)》 https://blog.csdn.net/u011728480/article/details/88420467 - 《linux kernel 中程序間描述符的傳遞方法及原理》 https://blog.csdn.net/u011728480/article/details/88553602 帶著這樣的想法其實已經很久了,由於現在的LinuxKernel太大了,對新手不友好。我就想著去找一個老一點的版本核心看看。結果去網上一找,就發現了前人已經做了許多許多了,比如這個之前就有了解的《linux 0.11核心完全註釋》,還比如其他許許多多前人種的‘樹’,看到了許多,最終我決定跟著國內現在比較好和新的資料從‘遠古’開始學習它。它就是《Linux核心完全註釋(PDF) v5.0 by 趙炯.pdf》。它是基於LinuxKernel0.12 講述的,它是我在ubuntu1804上編譯通過LinuxKernel0.12的主要參考和學習資料,同時也是我在Bochs上執行成功的主要參考和學習資料。 好的多說無益,直接看執行效果。
### 搭環境 --- 工欲善其事必先利其器。本文主要是在Ubuntu1804上編譯生成LinuxKernel,然後用Bochs執行我們的核心。
##### Ubuntu18.04環境安裝 我們應該首先安裝make,gcc,gcc-multilib,bin86。 - sudo apt install build-essential cmake make gcc-multilib g++-multilib module-assistant bin86 然後進入原始碼目錄。 - cd my_src - make disk 更多的詳情資訊檢視開源的rep。
##### 編譯兩個bochs版本備用 我們首先就得把Linux0.12的執行環境搭建起來,方便我們除錯。我們使用的是Bochs2.6 和 GDB遠端除錯。並編譯出兩個bochs版本,一個是帶本身除錯功能(命名為:bochs),一個是和gdb聯調(命名為:bochsdbg)。bochs 主要是除錯在init/main()函式之前的內容以及檢視更多的x86暫存器。 bochsdbg主要是除錯進入init/main()函式之後到sh成功執行的事情。 - 通過 ./configure --enable-debugger 生成bochs。 - 通過 ./configure --enable-gdb-stub 生成bochsdbg。
##### 執行我們編譯的核心 通過本文介紹生成的檔案是Linux核心映象,稍微懂點行的人都知道還差一個RootFS。這個檔案系統我們在網上下載的例如: http://oldlinux.org/Linux.old/bochs/linux-0.12-080324.zip 。本文生成的Linux核心映象使用的是rootimage-0.12-hd這個檔案系統。 我建議這裡自己配置兩個.bxrc檔案,一個對應bochs,一個對應bochsdbg遠端除錯。這樣在遇到問題的時候我們可以很方便的除錯。
### LinuxKernel啟動簡介 --- 本節簡述LinuxKernel的啟動流程。根據我近段時間的學習來看,這裡包含了許多的歷史性的東西,大家不要去細究為啥是這樣,很多都是為了相容。 此外在整個學習期間,由於涉及到許多的x86 硬體體系知識,除了參考上文我說的文件以外,還必須參考以下Intel官方文件: - Intel® 64 and IA-32 architectures software developer's manual combined volumes 2A, 2B, 2C, and 2D:Instruction set reference, A-Z - Intel® 64 and IA-32 architectures software developer's manual combined volumes 3A, 3B, 3C, and 3D: System programming guide - 《Linux核心完全註釋(PDF) v5.0 by 趙炯.pdf》 第4章,全篇精華。
##### boot/bootsect.S 階段 當我們的計算機上電以後,IntelCPU進入真實模式,並且PC指向了0xfff0整個地址,如下圖。什麼意思呢?就是開機的時候執行的第一句指令放在0xffff0這個地方,通常這裡有一個很重要的東西叫做BIOS。我們可以看到下圖,cs=0xf000,base=0xffff0000,在真實模式下面,cs:pc 就是真實的指向地址0xffff0。到了這裡不知道大家發現沒有,這裡還差一個東西,那就是bios本來是放在rom裡面的,怎麼被指向了記憶體地址0xffff0的地方呢?是誰在之前自動搬運的嗎?經過查詢後發現,大部分人說開機的時候,對特殊地址的訪問會被仲裁器件指向BIOS-ROM器件。仲裁器還可以把地址翻譯並指向我們熟悉的MEM和IO。所以這裡我理解對0xffff0的訪問就是對BIOS-ROM器件的直接訪問和執行。
##### boot/setup.S 階段 首先我們還是來看一下0x90200的位置是否是setup.S,換句話來說是否載入好了setup模組。
##### boot/head.s 階段 首先我們還是來看一下0x0的位置是否是head.s,換句話來說是否載入好了system模組。並且,從這裡開始,我們就是進入了真正的LinuxKernel的世界,前面都是做一些環境初始化,都是一些固定的內容。
##### init/main.c 到進入shell 這裡我們進入了init/main.c中的main函式,可從下圖看到。從這裡開始,也是我們大家都熟知的Linux核心部分。
##### tool/build.c 此工具是生成LinuxKernel映象的手段。但是我們在Ubuntu上生成的核心,由於gcc版本變更的原因,需要做一些變更。主要還是把生成的elf格式system模組通過objcopy 生成二進位制記憶體映象。主要原因就是elf格式需要一個elf載入器進行各個段的重定位,但是由於我們是核心,所以沒有。詳情,請檢視tool/build.c 及 Makefile。
### 開源 --- https://github.com/flyinskyin2013/LinuxKernel-src0.12 https://gitee.com/sky-X/LinuxKernel-src0.12 (映象)
### 後記 --- 為啥想要在ubuntu1804環境下弄這個東西呢?一方面是想學習一下,通過踩坑的方式加深自己的理解。另一方面還是太懶了,我只想在我的ubuntu1804上編譯核心,不想安裝其他虛擬機器了,我的電腦太卡了(畢竟8年的電腦了QAQ)。 經過了這一波除錯,我對LinuxKernel有了更深的認知,我覺得很不錯,如果以後有必要,我還可以分別對這些模組進行詳細的檢視,在這裡,我只是簡單的說明了init/main中的內容,其實,還有許多其他的內容是執行在背後的。比如system_call,sys_table等等內容。還有do_fork do_execve等等內容都是我在除錯過程中踩過的坑。 這裡還是要說明,深入除錯學習這個的原因還是想看看OS是怎麼執行起來,雖然不能說已經100%的熟知,但是也可管中窺豹。 注意,這個版本的核心和現代的2.0,4.0,5.0還缺了一些主要的知識,比如網路棧,VFS等。但是其他的一些內容,在現在的最新核心中,多多少少都能夠看到這個版本的一些影子。這也是學習這個核心的原因之一。
PS: 要轉載請註明出處,本人版權所有。
PS: 有問題請留言,看到後我會第一時間回