《操作系统真象还原》第6章 完善内核

6.1  函数调用约定简介

        函数约定calling conventions,即调用函数的一套约定,它体现在:

  • 参数的传递方式,是放在寄存器?栈?还是两者混合?
  • 参数的传递顺序,是从左往右?还是从右往左传递?
  • 是调用者保护环境,还是被调用者保护?保护那些寄存器呢?

        如果我们使用寄存器来传递参数,那么用哪些寄存器来储存参数?参数要保留又要存放在哪?所以我们用栈来存储参数,因为这有两个好处:

  1. 每个进程都有自己的栈。
  2. 保存参数的内存地址不用再花费精力维护,已经有栈机制来维护地址变化了。

        调用约定的种类繁多,这里我们只介绍C语言的一种调用约定cdecl和stdcall。

stdcall调用约定

  1. 调用者将所有参数从右向左入栈。
  2. 被调用者清理参数所占的栈空间。

cdecl调用约定

  1. 调用者将所有参数从右向左入栈。
  2. 调用者清理参数所占的栈空间。

        cdecl约定最大的亮点就是它允许函数中参数的数量不固定,这在以后动手实现printf函数时会体验这一优势。

6.2  汇编语言和C语言混合编程

        汇编语言和C语言混合编程可分为两大类

  1. 单独的汇编代码文件和单独的C语言文件分别编译成目标文件后,一起链接成可执行文件。
  2. 在C语言中嵌入汇编代码,直接编译生成可执行程序。

        本节所说的“汇编语言和C语言混合编程”属于第1种,第2种的内嵌汇编又称内联汇编,以后会有专门的章节来说。

        调用系统调用有两种方式:

  1. 将系统调用指令封装为c库函数,通过库函数进行系统调用。
  2. 不依赖任何库函数,直接通过汇编指令int与操作系统通信。

        这里重点提一下,当输入的参数小于等于5个时,linux用寄存器传递参数。好啦,具体例子看书p258页。

        

        

        C语言和和汇编语言可以相互调用,实际上是转化为机器码后才能实现。

        在混合编程中:

  • 导出符号供外部引用是用的关键字global,引用外部文件的符号使用的关键字extern。
  • C代码中只要将符号定义为全局便可以被外部引用,引用外部符号时用extern声明即可。

6.3  实现自己的打印函数

前提知识

        显卡的寄存器如上,上图起始是寄存器的目录,计算机工程师把每一个寄存器分组视为一个寄存器数组,提供一个寄存器Address Register用于指定数组下标,再提供一个寄存器Date Register用于对索引指向的数组元素(寄存器)进行输入输出操作。所以操作方法是现是先在Address Register中指定寄存器的索引值,用来确定所操作的寄存器是哪一个,然后再Date Register寄存器中队所索引的寄存器进行读写操作。

        

实现单个字符打印

        这个功能类似C语言中的 putchar,所以暂且将其命名为put_char。在此之前,为了开发方便,我们定义一些数据类型(lib/stdint.h)。

        lib/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

        这里新建了一个lib文件夹,专门存放各种库文件。不仅如此,在lib目录下还建立了user和kernel两个子目录,以后拱内核使用的库文件就放在lib/kernel下,lib/user中是用户进程使用的库文件。

        我们要实现的字符打印函数叫put_char,使用汇编语言写的,将放在print.S中完成,该文件也是各种打印函数的核心。下面是它的处理流程:

1)备份寄存器现场。

2)获取光标坐标值,光标坐标值是下一个可打印字符的位置。 

3)获取待打印的字符。

4)判断字符是否为控制字符,若是回车符、换行符、退格符三种控制字符之一,则进入相应的处理流程,否则认为是可见字符,进入输出流程处理。

5)判断是否需要滚屏。

6)更新光标坐标值,使其指向下一个打印字符的位置。

7)恢复寄存器现场,退出。

