內核使用c語言編寫,使用gcc編譯ios
ELF文件是指可執行廉潔個事,最初由UNIX系統實驗室做爲應用程序二進制接口開發和發行的shell
EFL文件類型:bash
程序中又很短的段,如代碼段,數據段等,一樣也有不少節,段是由節來組成的多個節通過鏈接過程,被合併成爲一個段.函數
跳oop
首先須要一個保護模式下,讀取磁盤數據的函數,將內核的可執行文件的內容拷貝到內存.ui
而後須要一個函數,解析可執行文件的內容,將不一樣段中的內容,根據表頭中定義的地址,拷貝到指定的地址上指針
最後,jmp到入口地址執行便可code
與實模式下的幾乎徹底相同,惟一的區別是,在將讀取的內容寫入內存的時候,使用的是32位寄存器,不在是16位寄存器.接口
; 功能:保護模式下讀取硬盤n個扇區 ; 參數: ; eax:開始讀取的磁盤扇區 ; cx:讀取的扇區個數 ; bx:數據送到內存中的起始位置 rd_disk_m_32: ; 這裏要保存eax 的緣由在與,下面section count 寄存器須要一個8位的寄存器 ; 只有acbd這四個寄存器可以拆分爲高低8位來使用,而dx做爲寄存器號,被佔用了 ; 所以須要個abc三個寄存器中一個來用,這裏選擇了 ax mov esi,eax mov di,cx ; 0x1f2 寄存器:sector count ,讀寫的時候都表示要讀寫的扇區數目 ; 該寄存器是8位的,所以送入的數據位 cl mov dx,0x1f2 mov al,cl out dx,al ; 恢復eax mov eax,esi ; eax中存放的是要讀取的扇區開始標號,是一個32位的值,所以 al 是低8位 ; 0x1f3 存放0~7位的LBA地址,該寄存器是一個8位的 mov dx,0x1f3 out dx,al ; 下面的 0x1f4 和5 分別是8~15,16~23位LBA地址,這倆寄存器都是8位的 ; 所以是用shr,將eax右移8位,而後每次都用al取eax中的低8位 mov cl,8 shr eax,cl mov dx,0x1f4 out dx,al shr eax,cl mov dx,0x1f5 out dx,al ; 0x1f6 寄存器低4位存放 24~27位LBA地址, ; 0x1f6 寄存器是一個雜項,其第六位,1標識LBA地址模式,0標識CHS模式 ; 上面使用的是LBA地址,所以第六位位1 shr eax,cl and al,0x0f or al,0xe0 mov dx,0x1f6 out dx,al ; 0x1f7 寄存器,讀取該寄存器的時候,其中的數據是磁盤的狀態 ; 寫到該寄存器的時候,寫入的殭屍要執行的命令,寫入之後,直接開始執行命令 ; 所以須要在寫該寄存器的時候,將全部參數設置號 ; 0x20 表示讀扇區,0x30寫扇區 mov dx,0x1f7 mov al,0x20 out dx,al .not_ready: ; 讀 0x1f7 判斷數據是否就緒,沒就緒就循環等待. nop in al,dx and al,0x88 cmp al,0x08 jnz .not_ready ; 到這一步表示數據就緒,設置各項數據,開始讀取 ; 一個扇區512字節,每次讀2字節,所以讀一個扇區須要256次從寄存器中讀取數據 ; di 中是最開始的cx也就是要讀取的扇區數 ; mul dx 是ax=ax * dx ,所以最終ax 中是要讀取的次數 mov ax,di mov dx,256 mul dx mov cx,ax ; 0x1f0 寄存器是一個16位寄存器,讀寫的時候,都是數據. mov dx,0x1f0 .go_on_read: in ax,dx mov [ebx],ax ;區別在這裏,使用的是ebx add ebx,2 ;ax 是 16位寄存器,讀出的也是2字節,所以讀一次 dx+2 loop .go_on_read ret
在.go_on_read
中,使用的是[ebx]
不在是實模式下的bx
,由於保護模式下,尋址,須要32位的寄存器,不能再使用16位寄存器進程
爲了之後便於維護,如今將文件歸類:
文件結構:
└── bochs
├── 02.tar.gz
├── 03.tar.gz
├── 04.tar.gz
├── 05a.tar.gz
├── 05b.tar.gz
├── 05c
├── boot
│ ├── include
│ │ └── boot.inc
│ ├── loader.asm
│ └── mbr.asm
├── build
└── start.sh
添加和加載內核文件,解析內核文件,以及跳轉執行內核的常數的宏
; -------------------------------------loader.bin ------------------------------------- ; 將要加載在內存的位置,和在虛擬磁盤的扇區位置 LOADER_IN_MEM equ 0x900 LOADER_IN_DISK equ 2 ; loader 執行的棧基址 LOADER_STACK_TOP equ LOADER_IN_MEM ; -------------------------------------loader.bin ------------------------------------- ; ------------------------------------構造GDT須要的數據 ---------------------------------- ; 16位爲一組,最後經過位操做拼接. GDT_48_G_4K equ 00000000_10000000b GDT_48_D_32 equ 00000000_01000000b GDT_48_L_32 equ 00000000_00000000b GDT_48_AVL equ 00000000_00000000b GDT_48_LEN_H equ 00000000_00001111b GDT_32_P equ 10000000_00000000b GDT_32_DPL_0 equ 00000000_00000000b GDT_32_DPL_3 equ 01100000_00000000b GDT_32_S_SYS equ 00000000_00000000b GDT_32_S_USER equ 00010000_00000000b GDT_32_TYPE_CODE equ 00001000_00000000b GDT_32_TYPE_DATA equ 00000010_00000000b ; -----------------------------------構造GDT須要的數據 ---------------------------------- ; -----------------------------------三個段描述符標表項 ---------------------------------- GDT_CODE_H32 equ (((00000000b<<8)+GDT_48_G_4K+GDT_48_D_32+GDT_48_L_32+GDT_48_AVL+GDT_48_LEN_H)<<16)+ \ (GDT_32_P+GDT_32_DPL_0+GDT_32_S_USER+GDT_32_TYPE_CODE+00000000b) GDT_DATA_H32 equ (((00000000b<<8)+GDT_48_G_4K+GDT_48_D_32+GDT_48_L_32+GDT_48_AVL+GDT_48_LEN_H)<<16)+ \ (GDT_32_P+GDT_32_DPL_0+GDT_32_S_USER+GDT_32_TYPE_DATA+00000000b) GDT_VGA_H32 equ (((00000000b<<8)+GDT_48_G_4K+GDT_48_D_32+GDT_48_L_32+GDT_48_AVL+00000000_00000000b)<<16)+ \ (GDT_32_P+GDT_32_DPL_0+GDT_32_S_USER+GDT_32_TYPE_DATA+00001011b) GDT_BASE equ 00000000b<<(24+32) GDT_CODE equ (GDT_CODE_H32<<32)+0x0000FFFF GDT_DATA equ (GDT_DATA_H32<<32)+0x0000FFFF GDT_VGA equ (GDT_VGA_H32<<32 )+0x80000007 ; -----------------------------------三個段描述符標表項 ---------------------------------- ; -----------------------------------構造選擇子須要的數據 ---------------------------------- SELECT_RPL_0 equ 00b SELECT_RPL_3 equ 11b SELECT_TI_GDT equ 000b SELECT_TI_LDT equ 100b ; -----------------------------------構造選擇子須要的數據 ---------------------------------- ; ----------------------------------- 分頁機制 ---------------------------------- PAGE_DIR_TABLE_POS equ 0x100000 PAGE_P equ 1b PAGE_RW_R equ 00b PAGE_RW_W equ 10b PAGE_US_S equ 000b PAGE_US_U equ 100b ; ----------------------------------- 分頁機制 ---------------------------------- ; ----------------------------------- 內核加載 ---------------------------------- KERNEL_BIN_IN_MEM equ 0x70000 KERNEL_BIN_IN_DISK equ 0x9 KERNEL_ENTRY equ 0xc0001500 ELF_PT_NULL equ 0 ; ----------------------------------- 內核加載 ----------------------------------
該文件無變化.
該文件變更較大:
kernel_init
函數用來解析內存中的ELF文件,而後將各個段複製到指定的內存位置,所以新加了兩個函數,一個kernel_init
和memcpy
jmp
一共新添加了3段代碼.
%include "boot.inc" SECTION loader vstart=LOADER_IN_MEM ; 上來就跳轉 jmp 0:loader_start gdt_base: dq GDT_BASE gdt_code: dq GDT_CODE gdt_data: dq GDT_DATA gdt_vga: dq GDT_VGA gdt_size equ $-gdt_base ; 這裏預留出 60 個段描述符的位置 times 60 dq 0 ; 界限,也就是全局段描述符表在內存中的地址位:gdt_base + 界限 ; 所以界限=長度-1 gdt_ptr dw (gdt_size-1) dd gdt_base ;構建選擇子 select_code equ (0x1<<3)+SELECT_TI_GDT+SELECT_RPL_0 select_data equ (0x2<<3)+SELECT_TI_GDT+SELECT_RPL_0 select_vag equ (0x3<<3)+SELECT_TI_GDT+SELECT_RPL_0 ards_buf times 240 db 0 ; ARDS結構,這裏定義了240/20個 mem_total dd 0 ; 存放最終結果的內存區域 ards_counts dw 0 ; 最後須要找到全部的ards結構中長度最大的那個內存最爲物理內存,所以須要一個遍歷 loader_start: ; --------------------------------獲取物理內存大小-------------------------------- xor ebx,ebx ;初始ebx爲0,告訴bios從頭開始遍歷每種類型的內存,後續該寄存器的值由bios更新,由bios使用 mov edx,0x534d4150 mov di,ards_buf .get_mem_size_e820: mov eax,0x0000e820 ; 子功能號 mov ecx,20 ; 每次填充的大小,相似於指示可用buf區大小的意思 int 0x15 add di,cx ; di中保存的是buf inc word [ards_counts] ;計數 cmp ebx,0 jnz .get_mem_size_e820 ; 在全部的ards結構中找內存最大值 mov ebx,ards_buf ;ebx 中存放的是地址 mov ecx,[ards_counts] ;ecx 中存放的是次數,須要取地址 xor edx,edx ;保存當前最大值 .find_max_mem: mov eax,[ebx] add eax,[ebx+8] add ebx,20 cmp edx,eax jge .next_ards mov edx,eax .next_ards: loop .find_max_mem jmp .set_max_mem .set_max_mem: mov [mem_total],edx ;最終結果存放 ; --------------------------------獲取物理內存大小-------------------------------- ; --------------------------------打印字符-------------------------------- mov ax,0xb800 mov gs,ax mov byte [gs:1120],'l' mov byte [gs:1121],00000111b mov byte [gs:1122],'o' mov byte [gs:1123],00000111b mov byte [gs:1124],'a' mov byte [gs:1125],00000111b mov byte [gs:1126],'d' mov byte [gs:1127],00000111b mov byte [gs:1128],'e' mov byte [gs:1129],00000111b mov byte [gs:1130],'r' mov byte [gs:1131],00000111b mov byte [gs:1132],'!' mov byte [gs:1133],00000111b ; --------------------------------打印字符-------------------------------- ; --------------------------------開啓分段模式-------------------------------- ; 開啓A20 in al,0x92 or al,000000010b out 0x92,al ; 開啓保護模式 mov eax,cr0 or eax,0x00000001 mov cr0,eax ; 加載GDT lgdt [gdt_ptr] ; --------------------------------開啓分段模式-------------------------------- ; 流水線的緣由,要強制刷新一次流水線 ; 這裏要用遠跳轉,由於進入了保護模式了,cs寄存器中存的不在是段基址,而是選擇子 jmp select_code:p_mode ; 主動告訴編譯器下面的代碼按照32位機器碼編譯 [bits 32] p_mode: ; 注意這裏ss段寄存器,也被賦值爲 select_data mov ax,select_data mov ds,ax mov es,ax mov ss,ax mov esp,LOADER_STACK_TOP mov ax,select_vag mov gs,ax mov byte [gs:1440],'p' ; ----------------------------------- 加載內核文件到內存 ----------------------------------- ; 加載內核的ELF文件 mov eax,KERNEL_BIN_IN_DISK mov ebx,KERNEL_BIN_IN_MEM mov ecx,200 call rd_disk_m_32 mov byte [gs:1442],'r' ; ----------------------------------- 加載內核文件到內存 ----------------------------------- ; 構建頁表目錄 call setup_page ; 修改全局描述符表 sgdt [gdt_ptr] mov ebx,[gdt_ptr+2] or dword [ebx+0x18+4],0xc0000000 add dword [gdt_ptr+2],0xc0000000 add esp,0xc0000000 ; 將頁表目錄放入cr3 mov eax,PAGE_DIR_TABLE_POS mov cr3,eax ; 打開cr0的pg位 mov eax,cr0 or eax,0x80000000 mov cr0,eax ; 從新加載 全局描述符表 lgdt [gdt_ptr] mov byte [gs:1600],'V' ; ----------------------------------- 解析內核文件 ----------------------------------- ; 解析內核文件,病跳轉執行 call kernel_init ; 須要設置棧指針 mov esp,0xc009f000 mov byte [gs:1760],'K' jmp KERNEL_ENTRY ; ----------------------------------- 解析內核文件 ----------------------------------- ; ----------------------------------- 構建內核頁表目錄 ----------------------------------- setup_page: ; 首先使用 clear_page 將頁表目錄所在的那一頁 PAGE_DIR_TABLE_POS 開始 ; 的4K空間清零 mov ecx,4096 mov esi,0 .clear_page: mov byte [PAGE_DIR_TABLE_POS+esi],0 inc esi loop .clear_page ; 將內核頁表目錄的第一個頁表和內核在3G處的頁表設爲同一個,爲了是在一進入分頁機制後,虛擬地址映射正常 .create_pde: mov eax,PAGE_DIR_TABLE_POS add eax,4096 ; 頁表目錄後面緊跟着就是第一個頁表 eax+4K, mov ebx,eax ; 所以eax中存的是第一個頁表的地址 or eax,PAGE_US_U|PAGE_RW_W|PAGE_P ; eax中是第一個頁表的地址,而後設置屬性,讓他成爲第一個頁表項 mov [PAGE_DIR_TABLE_POS],eax ; 將第一個頁表放在頁表目錄的第0個表項和高1G內存開始的地方. mov [PAGE_DIR_TABLE_POS+768*4],eax ;768=3*1024*1024 /(4*1024),因此高1G是在第768個頁表,*4是由於,每一個表項4字節 sub eax,4096 ; eax是第一個頁表的地址,減去4KB,就是 PAGE_DIR_TABLE_POS mov [PAGE_DIR_TABLE_POS+4092],eax ; 將頁表目錄最後一項設爲本身的地址 ; 填充第一個頁表 mov ecx,256 mov esi,0 mov edx,PAGE_US_U|PAGE_RW_W|PAGE_P ; edx如今是第一個頁表中的第一個表項,他表明的是物理內存的第一頁. .create_pte: mov [ebx+esi*4],edx ; 將物理內存從0開始依次映射到第一個頁表中 add edx,4096 inc esi loop .create_pte ; 填充高1G的其餘頁表目錄項,頁表目錄所在內存頁的後面頁就放着頁表,也就是說這些頁表,在物理內存上是連續的. mov eax,PAGE_DIR_TABLE_POS add eax,4096*2 ; eax 中如今是第一個頁表的地址 or eax,PAGE_US_U|PAGE_RW_W|PAGE_P mov ebx,PAGE_DIR_TABLE_POS mov ecx,254 ;第一個和最後一個已經建立完了,因此是256-2個 mov esi,769 ;是高1G的第2個頁表 .create_kernel_pde: mov [ebx,esi*4],eax inc esi add eax,4096 loop .create_kernel_pde ret ; ----------------------------------- 讀取磁盤文件 ----------------------------------- ; 功能:保護模式下讀取硬盤n個扇區 ; 參數: ; eax:開始讀取的磁盤扇區 ; cx:讀取的扇區個數 ; bx:數據送到內存中的起始位置 rd_disk_m_32: ; 這裏要保存eax 的緣由在與,下面section count 寄存器須要一個8位的寄存器 ; 只有acbd這四個寄存器可以拆分爲高低8位來使用,而dx做爲寄存器號,被佔用了 ; 所以須要個abc三個寄存器中一個來用,這裏選擇了 ax mov esi,eax mov di,cx ; 0x1f2 寄存器:sector count ,讀寫的時候都表示要讀寫的扇區數目 ; 該寄存器是8位的,所以送入的數據位 cl mov dx,0x1f2 mov al,cl out dx,al ; 恢復eax mov eax,esi ; eax中存放的是要讀取的扇區開始標號,是一個32位的值,所以 al 是低8位 ; 0x1f3 存放0~7位的LBA地址,該寄存器是一個8位的 mov dx,0x1f3 out dx,al ; 下面的 0x1f4 和5 分別是8~15,16~23位LBA地址,這倆寄存器都是8位的 ; 所以是用shr,將eax右移8位,而後每次都用al取eax中的低8位 mov cl,8 shr eax,cl mov dx,0x1f4 out dx,al shr eax,cl mov dx,0x1f5 out dx,al ; 0x1f6 寄存器低4位存放 24~27位LBA地址, ; 0x1f6 寄存器是一個雜項,其第六位,1標識LBA地址模式,0標識CHS模式 ; 上面使用的是LBA地址,所以第六位位1 shr eax,cl and al,0x0f or al,0xe0 mov dx,0x1f6 out dx,al ; 0x1f7 寄存器,讀取該寄存器的時候,其中的數據是磁盤的狀態 ; 寫到該寄存器的時候,寫入的殭屍要執行的命令,寫入之後,直接開始執行命令 ; 所以須要在寫該寄存器的時候,將全部參數設置號 ; 0x20 表示讀扇區,0x30寫扇區 mov dx,0x1f7 mov al,0x20 out dx,al .not_ready: ; 讀 0x1f7 判斷數據是否就緒,沒就緒就循環等待. nop in al,dx and al,0x88 cmp al,0x08 jnz .not_ready ; 到這一步表示數據就緒,設置各項數據,開始讀取 ; 一個扇區512字節,每次讀2字節,所以讀一個扇區須要256次從寄存器中讀取數據 ; di 中是最開始的cx也就是要讀取的扇區數 ; mul dx 是ax=ax * dx ,所以最終ax 中是要讀取的次數 mov ax,di mov dx,256 mul dx mov cx,ax ; 0x1f0 寄存器是一個16位寄存器,讀寫的時候,都是數據. mov dx,0x1f0 .go_on_read: in ax,dx mov [ebx],ax ;區別在這裏,使用的是ebx add ebx,2 ;ax 是 16位寄存器,讀出的也是2字節,所以讀一次 dx+2 loop .go_on_read ret ; ----------------------------------- 讀取磁盤文件 ----------------------------------- ; ----------------------------------- 解析內核ELF ----------------------------------- ; 功能:解析內核ELF文件 ; 不須要參數,由於參數是 KERNEL_BIN_IN_MEM ,並且代碼只是用一次 ; 所以參數直接寫死,不更改 kernel_init: xor eax,eax xor ebx,ebx xor ecx,ecx xor edx,edx ; 偏移42字節處是 e_phentsize是每一個程序頭結構的大小 mov dx,[KERNEL_BIN_IN_MEM+42] ; 偏移28字節處是 e_phoff,是第一個 程序頭結構在文件中的偏移 mov ebx,[KERNEL_BIN_IN_MEM+28] ; 加上 KERNEL_BIN_IN_MEM ,就是第一個程序頭在文件中的偏移地址 add ebx,KERNEL_BIN_IN_MEM ; 偏移44字節處是 e_phnum ,是程序頭結構的個數 mov cx,[KERNEL_BIN_IN_MEM+44] ; 遍歷每一個程序頭結構,按照其 p_offset,加載到最內存的指定位置 .each_segment: ; p_type =0 則該程序頭未使用,跳過 cmp byte [ebx+0],ELF_PT_NULL je .pt_null ; 程序頭結構的偏移16 字節是 p_filesz 是結構的大小 push dword [ebx+16] ; 程序頭結構的偏移16 字節是 p_offset 是該結構在ELF文件中的偏移地址 mov eax,[ebx+4] ; 加上 KERNEL_BIN_IN_MEM add eax,KERNEL_BIN_IN_MEM push eax ; 程序頭結構的偏移4 字節是 p_vaddr 是該結構最終 加載在內存中地址 push dword [ebx+8] call memcpy add esp,12 .pt_null: add ebx,edx loop .each_segment ret ; ----------------------------------- 解析內核ELF ----------------------------------- ; ----------------------------------- 批量拷貝 ----------------------------------- memcpy: cld push ebp mov ebp,esp push ecx mov edi,[ebp+8] mov esi,[ebp+12] mov ecx,[ebp+16] rep movsb pop ecx pop ebp ret ; ----------------------------------- 批量拷貝 -----------------------------------
memcpy
函數的解釋以下:
MOVSB(MOVe String Byte):即字符串傳送指令,這條指令按字節傳送數據。經過SI和DI這兩個寄存器控制字符串的源地址和目標地址,好比DS:SI這段地址的N個字節複製到ES:DI指向的地址,複製後DS:SI的內容保持不變。
而REP(REPeat)指令就是「重複」的意思,術語叫作「重複前綴指令」,由於既然是傳遞字符串,則不可能一個字(節)一個字(節)地傳送,因此須要有一個寄存器來控制串長度。這個寄存器就是CX,指令每次執行前都會判斷CX的值是否爲0(爲0結束重複,不爲0,CX的值減1),以此來設定重複執行的次數。所以設置好CX的值以後就能夠用REP MOVSB了。
CLD(CLear Direction flag)則是清方向標誌位,也就是使DF的值爲0,在執行串操做時,使地址按遞增的方式變化,這樣便於調整相關段的的當前指針。這條指令與STD(SeT Direction flag)的執行結果相反,即置DF的值爲1。
暫時比較簡單,直接是一個循環
int main( int argc, char const* argv[] ) { while ( 1 ) { } return 0; }
可是其編譯過程不簡單:
-m32
參數-e main
將main
函數做爲程序的入口,而後指定入口地址-Ttext 0xc0001500
,再而後,由於gcc -c
編譯結果位32位程序,所以連接的時候須要帶上-m elf_i386
參數所以最終爲:
gcc -o kernel.o -m32 -c ./kernel/main.c ld -Ttext 0xc0001500 -m elf_i386 -e main -o kernel.bin ./kernel.o
改動較大,由於改變路目錄結構,又新加了文件,所以,須要變更的比較多:
-o
命令,指定輸出的路徑和文件名mbr.asm
和loader.asm
文件引入的boot.inc
文件移動.所以,在編譯的時候要加上-I ./boot/inlucde
main.c
文件的編譯,連接,刻錄#! /bin/bash # 編譯mbr.asm echo "----------nasm mbr.asm----------" if !(nasm -o ./build/mbr.bin ./boot/mbr.asm -I./boot/include/);then echo "nasm error" exit fi # 刻錄mbr.bin echo "----------dd mbr.bin ----------" if !(dd if=./build/mbr.bin of=../hd60m.img bs=512 count=1 conv=notrunc);then echo "dd error" exit fi # 編譯 loader.asm echo "----------nasm loader.asm----------" if !(nasm -o ./build/loader.bin ./boot/loader.asm -I./boot/include/);then echo "nasm error" exit fi # 刻錄loader.bin echo "----------dd loader.bin ----------" if !(dd if=./build/loader.bin of=../hd60m.img bs=512 count=4 seek=2 conv=notrunc);then echo "dd error" exit fi # 編譯內核 echo "----------gcc -c kmain.c ----------" if !(gcc -o ./build/kernel.o -m32 -c ./kernel/main.c );then echo "dd error" exit fi # 連接 echo "----------ld kernel.o ----------" if !(ld -Ttext 0xc0001500 -m elf_i386 -e main -o ./build/kernel.bin ./build/kernel.o);then echo "dd error" exit fi # 刻錄 kernel.bin echo "----------dd kernel.bin ----------" if !(dd if=./build/kernel.bin of=../hd60m.img bs=512 count=40 seek=9 conv=notrunc);then echo "dd error" exit fi # 刪除臨時文件 sleep 1s rm -rf ./build/*.* # 運行bochs cd .. bochs
在0x1500
打上斷點,執行到後,開跟蹤.