真象还原操作系统_第六章_完善内核-程序员宅基地

技术标签: 汇编  编辑器  c语言  真像还原操作系统  嵌入式硬件  

一、函数调用约定

  • 调用约定:是调用函数时的一套约定,是被调用代码的接口,它体现在
    1. 参数的传递方式:在寄存器中/栈中/两者都有?
    2. 参数的传递顺序:从走到右/从右到左?
    3. 是调用者保存寄存器环境还是被调用者保存?保存哪些寄存器
  • 用栈保存参数的优点:
    1. 每个进程都有自己的栈,是自己的专用内存空间。
    2. 保存参数的地址不需要花费精力维护,因为已经有栈机制来维护地址的变化了,参数在栈中的位置可以通过栈顶的偏移量来得到。
  • 栈举例:
    • C语言的减法操作:
    int subtract(int a ,int b){
          return a-b;} //被调用者
    int sub = subtract(3,2);				//主调用者
    
    • 主调用者:
    push 2			;压入参数b
    push 3			;压入参数a
    call subtract	;调用参数subtract
    
    • 被调用者:
    //push 寄存器是将寄存器的值压入堆栈,可以保留该值的副本,而不会影响后续指令对寄存器的操作
    push ebp		 ;备份ebp,为以后用ebp作为基址来寻址参数。ebp之前为参数名和形参,ebp之后为函数体的参数。一般情况下,用[ss:bp+n]用作栈内寻址。
    mov ebp,esp		 ;将当前栈顶赋值给ebp
    mov eax,[ebp+8]  ;得到被减数,参数a
    sub eax,[ebp+12] ;得到减数,参数b
    pop ebp			 ;恢复ebp的值
    

在这里插入图片描述

  • 调用约定的作用:
    1. 规定谁负责回收参数所占的栈空间:回收栈空间是指将栈指针回退到高地址,下次入栈时,之前的参数将被覆盖。
    2. 规定当参数很多的情况下,主调函数将参数以什么杨的顺序传递?
    3. 调用约定就是调用方和被调用方对以上问题达成一致的约定。
  • 栈空间清理分类:
    在这里插入图片描述
  • stdcall
    • 调用者将所有参数从右往左入栈。
    • 被调用者清理参数所占的栈空间。
    • subtract函数举例:
    int subtract(int a ,int b){
          return a-b;} //被调用者
    int sub = subtract(3,2);				//主调用者
    
    • stdcall的调用者:
    push 2			;压入参数b
    push 3			;压入参数a
    call subtract	;调用参数subtract
    
    • stdcall的被调用者:
    push ebp			;压入ebp备份
    mov ebp,esp			;将esp备份给ebp,用ebp作为基址来访问栈中参数
    mov eax,[ebp+0x8]	;第一个参数a
    sub eax,[ebp+0xc]	;a-b后存入a中
    mov esp,ebp			;函数计算后将栈指针定位到返回地址处
    pop ebp				;将ebp恢复
    ret 8				;返回后使esp+8,使esp置于栈顶,清理栈空间
    ;因为返回地址在参数之下,所以ret指令执行时必须保证当前栈顶是返回地址。清理栈是在返回时顺便完成的。
    
  • cdecl
    • cdecl起源于C语言,被称为C调用约定,是C语言默认的调用约定。
    • 调用者将所有参数从右向左入栈。
    • 调用者清理参数所占的栈空间。
    • cdecl调用约定最大的亮点就是它允许函数中参数数量不固定,printf能够支持变成参数,它的原理就是利用字符串参数format中的"%"来匹配栈中的参数。
    • subtract函数举例:
    int subtract(int a ,int b){
          return a-b;} //被调用者
    int sub = subtract(3,2);				//主调用者
    
    • 主调用者:
    push 2			;压入参数b
    push 3			;压入参数a
    call subtract	;调用函数subtract
    add esp,8		;回收栈空间
    
    • 被调用者:
    push ebp
    mov ebp,esp
    mov eax,[ebp+0x8]
    sub eax,[ebp+0xc]
    mov esp,ebp
    pop ebp
    ret
    

二、汇编语言和C语言混合编程

1. C库函数与函数调用

  • 汇编语言和C语言混合编程分类:
    1. 单独的汇编代码文件与单独的C语言文件分别编译成目标文件后,一起链接成可执行文件。
    2. 在C语言中嵌入汇编代码,直接编译成可执行程序,称为内联汇编。
  • Linux系统调用
    • 系统调用是Linux内核提供的一套子程序,它和Windows的动态链接库dll的功能一样,用来实现一些在用户态不能/不易实现的功能。
    • 系统调用是提供给用户程序使用的,操作系统权力至高无上,不需要使用自己对外发布的功能接口,即系统调用。
    • 由于是用户程序想使用OS提供的功能,所以系统调用又称为OS功能调用。
    • 系统调用和BIOS中断调用的区别:
      1. BIOS中断每个功能都有一个入口,中断号0~0x20都是BIOS的中断调用;
      2. 而系统调用只有0x80号中断一个入口。
      3. 因为中断的实现需要用到中断描述符表,表中许多中断号是被预留的,所以linux就选了一个可用的中断号作为所有系统调用的同一入口,具体的子功能号在寄存器eax中单独指定。
      4. 总之,BIOS中断走的是中断向量表,所以有很多中断号给它用;而系统调用走的是中断描述符表中的一项而已,所以只用了第0x80号中断。
    • 调用“系统调用”有两种方式:
      1. 将系统调用指令封装为C库函数,通过库函数进行系统调用,操作简单。
      2. 不依赖库函数,直接通过汇编指令int与OS通信。
  • 系统调用举例:
    • write系统调用:
      在这里插入图片描述
    • C语言中调用write系统调用:
    #include<unistd.h>
    int main(){
          
    	write(1,"hello world\n",4);
    	return 0;
    }
    
    • 用int实现系统调用:
      • 当输入参数<=5时,Linux用寄存器传递参数;当参数>5时,把参数按照顺序放入连续的内存区域,并将首地址放入ebx寄存器。下面是参数<=5时的情况:
      • ebx、ecx、edx、esi和edi分别存储第1、2、3、4、5个参数。
    section .data
    str_c_lib: db "c library says:hello world!",0xa	;0xa为LF ASCII码,为换行符。
    str_c_lib_len equ $-str_c_lib
    str_syscall: db "syscall says:hello world!",0xa
    str_syscall_len equ $-str_syscall
    section .text
    global _start
    _start:
    ;;;;;;;;;;;;;;;;;;方式一:模拟C语言中系统调用库函数write;;;;;;;;;;;;;;;;;
    	push str_c_lib_len	;按照C调用约定压入参数
    	push str_c_lib		
    	push 1				
    	call simu_write		;调用下面定义的simu_write
    	add esp,12			;回收栈空间
    ;;;;;;;;;;;;;;;;;;方式二:跨国库函数,直接进行系统调用;;;;;;;;;;;;;;;;;;;
    	mov eax,4			;4号子功能号是write系统调用
    	mov ebx,1			;此项固定为文件描述符1,标准输出(stdout)指向屏幕				
    	mov ecx,str_syscall		
    	mov edx,str_syscall_len	
    	int 0x80			;发起中断,通知Linux完成请求的功能
    ;;;;;;;;;;;;;;;;;;推出程序;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    	mov eax,1			;1号子功能是exit
    	int 0x80			;发起中断,通知Linux完成请求的功能
    ;;;;;;;;下面自定义的simu_write用来模拟C库函数中系统调用函数write;;;;;;;;;;
    simu_write:
    	push ebp			;备份ebp
    	mov ebp,esp
    	mov eax,4			;4号子功能是write系统调用
    	mov ebx,[ebp+8]		;第一个参数
    	mov ecx,[ebp+12]	;第二个参数
    	mov edx,[ebp+16]	;第三个参数
    	int 0x80			;发起中断,通知Linux完成请求的功能
    	pop ebp				;恢复ebp
    	ret
    

