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

技术标签: 汇编  编辑器  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

智能推荐

使用nginx解决浏览器跨域问题_nginx不停的xhr-程序员宅基地

文章浏览阅读1k次。通过使用ajax方法跨域请求是浏览器所不允许的,浏览器出于安全考虑是禁止的。警告信息如下:不过jQuery对跨域问题也有解决方案,使用jsonp的方式解决,方法如下:$.ajax({ async:false, url: 'http://www.mysite.com/demo.do', // 跨域URL ty..._nginx不停的xhr

在 Oracle 中配置 extproc 以访问 ST_Geometry-程序员宅基地

文章浏览阅读2k次。关于在 Oracle 中配置 extproc 以访问 ST_Geometry,也就是我们所说的 使用空间SQL 的方法,官方文档链接如下。http://desktop.arcgis.com/zh-cn/arcmap/latest/manage-data/gdbs-in-oracle/configure-oracle-extproc.htm其实简单总结一下,主要就分为以下几个步骤。..._extproc

Linux C++ gbk转为utf-8_linux c++ gbk->utf8-程序员宅基地

文章浏览阅读1.5w次。linux下没有上面的两个函数,需要使用函数 mbstowcs和wcstombsmbstowcs将多字节编码转换为宽字节编码wcstombs将宽字节编码转换为多字节编码这两个函数,转换过程中受到系统编码类型的影响,需要通过设置来设定转换前和转换后的编码类型。通过函数setlocale进行系统编码的设置。linux下输入命名locale -a查看系统支持的编码_linux c++ gbk->utf8

IMP-00009: 导出文件异常结束-程序员宅基地

文章浏览阅读750次。今天准备从生产库向测试库进行数据导入,结果在imp导入的时候遇到“ IMP-00009:导出文件异常结束” 错误,google一下,发现可能有如下原因导致imp的数据太大,没有写buffer和commit两个数据库字符集不同从低版本exp的dmp文件,向高版本imp导出的dmp文件出错传输dmp文件时,文件损坏解决办法:imp时指定..._imp-00009导出文件异常结束

python程序员需要深入掌握的技能_Python用数据说明程序员需要掌握的技能-程序员宅基地

文章浏览阅读143次。当下是一个大数据的时代,各个行业都离不开数据的支持。因此,网络爬虫就应运而生。网络爬虫当下最为火热的是Python,Python开发爬虫相对简单,而且功能库相当完善,力压众多开发语言。本次教程我们爬取前程无忧的招聘信息来分析Python程序员需要掌握那些编程技术。首先在谷歌浏览器打开前程无忧的首页,按F12打开浏览器的开发者工具。浏览器开发者工具是用于捕捉网站的请求信息,通过分析请求信息可以了解请..._初级python程序员能力要求

Spring @Service生成bean名称的规则(当类的名字是以两个或以上的大写字母开头的话,bean的名字会与类名保持一致)_@service beanname-程序员宅基地

文章浏览阅读7.6k次,点赞2次,收藏6次。@Service标注的bean,类名:ABDemoService查看源码后发现,原来是经过一个特殊处理:当类的名字是以两个或以上的大写字母开头的话,bean的名字会与类名保持一致public class AnnotationBeanNameGenerator implements BeanNameGenerator { private static final String C..._@service beanname

随便推点

二叉树的各种创建方法_二叉树的建立-程序员宅基地

文章浏览阅读6.9w次,点赞73次,收藏463次。1.前序创建#include&lt;stdio.h&gt;#include&lt;string.h&gt;#include&lt;stdlib.h&gt;#include&lt;malloc.h&gt;#include&lt;iostream&gt;#include&lt;stack&gt;#include&lt;queue&gt;using namespace std;typed_二叉树的建立

解决asp.net导出excel时中文文件名乱码_asp.net utf8 导出中文字符乱码-程序员宅基地

文章浏览阅读7.1k次。在Asp.net上使用Excel导出功能,如果文件名出现中文,便会以乱码视之。 解决方法: fileName = HttpUtility.UrlEncode(fileName, System.Text.Encoding.UTF8);_asp.net utf8 导出中文字符乱码

笔记-编译原理-实验一-词法分析器设计_对pl/0作以下修改扩充。增加单词-程序员宅基地

文章浏览阅读2.1k次,点赞4次,收藏23次。第一次实验 词法分析实验报告设计思想词法分析的主要任务是根据文法的词汇表以及对应约定的编码进行一定的识别,找出文件中所有的合法的单词,并给出一定的信息作为最后的结果,用于后续语法分析程序的使用;本实验针对 PL/0 语言 的文法、词汇表编写一个词法分析程序,对于每个单词根据词汇表输出: (单词种类, 单词的值) 二元对。词汇表:种别编码单词符号助记符0beginb..._对pl/0作以下修改扩充。增加单词

android adb shell 权限,android adb shell权限被拒绝-程序员宅基地

文章浏览阅读773次。我在使用adb.exe时遇到了麻烦.我想使用与bash相同的adb.exe shell提示符,所以我决定更改默认的bash二进制文件(当然二进制文件是交叉编译的,一切都很完美)更改bash二进制文件遵循以下顺序> adb remount> adb push bash / system / bin /> adb shell> cd / system / bin> chm..._adb shell mv 权限

投影仪-相机标定_相机-投影仪标定-程序员宅基地

文章浏览阅读6.8k次,点赞12次,收藏125次。1. 单目相机标定引言相机标定已经研究多年,标定的算法可以分为基于摄影测量的标定和自标定。其中,应用最为广泛的还是张正友标定法。这是一种简单灵活、高鲁棒性、低成本的相机标定算法。仅需要一台相机和一块平面标定板构建相机标定系统,在标定过程中,相机拍摄多个角度下(至少两个角度,推荐10~20个角度)的标定板图像(相机和标定板都可以移动),即可对相机的内外参数进行标定。下面介绍张氏标定法(以下也这么称呼)的原理。原理相机模型和单应矩阵相机标定,就是对相机的内外参数进行计算的过程,从而得到物体到图像的投影_相机-投影仪标定

Wayland架构、渲染、硬件支持-程序员宅基地

文章浏览阅读2.2k次。文章目录Wayland 架构Wayland 渲染Wayland的 硬件支持简 述: 翻译一篇关于和 wayland 有关的技术文章, 其英文标题为Wayland Architecture .Wayland 架构若是想要更好的理解 Wayland 架构及其与 X (X11 or X Window System) 结构;一种很好的方法是将事件从输入设备就开始跟踪, 查看期间所有的屏幕上出现的变化。这就是我们现在对 X 的理解。 内核是从一个输入设备中获取一个事件,并通过 evdev 输入_wayland

推荐文章

热门文章

相关标签