代码实现

        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:
    pushad        ;备份32位寄存器环境,此指令的作用是把通用寄存器压栈,这里完成了第一步:备份寄存器现场
                ;需要保证gs中位正确的视频段选择子,因此每次打印时都位gs赋值
    mov ax,SELECTOR_VIDEO        ;不能直接把立即数送入段寄存器
    mov gs,ax

                ;第二步获取光标坐标值,光标坐标值是下一个可打印字符的位置
                ;先获得高8位

    mov dx,0x03d4            ;CRT Controller Registers寄存器的Address Register
    mov al,0x0e               ;索引值为0x0e 此索引指向光标的高8位
    out dx,al      
    mov dx,0x03d5             ;CRT Controller Registers寄存器的Data Register
    in al,dx                  ;我们从此端口获取数据
    mov ah,al                 ;由于获取的是高8位,所以给ah
                
                ;再获取低8位
    mov dx,0x03d4
    mov al,0x0f                ;索引值为0x0f 此索引指向光标坐标值的低8位
    out dx,al      
    mov dx,0x03d5
    in al,dx                   ;此时ax就是光标坐标值
    mov bx,ax                  ;将光标坐标值存入bx

    mov ecx,[esp + 36]         ;获取栈中要打印字节,这是1字节的数据,栈中除了调用put_char函数的返回地址占4字节外,还有pushad指令亚茹的8个32位的通用寄存器,所以偏移36字节的位置

                ;第四步:判断回车符CR、换行符LF和退格键backspace
    cmp cl,0xd                 ;回车符的ASCII码是0xd
    jz .is_carriage_return

    cmp cl,0xa                ;判断是否是LF(换行)0x0a
    jz .is_line_feed

    cmp cl,0x8				   ;判断是否是BS(backspace退格)的ascII码0x8
    jz .is_backspace

    jmp .put_other	           ;否则一律认为是可见字符

                ;退格符的处理
.is_backspace:	               ;说明,退格符本质上只要将光标向前移一个显存位置就可以了,下一个打印字符会自动覆盖此处的字符,但为了避免没有下一个打印字符,所以此处添加了空格
    
    dec bx                     ;光标位置-1,符合我们的认知退格键光标位置向前进一
    shl bx,1                  ;光标位置转换成对应字符的显存位置的便宜
    mov byte [gs:bx], 0x20	   ;将待删除的字节补为空格, 0x20是空格符的ASCII码值 
    inc bx                     ;bx+1, 指向这个字符的属性位置, 也就是设定背景色, 字符颜色
    mov byte [gs:bx], 0x07     ;0x07, 就是黑底白字
    shr bx,1                   ;bx虽然指向这个字符的颜色属性字节,但是除以2还是变回这个字符的光标位置,余数不要了
    jmp .set_cursor      

                ;可见字符的处理
.put_other:
    shl bx,1                   ;转换为显存的地址偏移
    mov [gs:bx], cl			   ; ascii字符本身
    inc bx                     ;bx+1,指向字符属性
    mov byte [gs:bx],0x07	   ; 字符属性
    shr bx, 1				   ; 恢复为光标值
    inc bx				       ; 下一个光标值
    cmp bx, 2000		       ;80*25模式下的屏幕可显示的字符数是2000,若超过2000说明要滚屏了
    jl .set_cursor	
    
.is_line_feed:				   ; 是换行符LF(\n)
.is_carriage_return:		   ; 是回车符CR(\r)
					           ; 如果是CR(\r),只要把光标移到行首就行了。
    xor dx, dx				   ;要进行16位除法,高16位置会放在dx中,要先清零
    mov ax, bx				   ;ax是被除数的低16位.
    mov si, 80				   ;用si寄存器来存储除数80 由于是效仿linux,linux中\n便表示下一行的行首,所以本系统中,
    div si				       ; 把\n和\r都处理为linux中\n的意思,也就是下一行的行首。ax/80后,ax中存商,dx中存储的是余数,汇编除法
    sub bx, dx				   ; 光标值减去除80的余数便是取整
					           ; 以上4行处理\r的代码,此时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                                                     
    mov ecx, 960			   ; 一共有2000-80=1920个字符要搬运,共1920*2=3840字节.一次搬4字节,共3840/4=960次 
    mov esi, 0xb80a0		   ; 第1行行首
    mov edi, 0xb8000		   ; 第0行行首
    rep movsd				   ;rep movs word ptr es:[edi], word ptr ds:[esi] 简写为: rep movsw

                               ;将最后一行填充为空白
    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值
                ;先设置高8位
    mov dx, 0x03d4			   ;索引寄存器
    mov al, 0x0e			   ;用于提供光标位置的高8位
    out dx, al
    mov dx, 0x03d5			   ;通过读写数据端口0x3d5来获得或设置光标位置 
    mov al, bh
    out dx, al

                ;再设置低8位
    mov dx, 0x03d4
    mov al, 0x0f
    out dx, al
    mov dx, 0x03d5 
    mov al, bl
    out dx, al
.put_char_done: 
    popad
    ret








        为了方便其他文件调用put_char函数,我们建立一个头文件,以后文件调用把头文件加进去即可。

     lib/kernel/print.h