2. 汇编语言和C语言共同协作

  • C语言和汇编语言可以协作是因为有编译器。gcc是C语言编译器,nasm是汇编语言编译器,它们都可以把文件翻译为机器语言。
  • 举例:C_with_S_c.c和C_with_S_S.S:
    1. 代码C_with_S_c.c:
    extern void asm_print(char*,int);
    void c_print(char* str){
          
    	int len = 0;
    	while(str[len++]);
    	asm_print(str,len);
    }
    //在C语言中,只有符号定义为全局便可以被外部引用。
    
    1. 代码C_with_S_S.S:
    section .data
    str: db "asm_print says hello world!",0xa,0
    ;0xa为换行符,0为手工加上的字符串结束符\0的ASCII码
    str_len equ $-str
    
    section .text
    extern c_print
    global _start		;global将_start导出为全局符号,给编译器用。
    _start:		
    	;;;;;调用C代码中的函数c_print;;;;;
    	push str		;传入参数
    	call c_print	;调用C函数
    	add esp,4		;回收栈空间
    	;;;;;推出程序;;;;;
    	mov eax,1		;1号子功能是exit系统调用
    	int 0x80		;发起中断,通过Linux完成请求的功能
    
    global asm_print	;相当于asm_print(str,size)
    ;在汇编语言中,符号定义为global才可以被外部引用,无论是函数还是变量。
    asm_print:
    	push ebp		;备份ebp
    	mov ebp,esp
    	mov eax,4		;4号子功能是write系统调用
    	mov ebx,1		;此项固定为文件描述符1,标准输出(stdout)指向屏幕
    	mov ecx,[ebp+8]	;第一个参数
    	mov edx,[ebp+12];第二个参数
    	int 0x80		;发起中断,通过Linux完成请求的功能
    	pop ebp			;恢复ebp
    	ret
    
    1. C_with_S_c.c和C_with_S_S.S相互调用
      在这里插入图片描述
  • 函数声明的作用:
    1. 告诉编译器该函数的参数所需的栈空间大小和返回值,让编译器准备好环境。
    2. 如果是外部函数,一定要在链接时将对应的目标文件一起链接。

三、实现自己的打印函数

1. 显卡的端口控制

  • 我们之前对显卡的操作和对普通内存的操作是一样的。打印字符就是往显存中mov一些字符的ASCII码和属性。
  • 显存中的寄存器:
    在这里插入图片描述
  • 寄存器分组
    • 端口实际上是IO接口电路上的寄存器,为了能访问到这些CPU外部的寄存器,计算机系统为这些寄存器同一编址,一个寄存器被赋予一个地址。寄存器的地址范围是0~65535。
    • 我们用专门的in和out指令来读写这些寄存器。
    • 寄存器分组的原因:IO接口电路上的寄存器数量取决于具体外设。因为显卡上的寄存器太多了,如果每个寄存器都占用一个端口,资源会被浪费。所以计算机系统给的端口是固定的。
    • 寄存器分组的使用:工程系将每个寄存器分组视作一个寄存器数组,提供了一个寄存器来指定数组下标,一个寄存器用于索引所指向的数组元素进行输入输出。这样两个寄存器就可以定位寄存器数组中的任何寄存器了。
    • 这两个寄存器分别为Address Register和Data Register;Address Register中指定寄存器的索引值,Data Register中对索引的寄存器进行读写操作。

2. 实现单子字符打印

  • 新建文件夹:
    在这里插入图片描述
    • lib目标用来存放各种库文件。
    • lib下建立user和kernel两个子目录,以后供内核使用的库文件就放在lib/kernel/下,lib/user/中是用户进程使用的库文件
  • print.S中完成打印函数:
    1. 备份寄存器现场。
    2. 获取光标坐标值,光标坐标值是下一个可打印字符的位置。
    3. 获取待打印的字符。
    4. 判断字符是否为控制字符,若是回车键、换行符或退格符,则进入相应的处理流程。否则认为是可见字符,进入输入流程。
    5. 判断是否需要滚屏。
    6. 更新光标坐标值,使其指向下一个打印字符的位置。
    7. 恢复寄存器现场,退出。
  • 光标
    • 光标是字符的坐标,只不过是一维的。
    • 因为一个字符占2B(第一个字节是ASCII码,第二个字节是格式),所以光标*2后才是字符在显存中的地址。
    • 光标的位置存放在光标坐标寄存器中。
    • 光标并不是自动更新,光标坐标寄存器是可写的。
    • CRT controller寄存器组中索引为0Eh和0Fh的寄存器分别为:Cursor Location High Register和Cursor Location Low Register,都是8位,分别存储光标的高8位和低8位。
    • 访问CRT controller寄存器组的寄存器,要先往端口地址为0x3d4的Address Register中写入寄存器的索引;再向端口地址为0x3d5的Data Register读/写数据。
  • 滚屏
    • 在80*25模式下的屏幕可显示字符数为2000。
    • 显卡中设置屏幕上显示字符的起始地址的寄存器:Start Address High Register和Start Address Low Register。只要指定起始地址,屏幕自动从该地址开始,向后显示2000字符。
    • 如果起始地址过大,显卡会将其在显存中回绕wrap around。
    • 两种实现方式:
      1. 通过Start Address High Register和Start Address Low Register来设置不同的起始地址,显存中可缓存16KB个字符,屏幕外的字符也可以找回。
      2. 默认情况下Start Address High Register和Start Address Low Register都是0,我们将他们固定为0,丢弃首行的字符。在本程序中我们使用第二种。
    • 第二种方案的实现方式:
      1. 将1 ~ 24行的内容移到0 ~ 23行,将第0行数据覆盖掉。
      2. 将第24行数据用空格覆盖,使它看起来像新行。
      3. 将光标移动到第24行行首。
  • 设置特权级
    • 和硬件相关的访问都属于内核的工作,包括打印。我们要有一套机制来防止用户进程直接访问内核资源的这种越界行为。
    • 检测这种“越权”的行为是由CPU负责的,而真正起检测作用的是人给CPU设置的规则,即特权级。
  • CPU对特权级的检测
    1. 用户进程需要用iretd返回指令上CPU运行的,CPU在执行iretd指令时会做特权检查:它检查DS、ES、FS和GS“数据”段寄存器【除了代码段CS和栈段SS寄存器之外的】。
    2. 在32位环境下,"数据"段寄存器中都是选择子,如果有任何一个段寄存器所指向的段描述符的DPL权限高于从iretd命令返回后的CPL(新的CPL,CPL就是加载到CS寄存器中选择子的RPL),CPU就会将该寄存器赋值为0。
    3. CPU的原则:不能让高特权级的资源被低特权级的程序访问。
    4. 选择子为0表示选择子的索引位、TI位和RPL位都是0,所以会在GDT中检索到第0个段描述符。由于第0个段描述符是空的,所以CPU抛出异常。
  • 用户进程的特权级
    • 用户进程的特权级由CS寄存器中选择子的RPL位决定,它将成为进程在CPU上运行时的CPL。
    • 将来为用户进程初始化寄存器时,CS中的选择子RPL必须为3,进而它就是从iretd指令返回后的新CPL。
    • 而我们用于访问显存的GS寄存器,在新的CPL=3的情况下,无论为它赋予何值,其选择子所指向的段描述符中的DPL都必须等于3。
    • 我们目前使用的显存段描述符是全局描述符表GDT中的第3个段描述符,但其DPL=0,怎么解决呢?
      1. 为用户进程创建一个显存段描述符,DPL=3,专门给用户进程用。
      2. 在打印函数中动手脚,将gs的值改为指向目前DPL=0的显存段描述符。
      3. 我们采用第二种方法,因为与硬件相关的必须请求内核的帮助。
      4. 所以,我们在初始化用户进程寄存器时,将gs赋值为0。用户进程在打印时,需要通过系统调用陷入内核,用户进程的CPL由3->0,执行内核代码,再将gs赋值为内核使用的现存段选择子即可。

-------------------代码-----------------------

  • “/home/lily/OS/boot/lib/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
  • “/home/lily/OS/boot/lib/kernel/print.S”
TI_GDT equ 0
RPL0 equ 0
SELECTOR_VIDEO equ (0x0003 << 3) + TI_GDT + RPL0

