1,AT&T与Intel汇编语法的差异
X86汇编使用Intel汇编语法,而Linux kernel使用AT&T汇编语法,所以先简单介绍 AT&T 和Intel 汇编语法
的不同之处。
1) 源操作数和目操作数方向
AT&T和 Intel 汇编语法相反,Intel 语法中第 1 个操作数作为目的操作数,第 2 个操作
数作为源操作数;相反,在 AT&T 语法中,第 1 个操作数是源操作数,第 2 个是目的操作数。
例如:
Intel 语法: "OP-code dst src"
AT&T 语法: "Op-code src dst"
2) 寄存器命名
在 AT&T 语法中,寄存器名字加上%前缀,例如,如果要使用 eax,写作:%eax。
3) 立即数
AT&T 语法中立即数以'$'符号作为前缀,静态 C变量前也要加上'$'前缀,在 Intel语
法中,16 进制常数加上'h'后缀, 但是在 AT&T 中,常量前要加上'0x',对于个 16 进制常数(在
AT&T 中)首先以$开头接着是 0x,最后是常数。
4) 操作数大小
在AT&T语法中,操作数占内存大小决定于汇编命令操作符最后个内容。操作符以'b', 'w'
和 'l'为后缀指明内存访问长度是 (8-bit), word(16-bit)还是long(32-bit)。而Intel语法在
操作数前加上' ptr', 'word ptr'和'dword ptr'内存操作数(这个操作数不是汇编命令操作符)
来达到相同目的。因此, Intel "mov al, ptr foo" 用AT&T语法就是 :"movb foo, %al"。
5) 内存操作数
在Intel语法中, 基址寄存器用'['和']'扩起来, 但是在AT&T中, 改用'('和')', 此外在Intel
语法中个间接内存寻址:
section:[base + index * scale + disp]在AT&T中则为:
section:disp(base, index, scale)
总之,需要记住点就是当个常数被用作disp或者scale时就不用加'$'前缀。现在,我们已
经提到了AT&T和Intel语法的主要区别点,我只是提到了小部分,全部内容可以参考GNU汇编文档
为了。
更好理解这些区别请看下面例子:
Intel Code AT&T Code
mov eax,1 movl $1, %eax
mov ebx,0ffh movl $0x0ff,%ebx,
80h $0x80
mov ebx,eax movl %eax,%ebx
mov eax,[ecx] movl (%ecx),%eax
mov eax,[ecx+3] movl 3(ecx),eax
mov eax,[ebx+20h] movl 0x20(%ebx),%eax
add eax,[ebx+ecx*2h] addl (%ebx,%ecx,%0x2),%eax
lea eax,[ebx+ecx] leal (%ebx,%ecx),%eax
sub eax,[ebx+ecx*4h-20h] subl -0x20(%ebx,%ecx,0x4),%eax
2.内联汇编基本形式
内联汇编格式非常直观:
asm(“assembly code”);
例如:
asm("movl %ecx, %eax"); /* 把 ecx 内容移动到 eax */
__asm__("movb %bh , (%eax)"); /* 把bh中个字节内容移动到eax 指向内存 */
你可能注意到了,这里使用了asm和__asm__关键字。二者皆可,如果asm关键字和其它变量有冲
突就可以使用__asm__了。如果有超过行指令每行要加上双引号,并且后面加上\n\t。这时GCC将
每行指令作为个串传给as(GAS),使用换行和TAB可以给汇编器传送正确格式化好代码行。
例如:
__asm__ ("movl %eax, %ebx\n\t "
"movl $56, %esi\n\t "
"movl %ecx, $label(%edx,%ebx,$4) \n\t "
"movb %ah, (%ebx)");
如果我们代码涉及到寄存器(例如改变了其内容),并且从汇编代码返回后并没有修复这些改变,
意想不到情况可能发生,GCC不知道你已经将寄存器内容改了,这将给我们带来麻烦,尤其在编
译器作了优化情况下,我们能做就是不要使用这些带来其它附加影响语句或者当我们退出时候还
原这些内容,否则只有等待崩溃了这里提到这种情况,就是我们将要在下节中阐述扩展形式内联
汇编。
3. 扩展形式内联汇编
前面介绍基础形式内联汇编思路方法,只涉及到嵌入汇编指令在高级形式中,我们将可以
指定操作数, 它允许我们指定输入输出寄存器[内联使用这些寄存器作为存储输入输出变量]和涉
及到clobbered寄存器列表[clobbered registers:内联汇编可能要改变其内容寄存器], 也并不
是定要要显式指明使用具体寄存器我们,也可以把它留给GCC去选择,这样GCC还可能更好进行优化处理,高级内联汇编基本格式如下:
asm ( assembler template
: output operands /* optional */
: input operands /* optional */
: list of clobbered registers /* optional */
);
其中assembler template包含汇编指令部分,括号中每个操作数用C表达式常量串描述,区别部
分用冒号分开,相同部分中每个小部分用逗号分开,操作数多少被限定为10或者由机器决定最大
值。如果没有输出部分但是有输入部分就必须在输出部分的前连续写两个冒号。
例如 :
asm ("cld \n\t"
"rep\n\t "
"stosl"
: /* no output registers */
: "c" (count), "a" (fill_value), "D" (dest)
: "%ecx", "%edi"
);
现在我们来分析上面代码功能, 上面代码循环count次把fill_value值到填充到edi寄存器指定内
存位置并且告诉GCC寄存器eax[这里应该是ecx]和edi中内容可能已经被改变了, 为了有个更清晰
理解我们再来看个例子:
a=10, b;
asm ("movl %1, %%eax;
movl %%eax, %0;"
:"=r"(b) /* output */
:"r"(a) /* input */
:"%eax" /* clobbered register */
);
上面代码所做就是用汇编代码把a值赋给b值得注意几点有:
1)“b”是输出操作数用%0来访问,”a”是输入操作数用%1来访问;
2)“r” 是个constra, 有关constra后面有详细介绍,这里我们只要记住这里constra ”r”让
GCC自己选择一个寄存器去存储变量a,输出部分constra前必须要有个“=”用来介明这是个输出
操作数并且只写;
3)你可能看到有寄存器名字前面写了两个%,这是用来帮助GCC区分操作数和寄存器,操作数只需
要1个%前缀;
4)在第 3个冒号后面clobbered register部分,%eax说明在内联汇编代码中将要改变eax中内容
GCC不要用他存储其它值。
当这段代码执行结束后,“b”值将会被改掉,它被指定作为输出操作数,换句话说在“asm”内
部对b改动将影响到asm外面。
下面我们将对各个部分分别进行详细讨论:
5.1 汇编模板
汇编模板部分包含嵌入到C中汇编指令格式如下:
每条指令放在个双引号内,或者将所有指令都放着个双引号内,每条指令都要包含1个分隔符,
合法分隔符是换行符(\n)或者分号用换行符时候通常后面放个制表符“\t”,我们已经知道为什么使用换行符+制表符了[前面部分有解释],其中访问 C操作数用%0,%1…等等。
5.2 操作数
C语言表达式 [大多情况是C变量] 将作为”asm”内部使用操作数每个操作数都以双引号开始,
对于输出操作数还要写个修改标志(=),constra和修改标志都放在双引号内,接下来部分就是C
表达式了[放在括号内]。举例来说:
标准形式如下:
"constra" (C expression) [ 如: “=r”(result) ]
对于输出操作数还有个修改标志(=) constra主要用来指定操作数寻址类型 (内存寻址或寄存器
寻址)也用来指明使用哪个寄存器,如果有多个操作数的间用逗号分隔,在汇编模板中每个操作
数都用数字引用[这些操作数],引用规则如下,如果总共有n个操作数(包括输入输出操作数),
那么第1个输出操作引用数字为0,依次递增,然后最后一个操作数是n-1,有关最多操作数限制
参见前面小结。
输出操作数表达式必须是左值,输入操作数没有这个限制,注意这里可以使表达式[不仅仅限于
变量],高级汇编形式常用在当编译器不知道这个机器指令存在的时候;-),如果输出表达式不能
直接寻址(比如是bit-field), constra就必须指定个寄存器。这种情况下GCC将使用寄存器作为
asm输出,然后保存这个寄存器值到输出表达式中。
如上所述般输出操作数必须是只写,GCC将认为在这条指令之前保存在这种操作数中的值已经过
期和不再需要了。高级形式asm也支持输入输出或者读写操作数。
现在我们来看些例子把个数字乘以5使用汇编指令lea
asm( “leal (%1,%1,4), %0”
: “=r” (five_times_x)
: “r” (x)
);
这里输入操作数是‘x’,不指定具体使用那个寄存器,GCC会自己选择输入输出寄存器来操作,
如果我们也可以让
GCC把输入和输出寄存器限定同个只需要使用读写操作数使用合适constra看下具体思路方法:
asm(“lea (%0,%0,4),%0”
: “=r” (five_times_x)
: “0” (x)
);
上面使输入和输出操作数存在相同寄存器中,我们不知道GCC具体使用哪个寄存器,但是我们也
可以指定个像这样:
asm(“lea (%0,%0,4),%0”
: “=c” (five_times_x)
: “c” (x)
);
上面 3个例子中, 我都没有在clobber list中放入任何寄存器值, 这是为什么?在前两个例子中,
GCC决定使用哪个寄存器并且自己知道哪儿改变了,第3个例子中我们也没有必要把ecx放在
clobber list中,是GCC知道X将存入其中,GCC知道ecx值,所以我们也不用放入clobber list。
5.3 Clobber List
一些指令破坏了某个寄存器值,我们就不得不在asm里面第3个冒号后Clobber List中标示出来通
知GCC这个里面值要被改掉, 这样GCC将不再假设的前存入这些寄存器中值是合法了我们不需要把
输入输出寄存器在这个部分标出,GCC知道asm将使用这些寄存器(它们已经显式被作为输入输出标出)。此外,如果指令中还用到其它寄存器,无论显示还是隐式使用到(没有在输入输出中标示
出),这些指令必须在clobbered list中标明。如果指令中以不可预见形式修改了内存值要加上”
memory”到clobbered list中这使得GCC不去缓存Cache在这些内存值。还有,如果内存被改变而
没有被列在输入和出部分,要加上volatile关键字,如果需要,可以对clobbered 寄存器多次读
写。
来看个乘法例子; _foo要求接受在eax和ecx值作为参数
asm(“movl %0,%%eax;
“movl %1,%%ecx;
Call _foo”
:/*no outputs*/
:“g”(from), “g” (to)
: “eax”, “ecx”
);
5.4 Volatile…?
如果你熟悉内核代码或者一些类似优秀代码,你一定见过很多在asm或者__asm__后声明前加了
volatile 或者__volatile__,之前我提到过有关asm和__asm__,但是volatile有什么用途呢?
如果汇编代码必须在我们放的位置被执行(例如不能被循环优化而移出循环),那就在asm的后面
放个valatile关键字,这样可以禁止这些代码被移动或删除,我们可以这样声明:
asm volatile ( ... : ... : ... : ...);
如果担心有变量冲突,使用__volatile__关键字,如果汇编语句只是做些运算而没有什么附加影
响,最好不要使用volatile,不用volatile时会给GCC做代码优化留下空间,在“常用窍门技巧”
章节中给出了很多例子,在那里你也可以详细看到clobber-list使用。
4. 深入constra
此时你可能理解了constra对内联汇编有很大影响,但是我们到目前为止才接触到有关constra
小部分,constra可以指出各操作数是在寄存器中,在哪个寄存器中,指出操作数是个内存引用
或具体内存地址,无论操作数是直接常量或者可能是什么值。
6.1 常用constras
虽然有很多constras,但是常用只有少数,下面我们就来看下这些限制条件。
1. 寄存器操作数限制条件: r
如果操作数指定了这个限制,操作数将使用通用寄存器来存储看下面例子:
asm ( “movl %%eax, %0” : “=r” (myval));
变量myval被保存在寄存器,eax中值被拷贝到这个寄存器中,并且在内存中myval值也会按这个
寄存器值被更新。当constras ”r” 被指定时GCC可能在任何个可用通用寄存器中保存这个值。
当然,如果你要指定具体使用那个寄存器,就要指定具体使用哪个寄存器constras如下表:
r Register(s)
a %eax, %ax, %al
b %ebx, %bx, %bl
c %ecx, %cx, %cl
d %edx, %dx, %adl
S %esi, %si D %edi, %di
2. 内存操作数constra: m
当操作数在内存中时,任何对其操作将直接通过内存地址进行,和寄存器constra相反,内存操
作是先把值存在某个寄存器中,修改后再将值回写到这个内存地址。寄存器constra通常只用在
对速度要求非常严格场合,内存constra可以更有效率,将各C语言变量在asm中更新[不需要寄存
器中转],而且可能你也不想用寄存器来暂存这个变量值例如:
asm (“sidt” %0” : : “m”(loc) );
3. 匹配constra
在某些情况下,一个变量可能用来保存输入和输出两种用途, 这种情况下, 我们就用匹配constra
asm (“incl %0” :”=a”(var) : “0”(var) );
我们在的前章节中已经看过类似例子,这个例子中eax寄存器被用来保存输入也用来保存输出变
量,输入变量被读入eax中,incl执行后,eax被更新,并且又保存到变量var中,这儿constra ”
0”指定使用和第1个输出相同寄存器,就是说输入变量应该只能放在eax中,这个constra可以在
下面情况下被使用:
a) 输入值从一个变量读入,这个变量将被修改并且修改过值要写回同一个变量;
b) 没有必要把输入和输出操作数分开
使用匹配constra最重要好处是对变量寄存器地使用更高效。
其它constra
1. “m”: 使用一个内存操作数,内存地址可以是机器支持范围内;
2. “o”: 使用一个内存操作数,但是要求内存地址范围在同段内,例如加上个小偏移量来形成一
个可用地址
3. “V”: 内存操作数,但是不在同个段内。换句话说,就是使用”m”所有情况除了”o”
4. “i”: 使用个立即整数操作数(值固定);也包含仅在编译时才能确定其值符号常量
5. “n”: 个确定值立即数很多系统不支持汇编时常数操作数小于个字这时候使用n就比使用i
好
6. “g”: 除了通用寄存器以外任何寄存器内存和立即整数
下面是x86特有constra:
"r" : Register operand constra, look table given above.
"q" : Registers a, b, c or d.
"I" : Constant in range 0 to 31 (for 32-bit shts).
"J" : Constant in range 0 to 63 (for 64-bit shts).
"K" : 0xff.
"L" : 0xffff.
"M" : 0, 1, 2, or 3 (shts for lea instruction).
"N" : Constant in range 0 to 255 (for out instruction).
"f" : Floating po register
"t" : First (top of stack) floating po register
"u" : Second floating po register
"A" : Species the `a’ or `d’ registers. This is primarily useful for 64-bit eger values
ended to be ed
with the `d’ register holding the most signicant bits and the `a’ register holding
the least signicant
bits.
6.2 constra修改标记 在使用constra时候为了更精确控制约束GCC提供了些修改标记常用 修改标记有:
1. “=”指这个操作数是只写;之前保存在其中的值将废弃而被输出值所代替
2. “&” Means that this operand is an earlyclobber operand, which is modied before the
instruction
is finished using the input operands. Therefore, this operand may not lie in a register
that is used as
an input operand or as part of any memory address. An input operand can be tied to an
earlyclobber
operand its _disibledevent=>__asm__ __volatile__ (“ addl %%ebx, %%eax”
: ”=a”(foo)
: ”a”(foo), “b”(bar)
);
prinft(“foo+bar=%d \n”, foo);
0;
}
这里我们强制让GCC将foo值存在%eax,bar 存在5ebx中并且让输出放在%eax中,其中”=”表明这
是个输出寄存器,再看看其它思路方法来加这两个数
__asm__ __volatile__ (
“lock; \n ”
“addl %1,%0; \n ”
:”=m”(my_var)
:”ir”(my_), “m”(my_var)
:
);
这是个原子加法操作可以去除指令lock移除原子性在输出部分”=m”指出my_var作为输出并且在
内存中类似
”ir”指出my_是个整型数并且要保存到个寄存器中(可以想象上面有关constra表)这里没有
clobber list
2. 我们在些寄存器活变量上来执行些动作来对比下这些值
__asm__ __volatile__ ( “decl %0; e %1”
: “=m” (my_var), “=q” (cond)
: “m” (my_var)
: ”memory”
);
上面将my_var减并且如果减后结果为零就将cond置位我们可以再汇编语句的前加上”lock; \n
\t”变成原子操作,同样我们可以用”incl %0”来代替”decl %0”来增加my_var值
这里值得注意几点是:
1) my_var是个存在内存中变量
2) cond是个存在任何通用寄存器中(eax,ebx,ecx,edx)这时由于限制条件”=q”决定
3) clobber list中指定了memory介绍说明代码将改变内存值
3. 如何设置和清除寄存器中某位?这就是下个我们要看窍门技巧
__asm__ __volatile__( “btsl %1, %0”
: “=m” (ADDR)
: “Ir” (pos) : “cc”
);
这里在变量ADDR(个内存变量)在’pos’位置值被设置成了1.我们可以时候btrl来清除由btsl设
置位.pos变量限定”Ir” 指明pos放在寄存器中并且值为0-31(I是个x86相关constra).例如我们
可以设置或者清除ADDR变量中,从第0到第31位值这个要改变其中值所以我们加上”cc”在
clobberlist中
4. 现在我们来看些更加复杂但是有用的串拷贝
inline char* strcpy (char* dest, const char* src)
{
d0, d1, d2;
__asm__ __volatile__( "1:\tlodsb\n\t"
"stosb\n\t"
"testb %%al,%%al\n\t"
"jne 1b"
: "=&S" (d0), "=&D" (d1), "=&a" (d2)
: "0" (src),"1" (dest)
: "memory");
dest;
}
源地址存在ESI寄存器中目地址存在EDI中接着开始复制直到遇到0结束复制约束条件”&S”,”
&D”,”&a”指明我们使用是ESI,EDI和EAX寄存器并且这些寄存器是很明显clobber寄存器("=&S"
(d0), "=&D" (d1), "=&a"(d2) 这里用这 3个寄存器作输出GCC很明显知道他们将被clobber所以
后面clobber list不用再写了),它们内容在执行后会改变这里还有很明显可以看出为什么
memory被放在clobber list中 (d0, d1, d2被更新)
我们再来看个相似用来移动块双字注意这个通过宏来定义
# mov_blk(src, dest, numwords) \
__asm__ __volatile__ (\
"cld\n\t" \
"rep\n\t " \
"movsl" \
: \
: "S" (src), "D" (dest), "c" (numwords) \
: "%ecx", "%esi", "%edi" \
)
这里没有输出块移动过程导致ECX,ESI,EDI内容改变所以我们必须把它们放在clobber list中
在linux中系统是用 GCC内联汇编形式实现就让我们来看看个系统是如何实现所有系统都是用宏
来写
(linux/unistd.h). 例如个带 3个参数系统定义如下:
# _syscall3(type,name,type1,arg1,type2,arg2,type3,arg3) \
type name(type1 arg1,type2 arg2,type3 arg3) \
{ \
long __res; \
__asm__ volatile ( " $0x80" \
: "=a" (__res) \ : "0" (__NR_##name),"b" ((long)(arg1)),"c" ((long)(arg2)), \
"d" ((long)(arg3))); \
__syscall_(type,__res); \
}
旦个带 3个参数系统发生上面这个宏用来执行系统系统号放在eax中每个参数放在ebx,ecx,edx
中最后”0x80”执行系统返回值放在eax中,所有系统都是用上面类似方式实现.Exit是带个参数
系统我们看下这个实现代码如下:
{
asm("movl $1,%%eax; /* SYS_exit is 1 */
xorl %%ebx,%%ebx; /* Argument is in ebx, it is 0 */
$0x80" /* Enter kernel mode */
);
}
Exit号是1参数为0,所以我们把1放到eax中和把0放到ebx中,通过 $0x80 exit(0)就被执行了
这就是exit如何工作