#ifndef __LIB_KERNEL_PRINT_H
#define __LIB_KERNEL_PRINT_H
#include "stdint.h"    
void put_char(uint8_t char_asci)        
#endif

        接下来验证我们所写的函数是否可以正常调用

      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-4.4 -m32 -I lib/kernel/ -c -o kernel/main.o kernel/main.c
        链接 main.o和 print.o
ld -o kernel/kernel.bin -m elf_i386 -Ttext 0xc0001500 -e main kernel/main.o lib/kernel/print.o
         写入磁盘
dd if=kernel/kernel.bin of=/home/moyao/Desktop/bochs/hd60M.img bs=512 count=200 conv=notrunc seek=9

实现字符串打印

        前提知识

        我们在C语言中定义的字符串,C编辑器会把字符串的结尾自动加上'\0',用它作为字符串的结束标记,以此来确定字符串的长度,'\0'的ASCII码是0.

代码实现

        OS/lib/kernel/print.S

[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]         ;从栈中获取待打印的字符串地址 函数返回地址4字节,两个32位寄存器8字节,一共12字节。
                                ;编译器将字符串作为参数时,传递的是字符串所在的内存起始地址,也就是说压入栈中的是存储该字符串的内存首地址
.goon:
    mov cl,[ebx]                ;将单个字符传递给ecx
    cmp cl,0                    ;判断是否为'\0',如果是则函数结束
    jz .str_over
    push ecx                    ;把ecx压入栈中,为put_char 函数传递参数   
    call put_char               
    add esp,4                   ;回收参数所占的栈空间
    inc ebx                     ;指向字符串的下一个字符
    jmp .goon
.str_over:
    pop ecx
    pop ebx
    ret

        具体代码详解看书p276

        OS/lib/kernel/print.h

        在头文件中添加下行声明代码

void put_str(char* messags);

        

        OS/kernel/main.c

#include"print.h"
void main(void) {
    put_str("I am kernel\n");
    while(1);
}

     

编译print.S

nasm -f elf -o lib/kernel/print.o lib/kernel/print.S

编译main.c

gcc-4.4 -m32 -I lib/kernel/ -c -o kernel/main.o kernel/main.c

链接 main.o和 print.o

ld -o kernel/kernel.bin -m elf_i386 -Ttext 0xc0001500 -e main kernel/main.o lib/kernel/print.o

写入磁盘

dd if=kernel/kernel.bin of=/home/moyao/Desktop/bochs/hd60M.img bs=512 count=200 conv=notrunc seek=9

实现整数打印

        前提知识

        用于打印整数的函数名为put_int,它的原理是将数字转换成对应的字符串,比如数字9变成字符'9'。所以这里依然是封装put_char。

        代码实现

lib/kernel/print.S

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

;----------将小段字节序的数字变成对应的ASCII后,倒置-------
;输入:栈中参数位待打印的数字
;输出:在屏幕上打印十六进制数字,并不会打印前缀0x
;如打印十进制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位数字中,十六进制数字的位数是8
    mov ebx,put_int_buffer

;将32位数字按照十六进制的形式从低位到高位逐个处理
;因为是32位的数字,4位一个十六进制,所以共处理8个十六进制数字

.16based_4bits:
;遍历每一个十六进制数字
    and edx,0x0000000F        ;解析16进制数字,这里是32位数字的第四位
;由于我们是数字转换为字符,因为数字与字母的ASCII码并不连续,所以这里要区分0~9和A~F
    cmp edx,9
    jg .is_A2F                ;如果edx大于9,跳转A to F函数处理
    add edx,'0'               ;edx加上'0'等于对应字符
    jmp .store

.is_A2F:
    sub edx,10                ;因为edx至少为10以上,这里只需要A以上的偏移值来到达对应的字符
    add edx,'A'
    
;将每个十六进制数字转换为字符后,类似大端的顺序存储到缓冲区put_int_buffer
.store:
    mov [ebx+edi],dl          ;dl是edx寄存器的第八位,所以dl就是数字对应的字符的ascii码
    dec edi                   ;第一次,edi=7,此时指向寄存器最后一个字节
    shr eax,4                 ;32位数字向右移四位,此时可以继续访问第四位,前面我们定义了ecx=8,所以这里会持续取值直到32位全部被取完
    mov edx,eax
    loop .16based_4bits

;此时put_int_buffer中全是字符
;但在打印之前,我们想把多余的0去掉,比如说0x00000123,我们只想打印123,或者全0只打印一个0
.ready_to_print:
    inc edi                   ;此时edi被减至-1,加1使其为0