[bits 32]
section .text
;-----------put_char---------------
;功能描述:把栈中的1个字符写入光标所在处
;----------------------------------
global put_char                 ;将put_char导出为全局符号
put_char:
    pushad                      ;备份32位寄存器环境,push all double,将832位寄存器都备份了。它们入栈的顺序为:EAX->ECX->EDX->EBX->ESP->EBP->ESI->EDI
    mov ax,SELECTOR_VIDEO       ;需要保证gs中为正确的视频段选择子,为保险起见,每次打印都为gs赋值
    mov gs,ax                   ;不能直接把立即数送入段寄存器
    ;;;;;获取当前光标的位置;;;;;
    ;先获取高8位
    mov dx,0x03d4               ;索引寄存器,03d4为Address Register,用于索引寄存器。
    mov al,0x0e                 ;用于提供光标位置的高8位
    out dx,al
    mov dx,0x03d5               ;03d5是Data Register;可以写数据和读数据。通过读写数据端口0x3d5来获取/设置光标的位置
    in al,dx                    ;得到光标位置的高8位
    mov ah,al                   ;将得到的光标高8位放入ah中

    ;再获取低8位
    mov dx,0x03d4
    mov al,0x0f                 ;用于提供光标位置的低8位
    out dx,al 
    mov dx,0x03d5 
    in al,dx 

    ;将光标位置存入bx,bx寄存器习惯性作为基址寻址。此时bx是下一个字符的输出位置。
    mov bx,ax 
    ;获取栈中压入字符的ASCII码
    mov ecx,[esp + 36]          ;pushad压入8*32b=32字节,加上主调函数4B的返回地址。故栈顶偏移36字节。
    ;判断字符是什么类型
    cmp cl,0xd                  ;CR是0x0d,回车键
    jz .is_carriage_return 
    cmp cl,0xa                  ;LF是0x0a,换行符
    jz .is_line_feed 

    cmp cl,0x8                  ;BS是0x08,退格键
    jz .is_backspace 
    jmp .put_other 

.is_backspace:                  ;理论上将光标移到该字符前即可,但怕下个字符为回车等,原字符还留着当地,所以用空格/空字符0替代原字符
    dec bx                      ;bx值-1,光标指向前一个字符
    shl bx,1                    ;左移一位等于乘2,表示光标对应显存中的偏移字节
    mov byte [gs:bx],0x20       ;0x20表示空格
    inc bx                      ;bx+1
    mov byte [gs:bx],0x07       ;0x07表示黑屏白字,这是显卡默认的前景色和背景色,不加也行。
    shr bx,1                    ;右移一位表示除以2取整,bx由显存的相对地址恢复到光标位置
    jmp .set_cursor             ;设置光标位置

.put_other:                     ;处理可见字符
    shl bx,1                    ;光标左移1位等于乘2,表示光标位置
    mov [gs:bx],cl              ;将ASCII字符放入光标位置中
    inc bx                      ;bx+1
    mov byte [gs:bx],0x07       ;字符属性,黑底白字
    shr bx,1                    ;右移一位表示除以2取整,bx由显存的相对地址恢复到光标位置
    inc bx                      ;bx+1,下一个光标值
    cmp bx,2000                 ;看是否需要滚屏
    jl .set_cursor              ;"JL""jump if less"(如果小于则跳转):若光标值<=2000,表示未写到。显存的最后,则去设置新的光标值,若超过屏幕字符数大小(2000),则换行(滚屏)。

.is_line_feed:                  ;是换行符LF(\n)
.is_carriage_return:            ;是回车键CR(\r),\n和\r在Linux中都是\n的意思。
    xor dx,dx                   ;dx是被除数的高16位,清零
    mov ax,bx                   ;ax是被被除数的低16位,bx是光标位置
    mov si,80                   ;si = 80为除数
    div si                      ;80取模,(dx + ax)/si = ax() + dx(余数) 即bx/80=几行(ax) + 第几列(dx)
    ;如果除数是16位,被除数就是32位,位于dx和ax(高16位,低16位)中;结果的商放在ax中,余数放入dx中
    sub bx,dx                   ;bx-dx表示将bx放在行首,实现了回车的功能。

.is_carriage_return_end:        ;回车符处理结束,判断是否需要滚屏
    add bx,80 
    cmp bx,2000 
.is_line_feed_end:              ;若是LF,则光标移+80即可
    jl .set_cursor

.roll_screen:                   ;若超过屏幕大小,开始滚屏:屏幕范围是0~23,滚屏原理是把1~24->0~23,再将24行用空格填充
    cld 
    mov ecx,960                 ;2000-80=1920个字符,共1920*2=3840字节,一次搬运4字节,一共要搬运3840/4=960次        
    mov esi,0xc00b_80a0         ;1行行首,源索引地址寄存器
    mov edi,0xc00b_8000         ;0行行首,目的索引地址寄存器
    rep movsd                   ;repeat move string doubleword,以32b为单位进行移动,直到ecx=0
    ;将最后一行填充为空白
    mov ebx,3840                ;最后一行从3840开始
    mov ecx,80                  ;一行80字符,每次清空1字符(2B),一行要移动80.cls:
    mov word [gs:ebx],0x0720    ;0x0720是黑底白字的空格键,一次清空一个字符(2B)
    add ebx,2                   ;ebx移动到下一个字符处
    loop .cls                   ;循环.cls,直到ecx=0
    mov bx,1920                 ;bx存放下一个字符的光标位置,即3840/2=1920

.set_cursor:                    ;将光标设置为bx值
    ;先设置高8位
    mov dx,0x03d4               ;索引寄存器,通过0x3d4写入待操作寄存器的索引
    mov al,0x0e                 ;用于提供光标的高8位
    out dx,al 
    mov dx,0x03d5               ;通过数据端口0x3d5来设置光标位置
    mov al,bh                   ;将bx的光标位置的高8位放入al中,通过al输入到dx = 0x3d5端口
    out dx,al                   ;[0x3d5端口] = bx高8= bh
    ;再设置低8位
    mov dx,0x03d4 
    mov al,0x0f                 ;用于提供光标的低8位
    out dx,al 
    mov dx,0x03d5               ;通过数据端口0x3d5来设置光标位置 
    mov al,bl                   ;将bx的光标位置的低8位放入al中,通过al输入到dx = 0x3d5端口 
    out dx,al                   ;[0x3d5端口] = bx低8= bl
    .put_char_done:
        popad                   ;将之前入栈的832b的寄存器出栈
        ret                     
  • 因为print.S的函数put_char为外部函数,其他文件使用时需要声明。为避免麻烦,我们将其写成头文件。
  • “/home/lily/OS/boot/lib/kernel/print.h”
#ifndef __LIB_KERNEL_PRINT_H    //防止头文件被重复包含
#define __LIB_KERNEL_PRINT_H    //以print.h所在路径定义了这个宏,以该宏来判断是否重复包含
#include "stdint.h"
void put_char(uint8_t char_asci);
#endif
  • “/home/lily/OS/boot/kernel/main.c”
#include "print.h"
void main(void){
    
    put_char('k');
    put_char('e');
    put_char('r');
    put_char('n');
    put_char('e');
    put_char('l');
    put_char('\n');
    put_char('1');
    put_char('2');
    put_char('\b');
    put_char('3');
    while (1);
}
  • 编译print.S
nasm -f elf -o lib/kernel/print.o lib/kernel/print.S
  • 编译main.c
gcc -m32 -I lib/kernel -c -o kernel/main.o kernel/main.c
  • 链接main.o和print.o;链接时main.o要放在第一个,先调用,后实现(P274)
ld -m elf_i386 -Ttext=0xc0001500 -e main -o kernel/kernel.bin kernel/main.o lib/kernel/print.o
  • 写入硬盘
dd if=/home/lily/OS/boot/mbr.bin of=/home/lily/bochs/hd60M.img bs=512 count=1 conv=notrunc
dd if=/home/lily/OS/boot/loader.bin of=/home/lily/bochs/hd60M.img  bs=512 count=4 seek=2 conv=notrunc
dd if=/home/lily/OS/boot/kernel/kernel.bin of=/home/lily/bochs/hd60M.img bs=512 count=200 seek=9 conv=notrunc

在这里插入图片描述

  • 启动bochs
bin/bochs -f bochsrc.disk

在这里插入图片描述

3. 实现字符串打印

  • 通过比较字符是否为’\0’来判断字符串是否结束
    在这里插入图片描述
  • C编译器会为字符串分配一块内存,在这块内存中存储字符串中各字符的ASCII码,并且在结尾处自动补上’\0’,其ASCII码为0。
  • 编译器将字符串作为参数时,传递的是字符串所在的内存起始地址,也就是说压入栈中的是传出该字符串的内存首地址。

