代码、内容参考来自于包括《操作系统真象还原》、《一个64位操作系统的设计与实现》以及《ORANGE’S:一个操作系统的实现》。
1.概述
gcc支持在C代码中直接嵌入汇编代码,所以内联汇编又称为gcc assembly code。
内联汇编按格式分为两大类,一类是最简单的基本内联汇编,另一类是复杂一些的扩展内联汇编,内联汇编使用的语法是汇编语言AT&T。
AT&T是汇编语言的一种语法风格
语法规则:在指令名字后加上了操作数大小后缀,b表示1字节、w表示两字节、l表示4字节。

立即数与地址:
- 在Intel语法中,立即数就是普通数字,如果想用立即数表示地址,需要加上中括号。[立即数]才表示地址。
- AT&T中,数字被优先认为是地址,若想表示立即数,则需要加上$前缀。
AT&T内存寻址
segreg(段基址):base_address(offset_address,index,size)
对应的表达式是segreg(段基址):base_address+offset+indexsize。
相当于Intel32位内存寻址的segreg:[base+indexsize+offset]。
base_address是基地址,可以为整数/变量名,可正可负。
offset_address是偏移地址,index为索引值,这两个必须是8个通用寄存器之一。
size是长度,只能为1、2、3、4。
直接寻址:movl $255,0xc000_08f0或mov $6,var(变量名)。
寄存器间接寻址:寻址中只有offset_address项。如mov (%eax),%ebx。
寄存器相对寻址:只有offset_address和base_address项,格式为base_address(offset_address),这样得出的内存地址为基址+偏移地址之和。如:movb -4(%ebx),%al,意思是将(ebx-4)所指向的内存复制1字节到寄存器al。
变址寻址:称为变址的原因是它有变量index,因为index是size的倍数,所以有index就有size。
一共有 4 种变址寻址组合:
- 无 base_address,无offset_address:movl %eax,(,%esi,2),功能是将eax的值写入esi*2所指向的内存。
- 无 base_address,有 offset_address:movl %eax,(%ebx,%esi,2),功能是将eax的值写入ebx+esi*2所指向的内存。
- 有 base_address,无 offset_address:movl %eax,base_value(,%esi,2),功能是将eax的值写入base_value+esi*2所指向的内存。
- 有 base_address,有 offset_address:movl %eax,base_value(%ebx,%esi,2),功能是将 eax 的值写入 base_value+ebx+esi*2 所指向的内存。
2.基本内联汇编
基本内联汇编格式:
asm [volatile] ("assembly code")各关键字之间可以用空格/制表符分隔,也可以紧凑。
各部分意义如下:
asm和__asm__是一样的,是由gcc定义的宏:#define _asm _asm。volatile和__volatile__是一样的,是由gcc定义的宏:#define _volatile _volatile。
gcc -o可以指定优化级别,volatile表示原样保留汇编代码。
内联汇编是我们写的assembly code,它必须在圆括号中,用双引号引起来,可以为空。
assembly code规则:
- 指令必须由双引号引起来,无论双引号中是一条/多条指令。
- 一对双引号不能跨行,如果跨行需要在结尾用’’转义。
- 指令之间用分号、换行符或换行符+制表符分隔:‘;’ ‘\n’ ‘\n’‘\t’。
即使指令分布在多个双引号中,gcc最终也要将它们合并到一起处理。合并后,指令间必须有分隔符,所以除最后一个双引号外,其余双引号结尾处必须有分隔符。
正确写法
asm ("mov $9,%eax;""pushl %eax")错误写法
asm ("mov $9,%eax""pushl %eax")操作数的顺序也要注意,例如:
char* str="hello,world\n";
int count=0;
void main(){
asm("pusha; \
movl $4,%eax; \
movl $1,%ebx; \
movl str,%ecx; \
movl $12,%edx; \
int $0x80 \
mov %eax,count; \
popa \
");
}第1~2行定义了两个全局变量,待打印的字符串是str,count用来存储返回值。
第4~12行是内联汇编,这是咱们之前说过的C语言中跨过运行库直接调用系统调用的实例。这完全是AT&T风格的汇编语句:寄存器前面加前缀%,立即数前面加前缀S,操作数由左到右的顺序。
第4行将8个通用寄存器压栈,AT&T中的汇编指令是pusha (Intel中的是pushad)。第5行传入第4号系统调用,这就是 write的调用号。
第6~8行是为write系统调用传入参数,前面说系统调用的时候有讲过参数传递所用到的寄存器,不再赘述。
第9行用int 0x80执行系统调用,在AT&T中立即数的地位比较低,要加S前缀才表示数字为立即数(常数)。
第10行是获取write的返回值,返回值都是存储在eax寄存器中,所以将其复制到变量count中。
3.扩展内联汇编
内联汇编格式
asm [volatile] ("assembly code":output:intput:clobber/modify)和前面的基本内联汇编相比,扩展内联汇编在圆括号中变成了4部分,多了output、input和 clobber/modify三项。其中的每一部分都可以省略,甚至包括assembly code。省略的部分要保留冒号分隔,四部分都可以省略,省略部分要保留分隔符。如果省略的是后面的一/多个部分,则分隔符不用保留。
assembly code:汇编指令。
内联汇编的目的是让汇编帮助C完成某些功能,所以C代码要为其提供参数和用于存放输出结果的空间。内联汇编类似机器,C代码类似人,人要为机器提供原材料input,机器运行后,将产出放入到output中。
output 用来指定汇编代码的数据如何输出给 C 代码使用。output中每个操作数的格式为:“操作数修饰符约束名”(C变量名)。引号和圆括号不能省略,操作数修饰符为可选项,多个操作数之间使用逗号分隔。
input 用来指定 C 中数据如何输入给汇编使用。input中每个操作数的格式为:“[操作数修饰符] 约束名”(C变量名)。引号和圆括号不能省略,操作数修饰符通常为等号,多个操作数之间使用逗号分隔。
以上的output()和 input()括号中的是 C 代码中的变量,output (c变量)和 input (c变量)就像C语言中的函数,将C变量(值或变量地址)转换成汇编代码的操作数。
clobber/modify:汇编代码执行后会破坏一些内存/寄存器资源,通过此项通知编译器,让gcc把它们保护起来。
约束
作用:将C代码中的操作数(变量、立即数)映射为汇编中所使用的操作数。
作用域:input和output。
寄存器约束
寄存器约束就是要求gcc使用哪个寄存器,将input或output中变量约束在某个寄存器中。
常见寄存器:
- a:表示寄存器eax/ax/al
- b: 表示寄存器 ebx/bx/bl
- c:表示寄存器 ecx/cx/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 个浮点寄存器
比较一下基本内联汇编和扩展内联汇编的区别
//加法操作
//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);
}加法指令的两个输入操作数是in_a 和in_b,输出和存储在变量out_sum中。在第5~6 行输入操作数 in_a 和 in_b 是分别手动movl到寄存器eax和ebx的。加法的和是在第8行movl到变量out_sum中的。
在基本内联汇编中的寄存器用单个%做前缀,在扩展内联汇编中,单个%用来表示占位符,所以在扩展内联汇编中的寄存器前面用两个%做前缀。
output和input中用的约束都是同一个,肯定永远是输入(input)中的汇编操作数优先被赋值,汇编代码经过运行,最后才是为输出(output)中的汇编操作数赋值,output和input中的约束都有a,也就是都用寄存器eax来导入导出数值。 肯定是eax先在input部分中被赋值为变量in_a,此时eax作为输入参数第一次被赋值,在加法指令addl运行后直接把结果放到寄存器eax中,此时eax作为输出结果第二次被赋值,这样eax直接就是最终的输出啦。 之后再处理out_put,由于eax已经是汇编中用于输出的操作数了,编译器背后通过mov操作把eax的值传给out_sum。
内存约束
内存约束是要求gcc直接将位于input和output中的C变量的内存地址作为内联汇编代码的操作数,不需要寄存器做中转,直接进行内存读写,也就是汇编代码的操作数是 C变量的指针。
m:表示操作数可以使用任意一种内存形式。
o:操作数为内存变量,但访问它是通过偏移量的形式访问,即包含offset_address的格式。
下面的文件mem.c用约束m为例:
#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);
}把in_a施加寄存器约束a,告诉gcc把变量in_a放到寄存器eax中,对in_b施加内存约束m,告诉gcc把变量in_b的指针作为内联代码的操作数。
立即数约束
立即数就是常数,此约束要求gcc在传值的时候不通过内存和寄存器,直接作为立即数传给汇编代码。
- i:表示操作数为整数立即数
- F: 表示操作数为浮点数立即数
- I:表示操作数为0~31之间的立即数
- J:表示操作数为0~63之间的立即数
- N:表示操作数为0~255之间的立即数
- O:表示操作数为0~32之间的立即数
- X:表示操作数为任何类型立即数
通用约束
此约束只用在input部分,但表示可与output和input中的第n个操作数用相同的寄存器/内存。
总结:由于是在C代码中插入汇编,所以约束的作用是让C代码的操作数变成汇编代码能使用的操作数。在内联汇编中用到的操作数,都是位于input和output中C操作数的副本,多数通过赋值的方式传给汇编代码,或通过指针的方式,当操作数的副本在汇编中处理完后,又重新赋值给C操作数。
占位符
作用:代表约束指定的操作数(寄存器、内存、立即数)
分类:
- 序号占位符
- 名称占位符
序号占位符
序号占位符是对在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
}
名称占位符
名称占位符需要在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中:

在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");
4.扩展内联汇编之机器模式
机器模式是用来在机器层面上指定数据的大小和格式的。
我们需要了解的几个操作码

5.参考
郑钢著操作系统真象还原
田宇著一个64位操作系统的设计与实现
丁渊著ORANGE’S:一个操作系统的实现


