操作系统真相还原——第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章 完善内核相关推荐
- 操作系统真象还原——第6章 完善内核
什么是函数调用约定 一言概之:函数调用约定就是调用函数时,如何传参入栈,调用后如何恢复栈的协定.我们实现的过程中使用的是cdecl协定,基本的原则是: 参数按从右到左的顺序入栈 由调用者清理栈空间 其 ...
- 《操作系统真象还原》第九章
<操作系统真象还原>第九章 本篇对应书籍第九章的内容 本篇内容介绍了线程是什么,多线程的原理,多线程用到的核心数据结构,以及通过代码实现了内核多线程的调度 线程是什么? 执行流 过去,计算 ...
- 《操作系统真象还原》第二章
<操作系统真象还原>第二章 编写MBR主引导记录 载入内存 过程: (1)程序被加载器(软件或硬件)加载到内存某个区域. (2)CPU的cs:ip寄存器被指向这个程序的起始地址. 从按下主 ...
- [操作系统] 操作系统真相还原读书笔记三:MBR加载loader到内存并跳转到loader执行
为什么要有loader程序? 通过操作系统真相还原读书笔记二:编写MBR主引导记录我们已经能够正常运行MBR主引导记录(有些书籍也叫做boot)程序了,但该程序什么也没做.我们的MBR 受限于 512 ...
- 《操作系统真象还原》1-3章 学习记录
文章目录 前言 一.开始实验前的一些基本问题解答? section的含义? vstart的含义? $ 和 $$区别? 实模式的特点? CPU如何和硬盘进行交互? CPU和IO设备交互方式? 程序载入内 ...
- 操作系统真象还原第1章:部署工作环境
前言 关于自制操作系统,一开始本来是想利用极客时间的操作系统实战45讲来的(为了能够更快完成),然而当自己真正去学的时候才发现这些视频还是比较坑的,最起码对于我来说,首先是代码注释很少,函数,变量命名 ...
- 操作系统真相还原学习笔记
录像51 操作系统真相还原–学习笔记 这个笔记是把郑钢先生的代码在windows上编译运行了,希望大家在win上也调试地愉快. 链接:https://pan.baidu.com/s/1T7Sj_ZwA ...
- 操作系统真相还原-编译遇到“__stack_chk_fail_local”错误
编写操作系统真相还原 – 中断系统时, 在编译链接时遇到了一个问题 我先按照书上所说(如下方), 进行编译 gcc -m32 -I lib/kernel/ -I lib/ -I kernel/ -c ...
- 操作系统真象还原第5章:保护模式进阶,向内核进阶
前言 由于涉及到马上要搞实习的事情,搞得我十分的浮躁,自己也是频繁失眠,想来还是自己太过懒了,没控制住自己,自己也在这一个多月没搞好,尤其是本来想花几天时间来写一个高性能服务器,也把游双大佬的linu ...
最新文章
- XP局域网访问无权限、不能互相访问问题的完整解决方案
- 从门禁系统的使用体验看良好的交互设计原则
- Linux下VSFTP配置全攻略
- onDraw什么时候被调用?
- 随机生成六位不重复数值
- 在中断程序里修改全局变量的童鞋注意啦~(C中的volatile作用 转载~)
- Android1 按钮
- 美国红帽软件公司是做什么的
- dp P1103 书本整理 洛谷
- 本人工作性质已改变,技术文摘随笔已经全部下线
- 【超详细!】【超全面!】计算机二级公共基础知识考点整理
- 2012云计算发展趋势
- 计算机鼠标装有,计算机插入鼠标时无法安装设备驱动程序的解决方法
- oracle库存计算公式,问安全库存量计算公式
- CSR蓝牙开发调试经验
- 北京市社会保险办理流程
- matlab基础总结与图像处理应用(上)
- html 怎么把图片变成圆角,css怎么样实现图片圆角
- Intel AVX入门实践
- R实战:【股票分析】用quantmod在股票的K线上添加标记