-------------------代码-----------------------

  • “/home/lily/OS/boot/lib/kernel/print.S”
TI_GDT equ 0
RPL0 equ 0
SELECTOR_VIDEO equ (0x0003 << 3) + TI_GDT + RPL0

[bits 32]
section .text
;------------------------------------------
;put_str通过put_char来打印以0字符结尾的字符串
;------------------------------------------
;输入:栈中参数为打印的字符串
;输出:无

global put_str 
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传递参数,把ecx的值入栈
    call put_char               ;call时会把返回地址入栈4
    add esp,4                   ;回收参数的栈空间
    inc ebx                     ;使ebx指向下一个字符
    jmp .goon
.str_over:
    pop ecx 
    pop ebx 
    ret 

;-----------put_char---------------
;功能描述:把栈中的1个字符写入光标所在处
;----------------------------------
global put_char                 ;将put_char导出为全局符号
put_char:
    pushad                      ;备份32位寄存器环境,push all double,将832位寄存器都备份了。它们入栈的顺序为:EAX->ECX->EDX->EBX->ESP->EBP->ESI->EDI
    mov ax,SELECTOR_VIDEO       ;需要保证gs中为正确的视频段选择子,为保险起见,每次打印都为gs赋值
    mov gs,ax                   ;不能直接把立即数送入段寄存器
    ;;;;;获取当前光标的位置;;;;;
    ;先获取高8位
    mov dx,0x03d4               ;索引寄存器,03d4为Address Register,用于索引寄存器。
    mov al,0x0e                 ;用于提供光标位置的高8位
    out dx,al
    mov dx,0x03d5               ;03d5是Data Register;可以写数据和读数据。通过读写数据端口0x3d5来获取/设置光标的位置
    in al,dx                    ;得到光标位置的高8位
    mov ah,al                   ;将得到的光标高8位放入ah中

    ;再获取低8位
    mov dx,0x03d4
    mov al,0x0f                 ;用于提供光标位置的低8位
    out dx,al 
    mov dx,0x03d5 
    in al,dx 

    ;将光标位置存入bx,bx寄存器习惯性作为基址寻址。此时bx是下一个字符的输出位置。
    mov bx,ax 
    ;获取栈中压入字符的ASCII码
    mov ecx,[esp + 36]          ;pushad压入8*32b=32字节,加上主调函数4B的返回地址。故栈顶偏移36字节。
    ;判断字符是什么类型
    cmp cl,0xd                  ;CR是0x0d,回车键
    jz .is_carriage_return 
    cmp cl,0xa                  ;LF是0x0a,换行符
    jz .is_line_feed 

    cmp cl,0x8                  ;BS是0x08,退格键
    jz .is_backspace 
    jmp .put_other 

.is_backspace:                  ;理论上将光标移到该字符前即可,但怕下个字符为回车等,原字符还留着当地,所以用空格/空字符0替代原字符
    dec bx                      ;bx值-1,光标指向前一个字符
    shl bx,1                    ;左移一位等于乘2,表示光标对应显存中的偏移字节
    mov byte [gs:bx],0x20       ;0x20表示空格
    inc bx                      ;bx+1
    mov byte [gs:bx],0x07       ;0x07表示黑屏白字,这是显卡默认的前景色和背景色,不加也行。
    shr bx,1                    ;右移一位表示除以2取整,bx由显存的相对地址恢复到光标位置
    jmp .set_cursor             ;设置光标位置

.put_other:                     ;处理可见字符
    shl bx,1                    ;光标左移1位等于乘2,表示光标位置
    mov [gs:bx],cl              ;将ASCII字符放入光标位置中
    inc bx                      ;bx+1
    mov byte [gs:bx],0x07       ;字符属性,黑底白字
    shr bx,1                    ;右移一位表示除以2取整,bx由显存的相对地址恢复到光标位置
    inc bx                      ;bx+1,下一个光标值
    cmp bx,2000                 ;看是否需要滚屏
    jl .set_cursor              ;"JL""jump if less"(如果小于则跳转):若光标值<=2000,表示未写到。显存的最后,则去设置新的光标值,若超过屏幕字符数大小(2000),则换行(滚屏)。

.is_line_feed:                  ;是换行符LF(\n)
.is_carriage_return:            ;是回车键CR(\r),\n和\r在Linux中都是\n的意思。
    xor dx,dx                   ;dx是被除数的高16位,清零
    mov ax,bx                   ;ax是被被除数的低16位,bx是光标位置
    mov si,80                   ;si = 80为除数
    div si                      ;80取模,(dx + ax)/si = ax() + dx(余数) 即bx/80=几行(ax) + 第几列(dx)
    ;如果除数是16位,被除数就是32位,位于dx和ax(高16位,低16位)中;结果的商放在ax中,余数放入dx中
    sub bx,dx                   ;bx-dx表示将bx放在行首,实现了回车的功能。

.is_carriage_return_end:        ;回车符处理结束,判断是否需要滚屏
    add bx,80 
    cmp bx,2000 
.is_line_feed_end:              ;若是LF,则光标移+80即可
    jl .set_cursor

.roll_screen:                   ;若超过屏幕大小,开始滚屏:屏幕范围是0~23,滚屏原理是把1~24->0~23,再将24行用空格填充
    cld 
    mov ecx,960                 ;2000-80=1920个字符,共1920*2=3840字节,一次搬运4字节,一共要搬运3840/4=960次        
    mov esi,0xc00b_80a0         ;1行行首,源索引地址寄存器
    mov edi,0xc00b_8000         ;0行行首,目的索引地址寄存器
    rep movsd                   ;repeat move string doubleword,以32b为单位进行移动,直到ecx=0
    ;将最后一行填充为空白
    mov ebx,3840                ;最后一行从3840开始
    mov ecx,80                  ;一行80字符,每次清空1字符(2B),一行要移动80.cls:
    mov word [gs:ebx],0x0720    ;0x0720是黑底白字的空格键,一次清空一个字符(2B)
    add ebx,2                   ;ebx移动到下一个字符处
    loop .cls                   ;循环.cls,直到ecx=0
    mov bx,1920                 ;bx存放下一个字符的光标位置,即3840/2=1920

.set_cursor:                    ;将光标设置为bx值
    ;先设置高8位
    mov dx,0x03d4               ;索引寄存器,通过0x3d4写入待操作寄存器的索引
    mov al,0x0e                 ;用于提供光标的高8位
    out dx,al 
    mov dx,0x03d5               ;通过数据端口0x3d5来设置光标位置
    mov al,bh                   ;将bx的光标位置的高8位放入al中,通过al输入到dx = 0x3d5端口
    out dx,al                   ;[0x3d5端口] = bx高8= bh
    ;再设置低8位
    mov dx,0x03d4 
    mov al,0x0f                 ;用于提供光标的低8位
    out dx,al 
    mov dx,0x03d5               ;通过数据端口0x3d5来设置光标位置 
    mov al,bl                   ;将bx的光标位置的低8位放入al中,通过al输入到dx = 0x3d5端口 
    out dx,al                   ;[0x3d5端口] = bx低8= bl
    .put_char_done:
        popad                   ;将之前入栈的832b的寄存器出栈
        ret                     
  • “/home/lily/OS/boot/lib/kernel/print.h”
#ifndef __LIB_KERNEL_PRINT_H    //防止头文件被重复包含
#define __LIB_KERNEL_PRINT_H    //以print.h所在路径定义了这个宏,以该宏来判断是否重复包含
#include "stdint.h"
void put_char(uint8_t char_asci);
void put_str(char* message);
#endif
  • “/home/lily/OS/boot/kernel/main.c”
#include "print.h"
void main(void){
    
    put_str("I am kernel\n");
    while (1);
}
  • 编译
nasm -f elf -o lib/kernel/print.o lib/kernel/print.S
gcc -m32 -I lib/kernel -c -o kernel/main.o kernel/main.c
  • 链接;链接时main.o要放在第一个,先调用,后实现(P274)
ld -m elf_i386 -Ttext=0xc0001500 -e main -o kernel/kernel.bin kernel/main.o lib/kernel/print.o
  • 写入硬盘
