操作系统真相还原——第6章 完善内核

  1. 函数底层调用约定

    • cdecl:函数参数由栈进行传递,从右向左顺序入栈,栈空间由调用者清理,函数的返回值存储在EAX 寄存器。
    • syscall:参数从右到左入校。参数列袤的大小被放置在AL 寄存器中
    • optlink:参数也是从右到左压钱.从最左边开始的三个参数会被放置在寄存器EAX,EDX 和ECX 中
    • pascal:参数从 左至 右入栈,被调用者负责在返回前清理堆栈。
  2. 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
    
  3. 汇编的指令结果对于其他寄存器值的改变是有约定的

  4. 汇编和C的混合编程

    • 独立链接:C文件和汇编文件各自编译成目标文件后进行链接
    • 内联汇编:在C语言中嵌入汇编代码,直接编译生成可执行程序
  5. 系统调用是Linux内核的一套子程序,用于给用户提供系统级功能调用,类似于Windows的动态链接库dll文件的功能

  6. 系统调用的入口只有0x80号中断

    • 具体子功能号需要在寄存器eax中单独指定
    • (1) ebx 存储第1 个参数。 (2) ecx 存储第2 个参数。 (3) edx 存储第3 个参数。 (4) esi 存储第4 个参数。 (5) edi 存储第5 个参数。
  7. 可以使用man命令查看linux下的系统调用手册,egman 2 write

  8. 系统调用的使用方式

    • 将系统调用指令封装成c库函数,通过库函数进行调用
    • 直接使用汇编指令int进行系统调用子功能的使用
  9. 系统调用参数的传递方式

    • 当输入的参数小于等于5 个时, Linux 用寄存器传递参数
    • 当参数个数大于5 个时,把参数按照顺序放入连续的内存区域,并将该区域的首地址放到ebx 寄存器
  10. C语言中,printf函数本质调用的write函数,而write函数本质是调用syscall的4号功能调用

  11. 机器码是各种语言最本质的表示

  12. 函数声明的作用

    • 告诉编译器参数所需要的栈空间大小及返回值
    • 函数是在外部文件定义的,要在链接阶段进行链接
  13. 端口就是IO设备的寄存器,每个寄存器都有独立地址,CPU通过Intel系统的端口号进行访问范围是0~65535,不是内存地址。端口使用专用的IO指令in和out进行读写

  14. 寄存器组思想使用两个寄存器操作一组寄存器

    • Address Register:用于指定寄存器数组某一个寄存器
    • Data Register:用于对索引所指向的数组元素(寄存器)进行输入输出操作
  15. 新建lib目录用来存放各种库文件

    • lib/kernel存放内核使用的库文件
    • lib/user存放用户进程使用的库文件
  16. pushad将所有双字长寄存器压入栈中,入栈顺序为
    EAX->ECX->EDX->EBX->ESP-> EBP->ESl->EDI

  17. 打印字符本质上就是把字符写入在显存中的某个地址处。在文本模式80*25 下的显存可以显示80*25=2000 个字符,每个字符占2 字节,低宇节是字符的ASCII 码,高字节是前景色和背景色属性

  18. 光标的坐标位置是存放在光标坐标寄存器中的,当我们在屏幕上写入一个字符时,光标的坐标并不会自动+ 1,因为光标和字符是分离的

  19. in指令,如果源操作是8 位寄存器,目的操作数必须是al, 如果源操作数是16 位寄存器,目的操作数必须是ax

  20. 获取光标位置

    • 通过向目标寄存器组输入某个具体寄存器索引找到寄存器
    • 从数据寄存器中获取值
  21. backspace键的原理
    光标向前移动一个显存位置,后面再输入字符会覆盖该字符,如果不输入字符则用空字符填充

  22. 滚屏的原理

    • 将所有行内容向上搬一行
    • 将最后一行使用空格覆盖
    • 将光标移到最后一行的行首
  23. 回车键的原理

    • CR:光标回撤到当前行首
    • LF:切换到下一行
  24. CPU在指令越权时候会做特权级检查

  25. CPU 是不会让低特权级程序有访问高特权级资源的机会的,有任何一个段寄存器所指向的段描述符的DPL权限高于从iretd 命令返回后的CPL,CPU 就会将该段寄存器赋值为0。GDT 中检索到第0 个段描述符,会抛出异常。即访问别人要比别人权限高,CPL 权限比数据段寄存器( DS 、ES 、陀、GS )指向的段描述符的DPL权限小, CPU 便认为这是一种越权访问。

  26. 用户进程的特权级由cs 寄存器中选择子的RPL 字段决定,它将成为进程在CPU 上运行时的CPL

  27. 避免头文件中变量的重复定义,可以使用条件编译指令#ifdef和#endif来封闭文件的内容,把要定义的内容放在中间即可

  28. #include使用<>括住的,让编译器到系统文件所在的目录中找到所包含的文件,这个目录通常是/usr/include

  29. put_str函数是字符串打印函数,每次处理一个字符循环打印字符串的所有字符

  30. 小端存储:字节的低位字节存储在内存低位上

  31. 成功截图

