操作系统真相还原——第6章 完善内核
函數底層調用約定
- cdecl:函數參數由棧進行傳遞,從右向左順序入棧,棧空間由調用者清理,函數的返回值存儲在EAX 寄存器。
- syscall:參數從右到左入校。參數列袤的大小被放置在AL 寄存器中
- optlink:參數也是從右到左壓錢.從最左邊開始的三個參數會被放置在寄存器EAX,EDX 和ECX 中
- pascal:參數從 左至 右入棧,被調用者負責在返回前清理堆棧。
cdecl調用約定下匯編代碼示例
int subtract(int a, int b); //被調用者 int sub = subtract(3,2); //主調用者;主調用者: ; 從右到左將參數入棧 push 2 ;壓入參數b push 3 ;壓入參數a call subtract ;調用函數subtract add esp, 8 ;回收棧空間;被調用者: push ebp ;壓ebp備份 mov ebp,esp ;將esp賦值給ebp mov eax,[ebp+0x8] ;偏移8字節處為第一個參數a add eax,[ebp+0xc] ;偏移0xc字節處為第二個參數b,參數a和b相加后存入eax mov esp,ebp ;為防止中間有入棧操作,用ebp恢復esp pop ebp ;將ebp恢復 ret匯編的指令結果對于其他寄存器值的改變是有約定的
匯編和C的混合編程
- 獨立鏈接:C文件和匯編文件各自編譯成目標文件后進行鏈接
- 內聯匯編:在C語言中嵌入匯編代碼,直接編譯生成可執行程序
系統調用是Linux內核的一套子程序,用于給用戶提供系統級功能調用,類似于Windows的動態鏈接庫dll文件的功能
系統調用的入口只有0x80號中斷
- 具體子功能號需要在寄存器eax中單獨指定
- (1) ebx 存儲第1 個參數。 (2) ecx 存儲第2 個參數。 (3) edx 存儲第3 個參數。 (4) esi 存儲第4 個參數。 (5) edi 存儲第5 個參數。
可以使用man命令查看linux下的系統調用手冊,egman 2 write
系統調用的使用方式
- 將系統調用指令封裝成c庫函數,通過庫函數進行調用
- 直接使用匯編指令int進行系統調用子功能的使用
系統調用參數的傳遞方式
- 當輸入的參數小于等于5 個時, Linux 用寄存器傳遞參數
- 當參數個數大于5 個時,把參數按照順序放入連續的內存區域,并將該區域的首地址放到ebx 寄存器
C語言中,printf函數本質調用的write函數,而write函數本質是調用syscall的4號功能調用
機器碼是各種語言最本質的表示
函數聲明的作用
- 告訴編譯器參數所需要的棧空間大小及返回值
- 函數是在外部文件定義的,要在鏈接階段進行鏈接
端口就是IO設備的寄存器,每個寄存器都有獨立地址,CPU通過Intel系統的端口號進行訪問范圍是0~65535,不是內存地址。端口使用專用的IO指令in和out進行讀寫
寄存器組思想使用兩個寄存器操作一組寄存器
- Address Register:用于指定寄存器數組某一個寄存器
- Data Register:用于對索引所指向的數組元素(寄存器)進行輸入輸出操作
新建lib目錄用來存放各種庫文件
- lib/kernel存放內核使用的庫文件
- lib/user存放用戶進程使用的庫文件
pushad將所有雙字長寄存器壓入棧中,入棧順序為
EAX->ECX->EDX->EBX->ESP-> EBP->ESl->EDI
打印字符本質上就是把字符寫入在顯存中的某個地址處。在文本模式80*25 下的顯存可以顯示80*25=2000 個字符,每個字符占2 字節,低宇節是字符的ASCII 碼,高字節是前景色和背景色屬性
光標的坐標位置是存放在光標坐標寄存器中的,當我們在屏幕上寫入一個字符時,光標的坐標并不會自動+ 1,因為光標和字符是分離的
in指令,如果源操作是8 位寄存器,目的操作數必須是al, 如果源操作數是16 位寄存器,目的操作數必須是ax
獲取光標位置
- 通過向目標寄存器組輸入某個具體寄存器索引找到寄存器
- 從數據寄存器中獲取值
backspace鍵的原理
光標向前移動一個顯存位置,后面再輸入字符會覆蓋該字符,如果不輸入字符則用空字符填充
滾屏的原理
- 將所有行內容向上搬一行
- 將最后一行使用空格覆蓋
- 將光標移到最后一行的行首
回車鍵的原理
- CR:光標回撤到當前行首
- LF:切換到下一行
CPU在指令越權時候會做特權級檢查
CPU 是不會讓低特權級程序有訪問高特權級資源的機會的,有任何一個段寄存器所指向的段描述符的DPL權限高于從iretd 命令返回后的CPL,CPU 就會將該段寄存器賦值為0。GDT 中檢索到第0 個段描述符,會拋出異常。即訪問別人要比別人權限高,CPL 權限比數據段寄存器( DS 、ES 、陀、GS )指向的段描述符的DPL權限小, CPU 便認為這是一種越權訪問。
用戶進程的特權級由cs 寄存器中選擇子的RPL 字段決定,它將成為進程在CPU 上運行時的CPL
避免頭文件中變量的重復定義,可以使用條件編譯指令#ifdef和#endif來封閉文件的內容,把要定義的內容放在中間即可
#include使用<>括住的,讓編譯器到系統文件所在的目錄中找到所包含的文件,這個目錄通常是/usr/include
put_str函數是字符串打印函數,每次處理一個字符循環打印字符串的所有字符
小端存儲:字節的低位字節存儲在內存低位上
成功截圖
源碼
// 文件目錄:kernel/print.h #ifndef __LIB_KERNEL_PRINT_H #define __LIB_KERNEL_PRINT_H #include "stdint.h" void put_char(uint8_t char_asci); void put_str(char* message); void put_int(uint32_t num); // 以16進制打印#endif// 文件目錄:kernel/stdint.h #ifndef __LIB_STDINT_H #define __LIB_STDINT_H typedef signed char int8_t; typedef signed short int int16_t; typedef signed int int32_t; typedef signed long long int int64_t; typedef unsigned char uint8_t; typedef unsigned short int uint16_t; typedef unsigned int uint32_t; typedef unsigned long long int uint64_t; #endif// 文件目錄:kernel/main.c #include "print.h" void main(void){put_str("i am kernel");put_int(0);put_char('\n');put_int(9);put_char('\n');put_int(0x00021a3f);put_char('\n');put_int(0x12345678);put_char('\n');put_int(0x00000000);put_char('\n');while(1); } ; 文件目錄:kernel/print.s ;1. 定義視頻段的段選擇子,一般放在配置文件中。要轉化成二進制再左移 TI_GDT equ 0 RPL0 equ 0 SELECTOR_VIDEO equ (0x0003<<3) + TI_GDT + RPL0; 2.section是偽指令用于劃分程序段,.data段可讀可寫,.text段只讀可執行 section .data put_int_buffer dq 0 ; 定義4個字的緩沖區用于數字到字符的轉換 [bits 32] section .text; 3.put_str 通過put_char來打印以0字符結尾的字符串 global put_str;global表示外部文件也可見 put_str: ;由于本函數中只用到了ebx和ecx,只備份這兩個寄存器push ebxpush ecxxor ecx, ecx ; 準備用ecx存儲參數,清空mov ebx, [esp + 12] ; 從棧中得到待打印的字符串地址 .goon:mov cl, [ebx]cmp cl, 0 ; 如果處理到了字符串尾,跳到結束處返回jz .str_overpush ecx ; 為put_char函數傳遞參數call put_charadd esp, 4 ; 回收參數所占的棧空間inc ebx ; 使ebx指向下一個字符jmp .goon .str_over:pop ecxpop ebxret;-------------------- 將小端字節序的數字變成對應的ascii后,倒置 ----------------------- ;輸入:棧中參數為待打印的數字 ;輸出:在屏幕上打印16進制數字,并不會打印前綴0x,如打印10進制15時,只會直接打印f,不會是0xf ;------------------------------------------------------------------------------------------ global put_int put_int:pushadmov ebp, espmov eax, [ebp+4*9] ; call的返回地址占4字節+pushad的8個4字節mov edx, eaxmov edi, 7 ; 指定在put_int_buffer中初始的偏移量mov ecx, 8 ; 32位數字中,16進制數字的位數是8個mov ebx, put_int_buffer;將32位數字按照16進制的形式從低位到高位逐個處理,共處理8個16進制數字 .16based_4bits: ; 每4位二進制是16進制數字的1位,遍歷每一位16進制數字and edx, 0x0000000F ; 解析16進制數字的每一位。and與操作后,edx只有低4位有效cmp edx, 9 ; 數字0~9和a~f需要分別處理成對應的字符jg .is_A2F add edx, '0' ; ascii碼是8位大小。add求和操作后,edx低8位有效。jmp .store .is_A2F:sub edx, 10 ; A~F 減去10 所得到的差,再加上字符A的ascii碼,便是A~F對應的ascii碼add edx, 'A';將每一位數字轉換成對應的字符后,按照類似“大端”的順序存儲到緩沖區put_int_buffer ;高位字符放在低地址,低位字符要放在高地址,這樣和大端字節序類似,只不過咱們這里是字符序. .store: ; 此時dl中應該是數字對應的字符的ascii碼mov [ebx+edi], dl dec edishr eax, 4mov edx, eax loop .16based_4bits;現在put_int_buffer中已全是字符,打印之前, ;把高位連續的字符去掉,比如把字符000123變成123 .ready_to_print:inc edi ; 此時edi退減為-1(0xffffffff),加1使其為0 .skip_prefix_0: cmp edi,8 ; 若已經比較第9個字符了,表示待打印的字符串為全0 je .full0 ;找出連續的0字符, edi做為非0的最高位字符的偏移 .go_on_skip: mov cl, [put_int_buffer+edi]inc edicmp cl, '0' je .skip_prefix_0 ; 繼續判斷下一位字符是否為字符0(不是數字0)dec edi ;edi在上面的inc操作中指向了下一個字符,若當前字符不為'0',要恢復edi指向當前字符 jmp .put_each_num.full0:mov cl,'0' ; 輸入的數字為全0時,則只打印0 .put_each_num:push ecx ; 此時cl中為可打印的字符call put_charadd esp, 4inc edi ; 使edi指向下一個字符mov cl, [put_int_buffer+edi] ; 獲取下一個字符到cl寄存器cmp edi,8jl .put_each_numpopadret; 把棧中的1個字符寫入光標所在處 global put_char; global使關鍵字對外部文件可見 put_char:pushad ;備份32位寄存器環境,壓入所有雙字長的寄存器值; 每次打印時都為gs賦值視頻段選擇子mov ax, SELECTOR_VIDEO ; 不能直接把立即數送入段寄存器mov gs, ax;獲取當前光標位置;先獲得高8位:先在地址寄存器組中確定具體端口,在到相應的寄存器中找到值;地址與數據分離mov dx, 0x03d4 ; 端口0x03d4是寄存器組的地址mov al, 0x0e ; 0x0e表示寄存器組中提供光標位置的高8位的具體寄存器out dx, al ; al中的立即數不能直接填入dx中mov dx, 0x03d5 ; 通過讀寫數據端口0x3d5來獲得或設置光標位置 in al, dx ; 得到了光標位置的高8位mov ah, al ; in指令目的操作數必須是al;再獲取低8位mov dx, 0x03d4mov al, 0x0fout dx, almov dx, 0x03d5 in al, dx;將光標存入bxmov bx, ax ;下面這行是在棧中獲取待打印的字符mov ecx, [esp + 36];pushad壓入4×8=32字節,加上主調函數的返回地址4字節,故esp+36字節; 判斷字符類型,跳轉執行相應功能cmp cl, 0xd ;CR是0x0djz .is_carriage_returncmp cl, 0xa ;LF是0x0ajz .is_line_feedcmp cl, 0x8 ;退格(backspace)的ascii碼是8jz .is_backspacejmp .put_other ; 處理退格字符的函數.is_backspace: ;;;;;;;;;;;; backspace的一點說明 ;;;;;;;;;; ; 當為backspace時,本質上只要將光標移向前一個顯存位置即可.后面再輸入的字符自然會覆蓋此處的字符 ; 但有可能在鍵入backspace后并不再鍵入新的字符,這時在光標已經向前移動到待刪除的字符位置,但字符還在原處, ; 這就顯得好怪異,所以此處添加了空格或空字符0 ; bx存放下一個要打印字符的光標坐標值,光標值乘2為在顯存中的對應位置(因為一個字符在顯存中有兩個屬性)dec bx ; dec表示將bx減一,即光標向前移動一個字符shl bx,1; 邏輯左移1位表示乘2,最高位移入進位標志位CF,最低位補零mov byte [gs:bx], 0x20 ;將待刪除的字節補為0或空格皆可inc bx;bx加1,指向屬性值的位置mov byte [gs:bx], 0x07; 0x07表示黑屏白字shr bx,1 ;右移表示除2,將顯存地址恢復成光標坐標jmp .set_cursor; 處理正常字符的函數.put_other:shl bx, 1 ; 光標位置是用2字節表示,將光標值乘2,表示對應顯存中的偏移字節mov [gs:bx], cl ; 字符值:上面對ecx操作使cl存儲要打印的字符inc bx ; 顯存地址+1為字符顯示屬性mov byte [gs:bx],0x07; 字符屬性:黑底白字shr bx, 1 ; 恢復老的光標值inc bx ; 下一個光標值,即打印完光標數加一cmp bx, 2000 ;比較光標值是否超出顯示范圍2000字節 jl .set_cursor ; 若光標值小于2000,表示在顯示范圍內,更新光標值; 若超出屏幕字符數大小(2000)則換行處理,linux換行為CRLF; 由于是效仿linux,linux中\n便表示下一行的行首,所以本系統中,; 把\n和\r都處理為linux中\n的意思,也就是下一行的行首。; 默認情況下屏幕上的內容是從顯存的首地址(物理地址)Oxb8000 起; 一直到以該地址向上偏移3999 字節的地方。.is_line_feed: ; 是換行符LF(\n).is_carriage_return: ; 是回車符CR(\r); 如果是CR(\r),只要把光標移到行首就行了。xor dx, dx ; dx是被除數的高16位,清0.mov ax, bx ; ax是被除數的低16位.mov si, 80 ; si存放除數div si ; 執行完成后dx存放余數sub bx, dx ; 坐標值bx-余數dx 結果為當前行首坐標,存放在bx中.is_carriage_return_end: ; 回車符CR處理結束add bx, 80cmp bx, 2000.is_line_feed_end: ; 若是LF(\n),將光標移+80便可。 jl .set_cursor;屏幕行范圍是0~24,滾屏的原理是將屏幕的1~24行搬運到0~23行,再將第24行用空格填充.roll_screen: ; 若超出屏幕大小,開始滾屏cld ; 將方向標志位DF置0,表示內存增長方向mov ecx, 960 ; 一共有2000-80=1920個字符要搬運,共1920*2=3840字節.一次搬4字節,共3840/4=960次 ,是控制rep重復執行的次數mov esi, 0xc00b80a0 ; 第1行行首mov edi, 0xc00b8000 ; 第0行行首rep movsd;esi地址對應內存數據給了edi地址對應的內存內容,然后esi和edi各自加4 ;將最后一行填充為空白mov ebx, 3840 ; 最后一行首字符的第一個字節偏移= 1920 * 2mov ecx, 80 ;一行是80字符(160字節),每次清理1字符(2字節),一行需要移動80次.cls:; 填充最后一行mov word [gs:ebx], 0x0720 ;0x0720是黑底白字的空格鍵add ebx, 2loop .cls mov bx,1920 ;將光標值重置為1920,最后一行的首字符..set_cursor: ;將光標設為bx值 ;;;;;;; 1 先設置高8位 ;;;;;;;;mov dx, 0x03d4 ;索引寄存器mov al, 0x0e ;用于提供光標位置的高8位out dx, almov dx, 0x03d5 ;通過讀寫數據端口0x3d5來獲得或設置光標位置 mov al, bhout dx, al;;;;;;; 2 再設置低8位 ;;;;;;;;;mov dx, 0x03d4mov al, 0x0fout dx, almov dx, 0x03d5 mov al, blout dx, al.put_char_done: ; 恢復環境popadret #!/bin/bash #### 分功能進行shell文本的編寫,放在bochs根目錄下 #1.刪除中間文件 rm -rf ./hd.img ./main.o ./print.o ./hd.img &&\#1.新建硬盤鏡像文件 bin/bximage -hd -mode="flat" -size=60 -q hd.img &&\#2.setup程序(mbr和loader)的處理 ## 將使用匯編編寫的主引導記錄編譯成二進制文件 nasm -I include/ -o mbr.bin ./setup/mbr.s &&\ ## 將內核加載文件編譯成二進制文件 nasm -I include/ -o loader.bin ./setup/loader.s &&\ ## 將主引導記錄的二進制文件寫入硬盤鏡像文件 dd if=mbr.bin of=hd.img bs=512 count=1 seek=0 conv=notrunc &&\ ## 將內核加載文件的二進制文件寫入硬盤鏡像文件中 dd if=loader.bin of=hd.img bs=512 count=3 seek=2 conv=notrunc &&\ ## 清理程序 rm -rf loader.bin mbr.bin &&\#3.內核程序的處理 ## 編譯print.s文件 nasm -f elf -o print.o ./kernel/print.s &&\ ## 將c語言文件編譯成32位匯編文件 gcc -m32 -c -o main.o ./kernel/main.c &&\ ## 將二進制文件寫入硬盤鏡像并指定起始虛擬地址 ld -m elf_i386 -Ttext 0xc0001500 -e main -o kernel.bin main.o print.o &&\ ## 將內核文件寫入虛擬硬盤中 dd if=kernel.bin of=hd.img bs=512 count=200 seek=9 conv=notrunc&&\ ## 清理文件 rm -rf main.o print.o kernel.bin &&\ #4.啟動bochs bin/bochs -f bochsrc內聯匯編:GCC支持在C代碼中直接嵌入匯編代碼,因為C語言不支持寄存器操作,可以實現C語言無法實現的功能
AT&T是一種匯編語言的語法風格,最先在UNIX中使用,目的操作數在右邊。intel語法目的操作數為左值
內聯匯編的聲明asm [volatile]("assembly code")
- asm用于內斂匯編的聲明,是由GCC內定義的宏
- volatile表示不要編譯器進行優化該部分代碼
- 如果內聯匯編代碼需要跨行,則應該在結尾使用反斜杠’\'進行轉義
- 匯編代碼除最后一個雙引號外,其余雙引號中的代碼最后一定要有分隔符
asm("movl $9,%eax;""pushl %eax") - 如果不使用編譯器優化,需要先進行堆棧寄存器環境的保存,pusha
通過系統調用打印字符
char *str = "hello,world\n"; int count = 0; void main() {asm (”pusha; \movl $4 ,%eax ; \movl $1 ,%ebx; \movl str ,%ecx;\movl $12 ,%edx ; \int $0x80; \mov %eax ,count;\pop a \”) ; )擴展內聯匯編
asm [volatile] ("assembly":output : input : clobber/modify)
- 括號內的4部分每一部分都可以省略
內存約束:要求gee 直接將位于input 和output 中的C 變量的內存地址作為內聯匯編代碼的操作數,不需要寄存器做中轉,直接進行內存讀寫,也就是匯編代碼的操作數是C 變量的指針。
立即數約束:此約束要求gee 在傳值的時候不通過內存和寄存器,直接作為立即數傳給匯編代碼。由于立即數不是變量,只能作為右值,所以只能放在input 中
gee 為了提速,編譯中有時會把內存中的數據緩存到寄存器,之后的處理都是直接讀取寄存器。編譯過程中編譯器無法檢測到內存的變化,只有編譯出來的程序在實際運行中才會出現變量的值被改變,也就是出現了內存變化的情況。
volatile 定義的變量,編譯器就不會將該變量的值緩存到寄存器中,每次訪問該變量時都會老老實實地從內存中獲取
機器模式:GCC 支持內聯匯編,由于各種約束均不能確切地表達具體的操作數對象,所以引用了機器模式,用來從更細的粒度上描述數據對象的大小及其指定部分。
前綴指代
- h -輸出寄存器高位部分中的那一字節對應的寄存器名稱,如ah 、bh、ch 、曲。
- b -輸出寄存器中低部分1 字節對應的名稱,如al 、bl 、cl 、di 。
- w -輸出寄存器中大小為2 個宇節對應的部分,如ax 、bx、ex 、dx 。
- k -輸出寄存器的四字節部分,如eax 、ebx 、ecx, edx
變量,只能作為右值,所以只能放在input 中
gee 為了提速,編譯中有時會把內存中的數據緩存到寄存器,之后的處理都是直接讀取寄存器。編譯過程中編譯器無法檢測到內存的變化,只有編譯出來的程序在實際運行中才會出現變量的值被改變,也就是出現了內存變化的情況。
volatile 定義的變量,編譯器就不會將該變量的值緩存到寄存器中,每次訪問該變量時都會老老實實地從內存中獲取
機器模式:GCC 支持內聯匯編,由于各種約束均不能確切地表達具體的操作數對象,所以引用了機器模式,用來從更細的粒度上描述數據對象的大小及其指定部分。
前綴指代
- h -輸出寄存器高位部分中的那一字節對應的寄存器名稱,如ah 、bh、ch 、曲。
- b -輸出寄存器中低部分1 字節對應的名稱,如al 、bl 、cl 、di 。
- w -輸出寄存器中大小為2 個宇節對應的部分,如ax 、bx、ex 、dx 。
- k -輸出寄存器的四字節部分,如eax 、ebx 、ecx, edx
總結
以上是生活随笔為你收集整理的操作系统真相还原——第6章 完善内核的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 探究ES suggest search
- 下一篇: C++实现声音文件的播放(OpenAL、