.skip_prefix_0:                                             ;跳过前缀的连续多个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                                                ;当edi=8时,虽然不会去打印,但是实际上已经越界访问缓冲区了
   jl .put_each_num
   popad
   ret











    

lib/kernel/print.h

void put_int(uint32_t num);	 

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);
}

编译print.S

nasm -f elf -o lib/kernel/print.o lib/kernel/print.S

编译main.c

gcc-4.4 -m32 -I lib/kernel/ -c -o kernel/main.o kernel/main.c

链接main.o和print.o

ld -o kernel/kernel.bin -m elf_i386 -Ttext 0xc0001500 -e main kernel/main.o lib/kernel/print.o

写入磁盘

dd if=kernel/kernel.bin of=/home/moyao/Desktop/bochs/hd60M.img bs=512 count=200 conv=notrunc seek=9

6.4  内联汇编

        本节讲学习在C代码中直接嵌入汇编代码,这种操作被称之为内联汇编。

AT&T语法简介

        GCC支持在C代码中直接嵌入汇编代码,但内联汇编使用的汇编语言,其语法是AT&T,所以我们还得先了解一下AT&T。

        AT&T在指令名字后面加上了操作数大小后缀,b表示1字节,w表示2字节,l表示4字节。例如:push 和 pushl。

         这两种语法在内存寻址方面存在较大的差异。在Intel语法中,立即数就是普通的数字,如果让立即数成为内存地址,需要使用中括号括起来,“[立即数]”;而在AT&T语法中,数字被认定为内存地址,立即数前要使用$表示,即$立即数

        

        具体看书p282。

基本内联汇编 

        基本内联汇编是最简单的内联形式,其格式为:

        asm [volatile] ("assembly code")

        asm用于声明内联汇编表达式,这是内联汇编固定的部分,不可少。asm和_asm_是一样的,是由gcc定义的宏:define _asm_ asm

        因为gcc有个优化选项-O,可以指定优化级别,优化说不定会把自己所写的代码修改了。所以关键字volatile是可选项,表示gcc不要修改我的汇编代码,volatile和_volatile_一样,是由宏定义的:#define _volatile_ volatile

        "assembly code"是我们所写的汇编代码,他必须位于圆括号中,且必须用双引号引起来。这是格式要求,只要满足这个格式要求asm [volatile] (" "),assembly code甚至可以为空。

        

        

扩展内联汇编

        

        内联汇编必然会面对两个问题:

  • 在内联汇编代码插入点之前的C代码,其编译后也要被分配寄存器等资源,插入的汇编代码也要使用寄存器,这是否会造成资源冲突?
  • 汇编语言如何访问C代码中的变量? 

        由于我们不可能让用户来保证数据完整性,即用户在内联汇编中将要是用的寄存器压入栈中(这里还有压入栈中使得效率降低的考虑)。编译器提供了一套模板,让用户在模板中提出要求。所以内联汇编的格式如下:

        asm [volatile] ("assembly code": output : input: clobber/modify)

        其中的每一个部分都可以省略,但要保留冒号分隔符来占位若省略的是后面的一个或者多个连续的部分,则分隔符也可以不用保留,例:input和clobber/modifu都要省略,则不需要保留output后面的冒号。

        打个比方,人要为机器提供加工的原材料(input),机器运行后,将生产出来的成果放在人能够得着的地方(output),人才能获取机器的输出结果。input和output就是C为汇编提供输入参数和存储其输出的部分,这是汇编与c交互的关键

        output: output 用来指定汇编代码的数据如何输出给 C 代码使用。内嵌的汇编指令运行结束后,如果想将运行结果存储到 c 变量中,就用此项指定输出的位置。output 中每个操作数的格式为:
                “操作数修饰符约束名” (C变量名)
其中的引号和圆括号不能少,操作数修饰符通常为等号'=' 。多个操作数之间用逗号’,’分隔。
        input: input 用来指定 C 中数据如何输入给汇编使用。要想让汇编使用 C 中的变量作为参数,就要在此指定。input 中每个操作数的格式为:
                “[操作数修饰符]约束名”( C 变量名)
 其中的引号和圆括号不能少,操作数修饰符为可选项。多个操作数之间用逗号’,’分隔。单独强调一下,以上的 output()和 input()括号中的是 C 代码中的变量, output(c 变量)和 input(c 变量)就像 C 语言 中的函数,将 C 变量(值或变量地址)转换成汇编代码的操作数。

        总之,output用于指定汇编代码的数据如何输出给C代码使用,input用于指定C中数据如何给汇编使用。

        clobber/modify:汇编代码执行后会破坏一些内存或者寄存器资源,通过此项通知编辑器,可能造成寄存器或内存数据的破坏,后面会详细讲。

        接下来我们讲讲input和outpu中的约束。