image-20220617101804711

  1. 源码

    // 文件目录: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 ebx
       push ecx
       xor ecx, ecx		      ; 准备用ecx存储参数,清空
       mov ebx, [esp + 12]	      ; 从栈中得到待打印的字符串地址 
    .goon:
       mov cl, [ebx]
       cmp cl, 0		      ; 如果处理到了字符串尾,跳到结束处返回
       jz .str_over
       push ecx		      ; 为put_char函数传递参数
       call put_char
       add esp, 4		      ; 回收参数所占的栈空间
       inc ebx		      ; 使ebx指向下一个字符
       jmp .goon
    .str_over:
       pop ecx
       pop ebx
       ret
    
    
    ;--------------------   将小端字节序的数字变成对应的ascii后,倒置   -----------------------
    ;输入:栈中参数为待打印的数字
    ;输出:在屏幕上打印16进制数字,并不会打印前缀0x,如打印10进制15时,只会直接打印f,不会是0xf
    ;------------------------------------------------------------------------------------------
    global put_int
    put_int:
       pushad
       mov ebp, esp
       mov eax, [ebp+4*9]		       ; call的返回地址占4字节+pushad的8个4字节
       mov edx, eax
       mov 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 edi
       shr eax, 4
       mov 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 edi
       cmp 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_char
       add esp, 4
       inc edi			       ; 使edi指向下一个字符
       mov cl, [put_int_buffer+edi]	       ; 获取下一个字符到cl寄存器
       cmp edi,8
       jl .put_each_num
       popad
       ret
    
    ; 把栈中的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, 0x03d4
       mov al, 0x0f
       out dx, al
       mov dx, 0x03d5 
       in al, dx
    
       ;将光标存入bx
       mov bx, ax	  
       ;下面这行是在栈中获取待打印的字符
       mov ecx, [esp + 36];pushad压入4×8=32字节,加上主调函数的返回地址4字节,故esp+36字节
       ; 判断字符类型,跳转执行相应功能
       cmp cl, 0xd ;CR是0x0d
       jz .is_carriage_return
       cmp cl, 0xa ;LF是0x0a
       jz .is_line_feed
       cmp cl, 0x8 ;退格(backspace)的ascii码是8
       jz .is_backspace
       jmp .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, 80
       cmp 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 * 2
       mov ecx, 80	 ;一行是80字符(160字节),每次清理1字符(2字节),一行需要移动80次
     .cls:; 填充最后一行
       mov word [gs:ebx], 0x0720		  ;0x0720是黑底白字的空格键
       add ebx, 2
       loop .cls 
       mov bx,1920				  ;将光标值重置为1920,最后一行的首字符.
    
     .set_cursor:   
     ;将光标设为bx值
    ;;;;;;; 1 先设置高8位 ;;;;;;;;
       mov dx, 0x03d4			  ;索引寄存器
       mov al, 0x0e				  ;用于提供光标位置的高8位
       out dx, al
       mov dx, 0x03d5			  ;通过读写数据端口0x3d5来获得或设置光标位置 
       mov al, bh
       out dx, al
    
    ;;;;;;; 2 再设置低8位 ;;;;;;;;;
       mov dx, 0x03d4
       mov al, 0x0f
       out dx, al
       mov dx, 0x03d5 
       mov al, bl
       out dx, al
     .put_char_done: ; 恢复环境
       popad
       ret
    
    
    #!/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 
    
    
  2. 内联汇编:GCC支持在C代码中直接嵌入汇编代码,因为C语言不支持寄存器操作,可以实现C语言无法实现的功能

  3. AT&T是一种汇编语言的语法风格,最先在UNIX中使用,目的操作数在右边。intel语法目的操作数为左值

  4. 内联汇编的声明asm [volatile]("assembly code")

    • asm用于内敛汇编的声明,是由GCC内定义的宏
    • volatile表示不要编译器进行优化该部分代码
    • 如果内联汇编代码需要跨行,则应该在结尾使用反斜杠’\'进行转义
    • 汇编代码除最后一个双引号外,其余双引号中的代码最后一定要有分隔符
      asm("movl $9,%eax;""pushl %eax")
    • 如果不使用编译器优化,需要先进行堆栈寄存器环境的保存,pusha
  5. 通过系统调用打印字符

    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 \
        ”) ;
    )
    
  6. 扩展内联汇编
    asm [volatile] ("assembly":output : input : clobber/modify)

    • 括号内的4部分每一部分都可以省略
  7. 内存约束:要求gee 直接将位于input 和output 中的C 变量的内存地址作为内联汇编代码的操作数,不需要寄存器做中转,直接进行内存读写,也就是汇编代码的操作数是C 变量的指针。

  8. 立即数约束:此约束要求gee 在传值的时候不通过内存和寄存器,直接作为立即数传给汇编代码。由于立即数不是变量,只能作为右值,所以只能放在input 中

  9. gee 为了提速,编译中有时会把内存中的数据缓存到寄存器,之后的处理都是直接读取寄存器。编译过程中编译器无法检测到内存的变化,只有编译出来的程序在实际运行中才会出现变量的值被改变,也就是出现了内存变化的情况。

  10. volatile 定义的变量,编译器就不会将该变量的值缓存到寄存器中,每次访问该变量时都会老老实实地从内存中获取

  11. 机器模式:GCC 支持内联汇编,由于各种约束均不能确切地表达具体的操作数对象,所以引用了机器模式,用来从更细的粒度上描述数据对象的大小及其指定部分。

  12. 前缀指代

    • h -输出寄存器高位部分中的那一字节对应的寄存器名称,如ah 、bh、ch 、曲。
    • b -输出寄存器中低部分1 字节对应的名称,如al 、bl 、cl 、di 。
    • w -输出寄存器中大小为2 个宇节对应的部分,如ax 、bx、ex 、dx 。
    • k -输出寄存器的四字节部分,如eax 、ebx 、ecx, edx

变量,只能作为右值,所以只能放在input 中

  1. gee 为了提速,编译中有时会把内存中的数据缓存到寄存器,之后的处理都是直接读取寄存器。编译过程中编译器无法检测到内存的变化,只有编译出来的程序在实际运行中才会出现变量的值被改变,也就是出现了内存变化的情况。

  2. volatile 定义的变量,编译器就不会将该变量的值缓存到寄存器中,每次访问该变量时都会老老实实地从内存中获取

  3. 机器模式:GCC 支持内联汇编,由于各种约束均不能确切地表达具体的操作数对象,所以引用了机器模式,用来从更细的粒度上描述数据对象的大小及其指定部分。

  4. 前缀指代

    • h -输出寄存器高位部分中的那一字节对应的寄存器名称,如ah 、bh、ch 、曲。
    • b -输出寄存器中低部分1 字节对应的名称,如al 、bl 、cl 、di 。
    • w -输出寄存器中大小为2 个宇节对应的部分,如ax 、bx、ex 、dx 。
    • k -输出寄存器的四字节部分,如eax 、ebx 、ecx, edx
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

逆羽飘扬

如果有用,请支持一下。

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值