6.1 函数调用约定简介
函数约定calling conventions,即调用函数的一套约定,它体现在:
- 参数的传递方式,是放在寄存器?栈?还是两者混合?
- 参数的传递顺序,是从左往右?还是从右往左传递?
- 是调用者保护环境,还是被调用者保护?保护那些寄存器呢?
如果我们使用寄存器来传递参数,那么用哪些寄存器来储存参数?参数要保留又要存放在哪?所以我们用栈来存储参数,因为这有两个好处:
- 每个进程都有自己的栈。
- 保存参数的内存地址不用再花费精力维护,已经有栈机制来维护地址变化了。
调用约定的种类繁多,这里我们只介绍C语言的一种调用约定cdecl和stdcall。
stdcall调用约定
- 调用者将所有参数从右向左入栈。
- 被调用者清理参数所占的栈空间。
cdecl调用约定
- 调用者将所有参数从右向左入栈。
- 调用者清理参数所占的栈空间。
cdecl约定最大的亮点就是它允许函数中参数的数量不固定,这在以后动手实现printf函数时会体验这一优势。
6.2 汇编语言和C语言混合编程
汇编语言和C语言混合编程可分为两大类
- 单独的汇编代码文件和单独的C语言文件分别编译成目标文件后,一起链接成可执行文件。
- 在C语言中嵌入汇编代码,直接编译生成可执行程序。
本节所说的“汇编语言和C语言混合编程”属于第1种,第2种的内嵌汇编又称内联汇编,以后会有专门的章节来说。
调用系统调用有两种方式:
- 将系统调用指令封装为c库函数,通过库函数进行系统调用。
- 不依赖任何库函数,直接通过汇编指令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可选项,它表示该变量是不稳定的、不可控的,于是就不会在编译阶段中被缓冲到寄存器里,而是从内存中取数据。