-
函数底层调用约定
- 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 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
-
内联汇编: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