ucore用到的AT&T格式的汇编与Intel格式的汇编在语法层面的不同
* 寄存器命名原则
AT&T:%eax Intel:eax
* 源/目的操作数顺序
AT&T:movl %eax, %ebx Intel:mov ebx, eax
AT&T:movl $0xff,%ebx Intel:mov ebx,0ffh
注:AT&T中表示16进制数用0x
* $表示一个常数/立即数
AT&T:movl $_value, %ebx Intel:mov ebx, _value
para = 0x04
movl $para, %ebx 结果是将立即数0x04装入寄存器%ebx
例:把地址放入ebx寄存器
AT&T:movl $0xd00d, %ebx Intel:mov ebx, 0xd00d
* 操作数长度标识,操作数的字长由操作符的最后一个字母决定 b,w,l
AT&T:movw %ax, %bx Intel:mov bx,ax
* 远程转移指令和远程子调用指令的操作码,在 AT&T 汇编格式中为 "ljmp" 和 "lcall"
* 寻址方式
AT&T: immed32(basepointer, indexpointer, indexscale)
=imm32 + basepointer + indexpointer x indexscale
Intel: [basepointer + indexpointer x indexscale + imm32]
在ucore中,由于 Linux 工作在保护模式下,用的是 32 位线性地址,所以在计算地址时不用考虑段基址和偏移量,即
immed32(basepointer, indexpointer, indexscale)
=imm32 + basepointer + indexpointer x indexscale
例子
* 直接寻址
AT&T:_foo Intel:[_foo]
_foo是一个全局变量,表示一个立即数,加上$,$_foo引用_foo本身的值
不加$,_foo的值作为一个地址对内存进行寻址,并引用相关地址上存储着的值
* 寄存器间接寻址
AT&T:(%eax) Intel:[eax]
* 变址寻址
AT&T:_variable(%eax) Intel:[eax + _variable]
AT&T:_array( ,%eax,4) Intel:[eax x 4 + _array]
AT&T:_array(%ebx,%eax,8) Intel:[ebx + eax x 8 + _array]
GCC基本内联汇编
格式:
asm("statements");
例:
asm("nop");
asm("cli");
"asm"和"__asm__"的含义一样
多行汇编时,每一行都加上"\n\t"(换行符和tab符),是为了让gcc把内联汇编代码翻译成一般的汇编代码时保证换行和留有一定的空格,因为gcc处理汇编时,要把asm(…)中的内容"打印"到汇编文件中,所以格式控制符是必要的
对于基本asm语句,GCC编译出来的就是双引号中的内容,如:
asm( "pushl %eax\n\t"
"movl $0,%eax\n\t"
"popl %eax"
);
下例中,我们在内联汇编中改变了edx和ebx的值,但gcc的处理方法特殊:先形成汇编文件,再交给汇编器GAS(GNU ASM,功能是将汇编代码转换成二进制形式的目标代码)去汇编,即编译器实际上把asm括号中双引号引起来的语句逐字的插入到编译后产生的.s
汇编文件中,并进行了相应的替换操作
所以GAS并不知道我们已经改变了edx和ebx的值,如果程序的上下文需要用到edx和ebx来暂存数据(程序的上下文是c语言,不能保证不会用到这两个寄存器),就会产生无法预料的多次赋值,会出现错误,变量_boo也有这样的问题,这就需要扩展GCC内联汇编
asm( "movl %eax, %ebx");
asm( "xorl %ebx, %edx");
asm( "movl $0, _boo");
GCC扩展内联汇编
基本格式,共四部分
asm [volatile] ( Assembler Template
: Output Operands
: Input Operands
: Clobbers );
汇编代码:内联汇编代码,语法同基本asm格式。变量用占位符表示。
输出位置:汇编代码中输出值所在的寄存器和内存单元列表。(执行汇编代码之后)
输入操作符:汇编代码中输入值所在的寄存器和内存单元的列表。(执行汇编代码前处理)
改变的寄存器:内联汇编代码中改变的寄存器(用于提醒编译器对这些寄存器进行保护操作)
举例:
#define read_cr0() ({
unsigned int __dummy;
__asm__(
"movl %%cr0,%0\n\t"
:"=r" (__dummy));
__dummy;
})
__asm__表示汇编代码的开始,其后可以跟__volatile__(这是可选项)其含义是避免“asm”指令被删除、移动或组合,指向代码时,如果不希望汇编语句被gcc优化而改变位置。就添加该关键词,如下
asm volatile(…)
或者更详细的
__asm__ __volatile__(…)
(……)中的部分就是具体的内联汇编指令代码,"……"中的为汇编指令部分
汇编指令中数字前加前缀%(称为占位符),如%1,%2,用来表示输出输入列表中的变量,表示使用寄存器的样板操作数,可以使用的操作数总数取决于具体CPU中通用寄存器的数量,如Intel有8个,指令中有几个操作数,说明有几个变量需要与寄存器结合,gcc编译时根据后面输出部分和输入部分的约束条件进行相应处理,由于样板操作数的前缀使用了%,所以在用到具体寄存器时前面加两个%,如%%cr0
占位符%n的用法:%0 %1 %2表示从输出部分开始出现的第1、2、3个变量
举例:
#include <stdio.h>
int main(void){
int xa=6;
int xb=2;
int result;
// 使用占位符,由r表示,编译器自主选择使用哪些寄存器,%0,%1。。。表示第1、2。。。个变量
// %0对应变量result
// %1对应变量xa
// %2对应变量xb
asm volatile(
"add %1,%2\n\t"
"movl %2,%0"
:"=r"(result)
:"r"(xa),"r"(xb)
);
printf("%d\n",result);
return 0;
}
下图中%0对应变量_out,变量_out指定的寄存器为%eax,所以编译器会将占位符替换为%eax
引用占位符
#include <stdio.h>
int main(void){
int xa=6;
int xb=2;
asm volatile(
"add %1,%0\n\t"
:"=r"(xb)
:"r"(xa),"0"(xb) //"0"(xb)为引用占位符,表示使用第一个命令的寄存器存放xb输出值
);
printf("%d\n",xb);
return 0;
}
输出部分用来规定对输出变量(目标操作数)如何与寄存器结合的约束,可以有多个约束,以逗号分开,每个约束以"="开头,接着一个字母表示操作数的类型,然后时变量结合的约束,如
"=r"表示相应的目标操作数可以使用任何一个通用寄存器
:"=r" (__dummy) 表示变量__dummy可以使用任何一个通用寄存器
如果是
"=m"表示相应的目标操作数是存放在内存单元中
:"=m"(__dummy) 表示变量__dummy使用内存单元来保存值
各个约束条件字母及其含义
字母 | |
---|---|
m,v,o | 内存单元 |
R | 任何通用寄存器 |
Q | 寄存器eax,ebx,ecx,edx之一 |
l,h | 直接操作数 |
E,F | 浮点数 |
G | 任意 |
a,b,c,d | 寄存器eax/ax/al,ebx/bx/bl,ecx/cx/cl,edx/dx/dl |
S,D | 寄存器esi或edi |
I | 常数(0~31) |
输入部分与输出部分相似,但没有"=",如果输入部分一个操作数所要求使用的寄存器与前面输出部分某个约束所要求的是同一个寄存器,就把对应操作数的编号(如"1",“2”)放在约束条件中
修改部分(也称乱码列表)如果想通知gcc当前内嵌汇编语句可能会对某些寄存器或内存进行修改,希望gcc在编译时能够考虑到,那么我们就在修改部分声明这些寄存器或内存
若要修改内存,用"memory"声明,表示操作完成后内存中的内容已有改变,如果原来某个寄存器的内容来自内存,那么现在内存中这个单元的内容已经改变,乱码列表通知编译器,有些寄存器或内存因内联汇编块造成乱码,可隐式地破坏了条件寄存器的某些位
若要修改寄存器,只需要将寄存器的名字用双引号括起来就可以了;如果要声明多个寄存器,则相邻两个寄存器名字之间用逗号隔开
另外,因为你在输入/输出部分的操作表达式中指定寄存器,或当你为一些输入/输出操作表达式使用“r”/“g”约束,让GCC为你选择一个寄存器时,GCC对这些寄存器的状态是非常清楚的,它知道这些寄存器是被修改的,你根本不需要在修改部分声明它们
注:指令部分必须有输入部分,输出部分、修改部分为可选,输入部分存在而输出部分不存在时,冒号“:”要保留,当"memory"存在时,三个冒号都要保留,如
#define __cli() __asm__ __volatile__("cli": : :"memory")
int count=1
int value=1
int buf[10]
void main()
{
asm(
"cld \n\t"
"rep \n\t"
"stosl"
:
: "c" (count), "a" (value), "D" (buf)
);
}
得到的主要汇编代码为
# 根据寄存器约束为变量选择合适的寄存器
movl count,%ecx
movl value,%eax
movl buf,%edi
#APP
cld
rep
stosl
#NO_APP
cld,rep,stos这几条的功能是向buff中写上count个value值,通过冒号后的语句指明输入,输出和被改变的寄存器,编译器就知道你的指令需要和改变哪些寄存器,从而优化寄存器的分配
符号"c"(count)指示把count的值放入ecx寄存器,系统会根据变量的宽度分配8/16/32位的寄存器,类似还有
a eax
b ebx
c ecx
d edx
S esi
D edi
I 常数值(0-31)
q,r 动态分配的寄存器
q指示编译器从eax、ebx、ecx、edx分配寄存器
r指示编译器从eax、ebx、ecx、edx、esi、edi分配寄存器
g eax,ebx,ecx,edx或内存变量
A 把eax和edx合成一个64位的寄存器
也可以让gcc自己选择合适的寄存器
asm("leal (%1,%1,4),%0"
: "=r" (x)
: "0" (x) # 输入与输出使用同一个寄存器
);
得到的主要汇编代码为
movl x,%eax
#APP
leal (%eax,%eax,4),%eax
#NO_APP
movl %eax,x # 最终系统为输出选择的寄存器中的内容会输出到变量中
如果强制使用固定的寄存器,如:不用占位符%1而用%ebx,则可以写为
asm("leal (%%ebx,%%ebx,4),%0"
: "=r" (x)
: "0" (x) # 尽管这里不需要输入,但是输入部分是必须存在的
);
来一个例子熟悉一下
#include <stdio.h>
int main()
{
int a = 10, b;
__asm__(
"movl %1, %%eax\n\t"
"movl %%eax, %0\n\t"
:"=r"(b) /* output */
:"r"(a) /* input */
:"%eax" /* clobbered register */
);
printf("Result: %d, %d/n", a, b);
return 0;
}
该程序实现把a的值赋给b
%0对应变量b,b是输出,系统自动为b指定一个寄存器
%1对应变量a,a是输入 ,系统自动为a指定一个寄存器
因为内嵌的汇编指令中用到了%eax寄存器,所以在修改部分进行声明,告诉编译器eax要被改写,在此期间不要用eax保存其它值
首先将变量a中的值赋给指定的寄存器,指定的寄存器通过%eax寄存器将值传递给变量b指定的那个寄存器,最后该寄存器将值赋给b
再来一个例子
#include <stdio.h>
int main(){
int data1 = 10;
int data2 = 20;
int result;
asm (
"imull %%edx, %%ecx\n\t"
"movl %%ecx, %%eax"
: "=a"(result)
: "d"(data1), "c"(data2)
);
printf("The result is %d\n",result);
return 0;
}
根据上文中的寄存器约束,这个例子中变量data1放在%edx中,data2放在%ecx中。输出结果会被放到%eax中,然后送到result变量中。
这里虽然显式的用到了各个寄存器名,但并不需要在修改部分中声明