dd if=/home/lily/OS/boot/mbr.bin of=/home/lily/bochs/hd60M.img bs=512 count=1 conv=notrunc
dd if=/home/lily/OS/boot/loader.bin of=/home/lily/bochs/hd60M.img  bs=512 count=4 seek=2 conv=notrunc
dd if=/home/lily/OS/boot/kernel/kernel.bin of=/home/lily/bochs/hd60M.img bs=512 count=200 seek=9 conv=notrunc

在这里插入图片描述

  • 启动bochs
bin/bochs -f bochsrc.disk

在这里插入图片描述

4.实现整数打印

  • 原理:将数字转换成对应的字符

-------------------代码-----------------------

  • “/home/lily/OS/boot/lib/kernel/print.S”
TI_GDT equ 0
RPL0 equ 0
SELECTOR_VIDEO equ (0x0003 << 3) + TI_GDT + RPL0

section .data 
put_int_buffer dq 0             ;定义8字节缓冲区用于数字到字符的转换

[bits 32]
section .text

;------------------------------------------
;put_str通过put_char来打印以0字符结尾的字符串
;------------------------------------------
;输入:栈中参数为打印的字符串
;输出:无

global put_str 
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传递参数,把ecx的值入栈
    call put_char               ;call时会把返回地址入栈4
    add esp,4                   ;回收参数的栈空间
    inc ebx                     ;使ebx指向下一个字符
    jmp .goon
.str_over:
    pop ecx 
    pop ebx 
    ret 

;-----------put_char---------------
;功能描述:把栈中的1个字符写入光标所在处
;----------------------------------
global put_char                 ;将put_char导出为全局符号
put_char:
    pushad                      ;备份32位寄存器环境,push all double,将832位寄存器都备份了。它们入栈的顺序为:EAX->ECX->EDX->EBX->ESP->EBP->ESI->EDI
    mov ax,SELECTOR_VIDEO       ;需要保证gs中为正确的视频段选择子,为保险起见,每次打印都为gs赋值
    mov gs,ax                   ;不能直接把立即数送入段寄存器
    ;;;;;获取当前光标的位置;;;;;
    ;先获取高8位
    mov dx,0x03d4               ;索引寄存器,03d4为Address Register,用于索引寄存器。
    mov al,0x0e                 ;用于提供光标位置的高8位
    out dx,al
    mov dx,0x03d5               ;03d5是Data Register;可以写数据和读数据。通过读写数据端口0x3d5来获取/设置光标的位置
    in al,dx                    ;得到光标位置的高8位
    mov ah,al                   ;将得到的光标高8位放入ah中

    ;再获取低8位
    mov dx,0x03d4
    mov al,0x0f                 ;用于提供光标位置的低8位
    out dx,al 
    mov dx,0x03d5 
    in al,dx 

    ;将光标位置存入bx,bx寄存器习惯性作为基址寻址。此时bx是下一个字符的输出位置。
    mov bx,ax 
    ;获取栈中压入字符的ASCII码
    mov ecx,[esp + 36]          ;pushad压入8*32b=32字节,加上主调函数4B的返回地址。故栈顶偏移36字节。
    ;判断字符是什么类型
    cmp cl,0xd                  ;CR是0x0d,回车键
    jz .is_carriage_return 
    cmp cl,0xa                  ;LF是0x0a,换行符
    jz .is_line_feed 

    cmp cl,0x8                  ;BS是0x08,退格键
    jz .is_backspace 
    jmp .put_other 

.is_backspace:                  ;理论上将光标移到该字符前即可,但怕下个字符为回车等,原字符还留着当地,所以用空格/空字符0替代原字符
    dec bx                      ;bx值-1,光标指向前一个字符
    shl bx,1                    ;左移一位等于乘2,表示光标对应显存中的偏移字节
    mov byte [gs:bx],0x20       ;0x20表示空格
    inc bx                      ;bx+1
    mov byte [gs:bx],0x07       ;0x07表示黑屏白字,这是显卡默认的前景色和背景色,不加也行。
    shr bx,1                    ;右移一位表示除以2取整,bx由显存的相对地址恢复到光标位置
    jmp .set_cursor             ;设置光标位置

.put_other:                     ;处理可见字符
    shl bx,1                    ;光标左移1位等于乘2,表示光标位置
    mov [gs:bx],cl              ;将ASCII字符放入光标位置中
    inc bx                      ;bx+1
    mov byte [gs:bx],0x07       ;字符属性,黑底白字
    shr bx,1                    ;右移一位表示除以2取整,bx由显存的相对地址恢复到光标位置
    inc bx                      ;bx+1,下一个光标值
    cmp bx,2000                 ;看是否需要滚屏
    jl .set_cursor              ;"JL""jump if less"(如果小于则跳转):若光标值<=2000,表示未写到。显存的最后,则去设置新的光标值,若超过屏幕字符数大小(2000),则换行(滚屏)。

.is_line_feed:                  ;是换行符LF(\n)
.is_carriage_return:            ;是回车键CR(\r),\n和\r在Linux中都是\n的意思。
    xor dx,dx                   ;dx是被除数的高16位,清零
    mov ax,bx                   ;ax是被被除数的低16位,bx是光标位置
    mov si,80                   ;si = 80为除数
    div si                      ;80取模,(dx + ax)/si = ax() + dx(余数) 即bx/80=几行(ax) + 第几列(dx)
    ;如果除数是16位,被除数就是32位,位于dx和ax(高16位,低16位)中;结果的商放在ax中,余数放入dx中
    sub bx,dx                   ;bx-dx表示将bx放在行首,实现了回车的功能。

.is_carriage_return_end:        ;回车符处理结束,判断是否需要滚屏
    add bx,80 
    cmp bx,2000 
.is_line_feed_end:              ;若是LF,则光标移+80即可
    jl .set_cursor

.roll_screen:                   ;若超过屏幕大小,开始滚屏:屏幕范围是0~23,滚屏原理是把1~24->0~23,再将24行用空格填充
    cld 
    mov ecx,960                 ;2000-80=1920个字符,共1920*2=3840字节,一次搬运4字节,一共要搬运3840/4=960次        
    mov esi,0xc00b_80a0         ;1行行首,源索引地址寄存器
    mov edi,0xc00b_8000         ;0行行首,目的索引地址寄存器
    rep movsd                   ;repeat move string doubleword,以32b为单位进行移动,直到ecx=0
    ;将最后一行填充为空白
    mov ebx,3840                ;最后一行从3840开始
    mov ecx,80                  ;一行80字符,每次清空1字符(2B),一行要移动80.cls:
    mov word [gs:ebx],0x0720    ;0x0720是黑底白字的空格键,一次清空一个字符(2B)
    add ebx,2                   ;ebx移动到下一个字符处
    loop .cls                   ;循环.cls,直到ecx=0
    mov bx,1920                 ;bx存放下一个字符的光标位置,即3840/2=1920

.set_cursor:                    ;将光标设置为bx值
    ;先设置高8位
    mov dx,0x03d4               ;索引寄存器,通过0x3d4写入待操作寄存器的索引
    mov al,0x0e                 ;用于提供光标的高8位
    out dx,al 
    mov dx,0x03d5               ;通过数据端口0x3d5来设置光标位置
    mov al,bh                   ;将bx的光标位置的高8位放入al中,通过al输入到dx = 0x3d5端口
    out dx,al                   ;[0x3d5端口] = bx高8= bh
    ;再设置低8位
    mov dx,0x03d4 
    mov al,0x0f                 ;用于提供光标的低8位
    out dx,al 
    mov dx,0x03d5               ;通过数据端口0x3d5来设置光标位置 
    mov al,bl                   ;将bx的光标位置的低8位放入al中,通过al输入到dx = 0x3d5端口 
    out dx,al                   ;[0x3d5端口] = bx低8= bl
    .put_char_done:
        popad                   ;将之前入栈的832b的寄存器出栈
        ret                     