寄存器约束

        寄存器约束是要求gcc使用哪个寄存器,将i南普陀货output中变量约束在某个寄存器。常见的寄存器约束有:

        a :表示寄存器 eax/ax/al
        b:表示寄存器 ebx/bx/bl
        c :表示寄存器 ecx/ex/cl
        d :表示寄存器 edx/dx/dl

        D :表示寄存器 edi/di
        S :表示寄存器 esi/si
        q:表示任意这 4 个通用寄存器之一: eax/ebx/ecx/edx
        r :表示任意这 6 个通用寄存器之一:eax/ebx/ecx/edx/esi/edi
        g :表示可以存放到任意地点(寄存器和内存)。相当于除了同 q 一样外,还可以让 gcc 安排在内存中
        A :把 eax和 edx 组合成 64 位整数
        f表示浮点寄存器
        t :表示第 1个浮点寄存器
        u :表示第 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));//output->out_sum,且放在eax中 input->in_a,in_b 且放入eax和ebx中
    printf("sum is %d\n",out_sum);

}

        注意扩展内联汇编中寄存器前缀是两个%。用约束名a为C变量in_a指定了eax,同理为in_b指定了ebx,并且把寄存器eax的值存储到C变量 out_sum中。这里要理解顺序,肯定是先input后output,这样eax的值就不会被搞混。 

内存约束

        内存约束要求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));
    printf("in_b is %d\n", in_b);

}

        %其实是序号占位符,%b0这是因为只需要低8位所以使用b表示1字节,在汇编代码中,会将b压入栈中,然后直接使用al寄存器movb到in_b的内存空间(栈中)。注意,movl指令是不允许“内存”到“内存”的复制,即不允许asm(" ": :"m"( ),"m"( ))的出现。

立即数约束

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

i:     表示操作数为整数立即数
F:   表示操作数为浮点数立即数
I:    表示操作数为 0~31 之间的立即数
J:   表示操作数为 0~63 之间的立即数
N:  表示操作数为 0~255 之间的立即数
O:  表示操作数为 0~32 之间的立即数
X:  表示操作数为任何类型立即数

通用约束

        0~9:  此约束只用在input部分,但表示可与output和input中低n个操作数用相同的寄存器或内存。

        这里在强调一下约束,由于我们在C语言中插入汇编代码,所以约束的作用是让C代码的操作数可以在汇编代码中使用,所有的约束都是给汇编用的,约束高数编译器,同一种操作数在两种环境下如何变换身份,如何对接沟通。在编译过程中C代码是要先变成汇编代码的,约束相当于指定C中数据的编译形式。

        扩展内联汇编提供了占位符,它的作用是代表约束指定的操作数。占位符分序号占位符和名称占位符两种。

        序号占位符 

        序号占位符是对在 output和input中的操作数,按照他们从左到右出现的次序从0开始编号,最多支持10个序号占位符。

        操作数用在 assembbly code 中,引用它的格式是%0~9。

        

        由于扩展内联汇编中的占位符要有前缀%,为了区别占位符和寄存器,只好在寄存器前用两个%%作为前缀符。

        我们还可以使在%和序号之间插入字符‘h’来表示操作数为ah(第8~15位),而在默认情况或者插入字符'b'的情况下,表示al(第0~7位)。

        具体例子可看书p289

名称占位符

        名称占位符是在output和input中把操作数显式地起个名字,它的格式如下:

        [名称]"约束名" (C变量) ,在assembly code中引用操作数时,采用%[名称]的形式使用

#include<stdio.h>
void main()
{
    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);

}

         

        clobber/modify通知我们修改了哪些寄存器,这是因为在assembly code部分我们可能调用了一些函数,这些函数内部会修改一些资源,所以我们在clobber/modify部分明确写出调用的寄存器,用双引号把寄存器名称引起来,多个寄存器之间要用都好分隔,这里的寄存器不需要加两个%%,只写名称即可。

         

        这里注意要讲的是为什么我们需要主动声明,这是因为进程在运算时,为了效率,在编译中会将数据缓存到CPU的寄存器中。所以若没有声明,可能会出现数据被改变了但寄存器中还是旧数据的情况。

        于是gcc为我们提供了volatile可选项,它表示该变量是不稳定的、不可控的,于是就不会在编译阶段中被缓冲到寄存器里,而是从内存中取数据。

         

         

         

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值