;------------将小端字节序的数字变成对应的ASCII码后,倒置--------------
;输入:栈中参数为待打印的数字
;输出:在屏幕上打印16进制数字,并不会打印前缀0x
;------------------------------------------------------------------
global put_int 
put_int:
    pushad                      
    mov ebp,esp 
    mov eax,[ebp+4*9]           ;将参数写入eax中,call返回地址占4B+pushad的84B
    mov edx,eax                 ;eax存储的是参数的备份,edx为每次参与位变换的参数,当转换为16进制数字后,eax将下一个参数给edx
    mov edi,7                   ;指定在put_int_buffer中初始的偏移量,表示指向缓冲区的最后一个字节
    mov ecx,8                   ;32位数字中,每4位表示一个16进制数字。所以32位可以表示816进制数字,位数为8。
    mov ebx,put_int_buffer      ;ebx为缓冲区的基址
;32位数字按照16进制的形式从低到高逐个处理,共处理816进制数字
.16based_4bits:
    ;32位数字按照16进制形式从低到高逐字处理
    and edx,0x0000_000F          ;解析16进制数字的每一位,and后edx只有低4位有效(最低位的16进制数字)
    cmp edx,9                   ;数字0~9和a~f需要分别处理成对应的字符
    jg .is_A2F                  ;jg:Jump if Greater,若大于9,则跳转.is_A2F
    add edx,'0'                 ;如果是0~9,则加上'0'的ASCII码
    jmp .store 
.is_A2F:
    sub edx,10                  ;A~F减去10所得的差,10的ASCII码为1
    add edx,'A'                 ;加上10的ASCII码得到字符的ASCII码
;将每个数字转换成对应的字符后,按照类似大端的顺序存储到缓冲区put_int_buffer中。
;高位字符放在低地址,低位字符放在高地址,这样和大端字符序类似。
.store:
    ;此时dl中应该是对应数字的ASCII码
    mov [ebx+edi],dl 
    dec edi 
    shr eax,4                   ;右移4位,去掉最低4位
    mov edx,eax
    loop .16based_4bits
;现在把put_int_buffer中已全是字符,打印之前把高位连续的字符去掉。
;例如:000123 -> 123
.ready_to_print:
    inc edi                     ;此时edi为-1(0xffff_ffff),加1使其为0
.skip_prefix_0:
    cmp edi,8                   ;若以及比较到第9个字符,表示待打印的字符都是0
    je .full0                   ;Jump if Equal 
;找出连续的0字符,edi作为非0的最高位字符的偏移
.go_on_skip:
    mov cl,[put_int_buffer+edi] 
    inc edi 
    cmp cl,'0'                  ;判断下一位字符是否为0 
    je .skip_prefix_0           
    dec edi                     ;若当前字符不为'0',则使edi减1恢复当前字符            
    jmp .put_each_num           ;若下一位不为0,则从这一位开始遍历
.full0:
    mov cl,'0'                  ;当输入字符都是0时,只打印0
.put_each_num:
    push ecx                    ;此时ecx中为可打印字符,作为参数传递入put_char中
    call put_char
    add esp,4                   ;覆盖掉ecx,清理栈参数,相当于pop ecx
    inc edi                     ;使edi指向下个字符
    mov cl,[put_int_buffer+edi] ;将下个字符放入cl中
    cmp edi,8
    jl .put_each_num
    popad 
    ret        
  • “/home/lily/OS/boot/lib/kernel/print.h”
#ifndef __LIB_KERNEL_PRINT_H    //防止头文件被重复包含
#define __LIB_KERNEL_PRINT_H    //以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
  • “/home/lily/OS/boot/kernel/main.c”
#include "print.h"
void main(void){
    
    put_str("I am kernel\n");
    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);
    while (1);
}
  • 编译
nasm -f elf -o lib/kernel/print.o lib/kernel/print.S
gcc -m32 -I lib/kernel -c -o kernel/main.o kernel/main.c
  • 链接;链接时main.o要放在第一个,先调用,后实现(P274)
ld -m elf_i386 -Ttext=0xc0001500 -e main -o kernel/kernel.bin kernel/main.o lib/kernel/print.o
  • 写入硬盘
dd if=/home/lily/OS/boot/mbr.bin of=/home/lily/bochs/hd60M.img bs=512 count=1 conv=notrunc
dd if=/home/lily/OS/boot/loader.bin of=/home/lily/bochs/hd60M.img  bs=512 count=4 seek=2 conv=notrunc
dd if=/home/lily/OS/boot/kernel/kernel.bin of=/home/lily/bochs/hd60M.img bs=512 count=200 seek=9 conv=notrunc

在这里插入图片描述

  • 启动bochs
bin/bochs -f bochsrc.disk

在这里插入图片描述

四、内联汇编

1. 什么是内联汇编

  • 定义:gcc支持在C代码中直接嵌入汇编代码,所以内联汇编又称为gcc assembly code。
  • 分类:
    1. 最简单的基本内联汇编
    2. 复杂一些的扩展内联汇编

2. 汇编语言AT&T

  • AT&T是汇编语言的一种语法风格
  • 语法规则:在指令名字后加上了操作数大小后缀,b表示1字节、w表示两字节、l表示4字节。
    在这里插入图片描述
  • 立即数与地址
    1. 在Intel语法中,立即数就是普通数字,如果想用立即数表示地址,需要加上中括号。[立即数]才表示地址。
    2. AT&T中,数字被优先认为是地址,若想表示立即数,则需要加上$前缀。
  • AT&T内存寻址
    1. segreg(段基址):base_address(offset_address,index,size)对应的表达式是segreg(段基址):base_address+offset+indexsize。相当于Intel32位内存寻址的segreg:[base+indexsize+offset]。
    2. base_address是基地址,可以为整数/变量名,可正可负。
    3. offset_address是偏移地址,index为索引值,这两个必须是8个通用寄存器之一。
    4. size是长度,只能为1、2、3、4。
    5. 直接寻址:movl $255,0xc000_08f0或mov $6,var(变量名)。
    6. 寄存器间接寻址:寻址中只有offset_address项。如mov (%eax),%ebx。
    7. 寄存器相对寻址:只有offset_address和base_address项,格式为base_address(offset_address),这样得出的内存地址为基址+偏移地址之和。如:movb -4(%ebx),%al,意思是将(ebx-4)所指向的内存复制1字节到寄存器al。
    8. 变址寻址:称为变址的原因是它有变量index,因为index是size的倍数,所以有index就有size。
      在这里插入图片描述

3. 基本内联汇编

  • 格式:
asm [volatile] ("assembly code")
  • 各关键字之间可以用空格/制表符分隔,也可以紧凑。
  • asm和__asm__是一样的,是由gcc定义的宏:#define asm asm。volatile和__volatile__是一样的,是由gcc定义的宏:#define volatile volatile。
  • gcc -o可以指定优化级别,volatile表示原样保留汇编代码。
  • 内联汇编是我们写的assembly code,它必须在圆括号中,用双引号引起来,可以为空。
  • assembly code规则:
    1. 指令必须由双引号引起来,无论双引号中是一条/多条指令。
    2. 一对双引号不能跨行,如果跨行需要在结尾用’'转义。
    3. 指令之间用分号、换行符或换行符+制表符分隔:‘;’ ‘\n’ ‘\n’‘\t’。
    4. 即使指令分布在多个双引号中,gcc最终也要将它们合并到一起处理。合并后,指令间必须有分隔符,所以除最后一个双引号外,其余双引号结尾处必须有分隔符。
  • 例如:
char* str="hello,world\n";
int count=0;
void main(){
    
	asm("pusha; \				//将8个通用寄存器入栈
		movl $4,%eax; \			//write的系统调用号
		movl $1,%ebx; \			//为write系统调用传参
		movl str,%ecx; \
		movl $12,%edx; \		
		int $0x80				//执行系统调用		
		mov %eax,count; \		//获取write返回值,返回值存储在eax中,将其复制到count中	
		popa
		");
}

4. 扩展内联汇编

  • 内联汇编格式
    asm [volatile] ("assembly code":output:intput:clobber/modify)
    
    1. 四部分都可以省略,省略部分要保留分隔符。如果省略的是后面的一/多个部分,则分隔符不用保留。
    2. assembly code:汇编指令。
    3. 内联汇编的目的是让汇编帮助C完成某些功能,所以C代码要为其提供参数和用于存放输出结果的空间。内联汇编类似机器,C代码类似人,人要为机器提供原材料input,机器运行后,将产出放入到output中。
    4. output中每个操作数的格式为:“操作数修饰符约束名(C变量名)”。引号和圆括号不能省略,操作数修饰符为可选项,多个操作数之间使用逗号分隔。
    5. input中每个操作数的格式为:“[操作数修饰符] 约束名”(C变量名)。引号和圆括号不能省略,操作数修饰符通常为等号,多个操作数之间使用逗号分隔。
    6. clobber/modify:汇编代码执行后会破坏一些内存/寄存器资源,通过此项通知编译器,让gcc把它们保护起来。
  • 约束
    1. 作用:将C代码中的操作数(变量、立即数)映射为汇编中所使用的操作数。

    2. 作用域:input和output。

    3. 分类:

      1. 寄存器约束

        • 寄存器约束就是要求gcc使用哪个寄存器,将input或output中变量约束在某个寄存器中。
        • g:表示可以存放到任意地点(寄存器和内存),即:除了和q一样,还可以让gcc安排在内存中。
        字符 表示的寄存器 字符 表示的寄存器
        a eax/ax/al D edi\di
        b ebx\bx\bl S esi\si
        c ecx\cx\cl A 把eax和edx组合成64位整数
        d edx\dx\dl f 表示浮点寄存器
        t 表示第1个浮点寄存器 u 表示第2个浮点寄存器
        q 任意4个通用寄存器之一:eax\ebx\ecx\edx r 任意6个通用寄存器之一:eax\ebx\ecx\edx\esi\edi
      //加法操作
      //1.基本内联汇编
      #include<stdio.h>
      int in_a = 1,in_b = 2,out_sum;
      void main(){
              
      	asm("pusha;
      		movl in_a,%eax;
      		movl in_b,%ebx;
      		addl %ebx,%eax;
      		movl %eax,out_sum;
      		popa");
      	printf("sum is %d\n",out_sum);
      }
      //2.扩展内联汇编 %表示占位符,所以寄存器前是两个%
      #include<stdio.h>
      void main(){
              
      	int in_a = 1,in_b = 2,out_sum;
      	asm("addl %%ebx,%%eax":"=a"(out_sum):"a"(in_a),"b"(in_b));
      	printf("sum is %d\n",out_sum);
      }
      
      1. 内存约束
        • 内存约束是要求gcc直接将位于input和output中的C变量的内存地址作为内联汇编代码的操作数,不需要寄存器做中转,直接进行内存读写。也就是汇编代码的操作数是C变量的指针。
        • m:表示操作数可以使用任意一种内存形式。
        • o:操作数为内存变量,但访问它是通过偏移量的形式访问,即包含offset_address的格式。
        #include<stdio.h>
        void main(){
                  
        	int in_a = 1,in_b = 2;
        	printf("in_b is %d\n",in_b);
        	asm("movb %b0,%1;"::"a"(in_a),"m"(in_b));	//将a的值给b
        	//%1是序号占位符,%b0是32为数据的低8位
        	printf("in_b now is %d\n",in_b);
        }
        
      2. 立即数约束
        • 立即数就是常数,此约束要求gcc在传值的时候不通过内存和寄存器,直接作为立即数传给汇编代码。
      字符 操作数代表的立即数 字符 操作数代表的立即数
      i 整数 F 浮点数
      I 0 ~ 31之间的立即数 J 0 ~ 63之间的立即数
      N 0 ~ 255之间的立即数 O 0 ~ 32之间的立即数
      X 任何类型的立即数
      1. 通用约束
        • 0~9:此约束只用在input部分,但表示可与output和input中的第n个操作数用相同的寄存器/内存。
    • 总结:由于是在C代码中插入汇编,所以约束的作用是让C代码的操作数变成汇编代码能使用的操作数。在内联汇编中用到的操作数,都是位于input和output中C操作数的副本,多数通过赋值的方式传给汇编代码,或通过指针的方式,当操作数的副本在汇编中处理完后,又重新赋值给C操作数。
  • 占位符
    • 作用:代表约束指定的操作数(寄存器、内存、立即数)
    • 分类:
      1. 序号占位符
        • 序号占位符是对在input和output中的操作数,按照它们从左到右出现的次序从0编号到9,最多支持10个序号占位符。
        • 操作数用在assembly code中,引用格式为:%0~9
        • 占位符指代约束对应的操作数,也就是汇编中的操作数,并不是圆括号中的C变量。
        asm("addl %%ebx,%%eax":"=a"(out_sum):"a"(in_a),"b"(in_b));
        //等价于
        asm("addl %2,%1":"=a"(out_sum):"a"(in_a),"b"(in_b));
        //"=a"(out_sum)序号为0,%0对应的是eax
        //"a"(in_a)序号为1,%1对应的是eax
        //"b"(in_b)序号为2,%1对应的是ebx
        
        • 占位符所表示的操作数默认为32位,但是可以根据指令的b、w、l 而选取低8、16、32位。也可以根据字符’h’和’b’来选取中8位(ah等)和低8位(al等)。
        #include<stdio.h>
        void main(){
                  
        	int in_a = 0x12345678,in_b = 0;
        	asm("movw %1,%0":"=m"(in_b):"a"(in_a));
        	printf("word in_b is 0x%x\n",in_b);	//b = 5678
        	in_b = 0;	//初始化in_b。防止紊乱
        
        	asm("movb %1,%0":"=m"(in_b):"a"(in_a));
        	printf("low byte in_b is 0x%x\n",in_b);	//b = 78
        	in_b = 0;	//初始化in_b。防止紊乱
        
        	asm("movb %h1,%0":"=m"(in_b):"a"(in_a));
        	printf("high byte in_b is 0x%x\n",in_b);	//b = 56
        }
        
      2. 名称占位符
        • 名称占位符需要在input和output中把操作数显示地起个名字,没有个数限制。
        • 指令格式:[名称]“约束名”(C变量)
        • 在assembly code中,采用%[名称]来引用操作数。
        #include<stdio.h>
        void main(){
                  
        	int in_a = 18,in_b = 3,out = 0;
        	asm("divb %[divisor];movb %%al.%[result]"
        		:[result]"=m"(out)
        		:"a"(in_a),[divisor]"m"(in_b)
        		);
        	printf("result is %d\n",out);	//18/3=6
        }
        
  • 操作数类型修饰符
    • 作用:用来修饰所约束的操作数:内存、寄存器。

    • 在output中:

      操作数类型修饰符 作用
      = 表示操作数只写,相当于为output括号中的C变量赋值,如:=a(c_var)相当于c_var=eax
      + 表示操作数可读写,告诉gcc所约束的寄存器/内存先被读入,再被写入。
      & 表示此output中的操作数要独占所约束(分配)的寄存器,只供output使用,任何input中所分配的寄存器不能与此相同。
    • 在input中:

      操作数类型修饰符 作用
      % 表示该操作数可以和下一个输入操作数互换
    • 一般情况下。input中的C变量是只读的,output中的C变量是只写的。

    • “+” 表示该output的C变量即可作为输入,也可作为输出,省去了在input中的声明约束。

    #include<stdio.h>
    		void main(){
          
    			int in_a = 1,in_b = 2;
    			asm("addl %%ebx,%%eax;":"+a"(in_a):"b"(in_b));
    			printf("in_a is %d\n",in_a);	
    		}
    
    • “&”用来表示此寄存器只能分配给output中的某C变量使用,不能再分配给input中某变量了。
    • "%"表示input中的输入可以和下一个input操作数互换,通常用在计算结果与操作数顺序无关的指令中。
    #include<stdio.h>
    void main(){
          
    	int in_a = 1,sum = 0;
    	asm("addl %1,%0;":"=a"(sum):"%I"(2),"0"(in_a)); 
    	//"%I"(2)表示立即数2,"0"(in_a)为通用约束,表示in_a会被分配到%0的寄存器中(sum所在的寄存器中),即eax中。
    	printf("sum is %d\n",sum);
    }
    
  • clobber/modify
    • 作用:告诉gcc我们修改了哪些寄存器/内存。
    • 如果在input和output中通过寄存器约束指定了寄存器,gcc必然会知道这些寄存器会被修改。所以,需要在clobber/modify中通知的寄存器是在assembly code中出现的,而在input和output中没出现的。
    • 格式:用双引号把寄存器引起来,多个寄存器间用逗号隔开,寄存器只需要写名称即可。
    • 在clobber/modify中,即使只写al、ax、eax,也表示eax。因为即使只动了寄存器的一部分,它的整体也会受影响。
    • 如果修改了标志寄存器eflags的标志位,则用"cc"声明。
    • 如果修改了内存,用"memory"声明。
    //例如:
    asm("movl %%eax,%0;movl %%eax,%%ebx":"=m"(ret_value)::"bx");
    

5. 扩展内联汇编之机器模式简介

  • 机器模式是用来在机器层面上指定数据的大小和格式的。
  • 我们需要了解的几个操作码
操作码 输出 例如
h 输出寄存器中8位(1字节)部分 ah、bh、ch、dh
b 输出寄存器中低8位(1字节)部分 al、bl、cl、dl
w 输出低16位(2字节)对应的部分 ax、bx、cx、dx
k 输出寄存器的32位(4字节)部分 eax、ebx、ecx、edx
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/weixin_45806011/article/details/135397144

智能推荐

分布式光纤传感器的全球与中国市场2022-2028年:技术、参与者、趋势、市场规模及占有率研究报告_预计2026年中国分布式传感器市场规模有多大-程序员宅基地

文章浏览阅读3.2k次。本文研究全球与中国市场分布式光纤传感器的发展现状及未来发展趋势,分别从生产和消费的角度分析分布式光纤传感器的主要生产地区、主要消费地区以及主要的生产商。重点分析全球与中国市场的主要厂商产品特点、产品规格、不同规格产品的价格、产量、产值及全球和中国市场主要生产商的市场份额。主要生产商包括:FISO TechnologiesBrugg KabelSensor HighwayOmnisensAFL GlobalQinetiQ GroupLockheed MartinOSENSA Innovati_预计2026年中国分布式传感器市场规模有多大

07_08 常用组合逻辑电路结构——为IC设计的延时估计铺垫_基4布斯算法代码-程序员宅基地

文章浏览阅读1.1k次,点赞2次,收藏12次。常用组合逻辑电路结构——为IC设计的延时估计铺垫学习目的:估计模块间的delay,确保写的代码的timing 综合能给到多少HZ,以满足需求!_基4布斯算法代码

OpenAI Manager助手(基于SpringBoot和Vue)_chatgpt网页版-程序员宅基地

文章浏览阅读3.3k次,点赞3次,收藏5次。OpenAI Manager助手(基于SpringBoot和Vue)_chatgpt网页版

关于美国计算机奥赛USACO,你想知道的都在这_usaco可以多次提交吗-程序员宅基地

文章浏览阅读2.2k次。USACO自1992年举办,到目前为止已经举办了27届,目的是为了帮助美国信息学国家队选拔IOI的队员,目前逐渐发展为全球热门的线上赛事,成为美国大学申请条件下,含金量相当高的官方竞赛。USACO的比赛成绩可以助力计算机专业留学,越来越多的学生进入了康奈尔,麻省理工,普林斯顿,哈佛和耶鲁等大学,这些同学的共同点是他们都参加了美国计算机科学竞赛(USACO),并且取得过非常好的成绩。适合参赛人群USACO适合国内在读学生有意向申请美国大学的或者想锻炼自己编程能力的同学,高三学生也可以参加12月的第_usaco可以多次提交吗

MySQL存储过程和自定义函数_mysql自定义函数和存储过程-程序员宅基地

文章浏览阅读394次。1.1 存储程序1.2 创建存储过程1.3 创建自定义函数1.3.1 示例1.4 自定义函数和存储过程的区别1.5 变量的使用1.6 定义条件和处理程序1.6.1 定义条件1.6.1.1 示例1.6.2 定义处理程序1.6.2.1 示例1.7 光标的使用1.7.1 声明光标1.7.2 打开光标1.7.3 使用光标1.7.4 关闭光标1.8 流程控制的使用1.8.1 IF语句1.8.2 CASE语句1.8.3 LOOP语句1.8.4 LEAVE语句1.8.5 ITERATE语句1.8.6 REPEAT语句。_mysql自定义函数和存储过程

半导体基础知识与PN结_本征半导体电流为0-程序员宅基地

文章浏览阅读188次。半导体二极管——集成电路最小组成单元。_本征半导体电流为0

随便推点

【Unity3d Shader】水面和岩浆效果_unity 岩浆shader-程序员宅基地

文章浏览阅读2.8k次,点赞3次,收藏18次。游戏水面特效实现方式太多。咱们这边介绍的是一最简单的UV动画(无顶点位移),整个mesh由4个顶点构成。实现了水面效果(左图),不动代码稍微修改下参数和贴图可以实现岩浆效果(右图)。有要思路是1,uv按时间去做正弦波移动2,在1的基础上加个凹凸图混合uv3,在1、2的基础上加个水流方向4,加上对雾效的支持,如没必要请自行删除雾效代码(把包含fog的几行代码删除)S..._unity 岩浆shader

广义线性模型——Logistic回归模型(1)_广义线性回归模型-程序员宅基地

文章浏览阅读5k次。广义线性模型是线性模型的扩展,它通过连接函数建立响应变量的数学期望值与线性组合的预测变量之间的关系。广义线性模型拟合的形式为:其中g(μY)是条件均值的函数(称为连接函数)。另外,你可放松Y为正态分布的假设,改为Y 服从指数分布族中的一种分布即可。设定好连接函数和概率分布后,便可以通过最大似然估计的多次迭代推导出各参数值。在大部分情况下,线性模型就可以通过一系列连续型或类别型预测变量来预测正态分布的响应变量的工作。但是,有时候我们要进行非正态因变量的分析,例如:(1)类别型.._广义线性回归模型

HTML+CSS大作业 环境网页设计与实现(垃圾分类) web前端开发技术 web课程设计 网页规划与设计_垃圾分类网页设计目标怎么写-程序员宅基地

文章浏览阅读69次。环境保护、 保护地球、 校园环保、垃圾分类、绿色家园、等网站的设计与制作。 总结了一些学生网页制作的经验:一般的网页需要融入以下知识点:div+css布局、浮动、定位、高级css、表格、表单及验证、js轮播图、音频 视频 Flash的应用、ul li、下拉导航栏、鼠标划过效果等知识点,网页的风格主题也很全面:如爱好、风景、校园、美食、动漫、游戏、咖啡、音乐、家乡、电影、名人、商城以及个人主页等主题,学生、新手可参考下方页面的布局和设计和HTML源码(有用点赞△) 一套A+的网_垃圾分类网页设计目标怎么写

C# .Net 发布后,把dll全部放在一个文件夹中,让软件目录更整洁_.net dll 全局目录-程序员宅基地

文章浏览阅读614次,点赞7次,收藏11次。之前找到一个修改 exe 中 DLL地址 的方法, 不太好使,虽然能正确启动, 但无法改变 exe 的工作目录,这就影响了.Net 中很多获取 exe 执行目录来拼接的地址 ( 相对路径 ),比如 wwwroot 和 代码中相对目录还有一些复制到目录的普通文件 等等,它们的地址都会指向原来 exe 的目录, 而不是自定义的 “lib” 目录,根本原因就是没有修改 exe 的工作目录这次来搞一个启动程序,把 .net 的所有东西都放在一个文件夹,在文件夹同级的目录制作一个 exe._.net dll 全局目录

BRIEF特征点描述算法_breif description calculation 特征点-程序员宅基地

文章浏览阅读1.5k次。本文为转载,原博客地址:http://blog.csdn.net/hujingshuang/article/details/46910259简介 BRIEF是2010年的一篇名为《BRIEF:Binary Robust Independent Elementary Features》的文章中提出,BRIEF是对已检测到的特征点进行描述,它是一种二进制编码的描述子,摈弃了利用区域灰度..._breif description calculation 特征点

房屋租赁管理系统的设计和实现,SpringBoot计算机毕业设计论文_基于spring boot的房屋租赁系统论文-程序员宅基地

文章浏览阅读4.1k次,点赞21次,收藏79次。本文是《基于SpringBoot的房屋租赁管理系统》的配套原创说明文档,可以给应届毕业生提供格式撰写参考,也可以给开发类似系统的朋友们提供功能业务设计思路。_基于spring boot的房屋租赁系统论文