GNU-C内嵌汇编

前言

各方面都需要在c(gcc)中使用汇编,因此系统的学习下内联汇编

AT&T与Intel汇编

之前一直在用的是Intel汇编,其读起来比较方便。但gcc中使用的是AT&T汇编,二者都是对x86-64指令集的助记,只是编译器支持不同罢了。

主要不同点:

1.源操作数和目的操作数的方向
AT&T和Intel汇编语法源操作数和目的操作数的方向正好相反。AT&T中是左源右目的。

1
2
OP-code dst src //Intel语法
Op-code src dst //AT&T语法

2.寄存器命名
在AT&T汇编中, 寄存器名前有%前缀。例如,如果要使用eax,得写作: %eax。

3.立即数
在AT&T语法中,立即数都有’$’前缀。引用的C语言静态变量也必须放上’$’前缀。

1
2
ffh //Intel语法
$0xff //AT&T语法

4.操作数大小
在AT&T语法中,操作符的最后一个字符决定着操作数访问内存的长度:b、w、l、q表示8、16、32、64bit

1
2
mov al, byte ptr foo //Intel语法
movb foo, %al //AT&T语法

5.内存操作数
在Intel语法中,基址寄存器是放在方括号[]中的,但AT&T是放在小括弧()内的。
此外对于AT&T汇编,当一个常数被用作disp或者scale时,不需要’$’前缀。

1
2
section:[base + index * scale + disp] //Intel语法 基、索引*比例、偏移
section:disp(base, index, scale) //AT&T语法 相同

示例对比:

Intel Code AT&T Code
mov eax,1 movl $1,%eax
mov ebx,0ffh movl $0xff,%ebx
int 80h int $0x80
mov ebx, eax movl %eax, %ebx
mov eax,[ecx] movl (%ecx),%eax
mov eax,[ebx+3] movl 3(%ebx),%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

基本内联汇编

如果内联汇编有多条指令,则每行要加上双引号,并且该行要以\n\t结尾。这是因为GCC会将每行指令作为一个字符串传给as(GAS),使用换行和TAB可以将正确且格式良好的代码行传递给汇编器。

1
2
3
4
__asm__ ( "movl %eax, %ebx\n\t"
"movl $56, %esi\n\t"
"movl %ecx, $label(%edx,%ebx,$4)\n\t"
"movb %ah, (%ebx)");

内联汇编表达式

1
2
3
4
5
__asm__( assembler template
: output operands /* optional */
: input operands /* optional */
: list of clobbered registers /* optional */
);

汇编指令部分;出描述;入描述;损坏说明

示例:用汇编代码把a的值赋给b

1
2
3
4
5
6
7
int a=10, b;
asm ( "movl %1, %%eax;
movl %%eax, %0;"
:"=r"(b) /* output */
:"r"(a) /* input */
:"%eax" /* clobbered register */
);

说明:

  1. “b”是输出操作数,用%0来访问,”a”是输入操作数,用%1来访问。
  2. r约束,让GCC自己去选择一个寄存器去存储变量a与b。=是输出必须加的只读修饰。
  3. 操作数已经用了一个%作为前缀,寄存器只能用“%%”做前缀了
  4. 内联汇编代码中会改变寄存器eax的内容,如此一来GCC在调用内联汇编前就不会依赖保存在寄存器eax中的内容了。

汇编模板部分

  1. 每条指令放在一个双引号内,或者将所有的指令都放着一个双引号内。
  2. 每条指令都要包含一个分隔符。合法的分隔符是换行符(\n)或者分号。用换行符的时候通常后面放一个制表符\t。
  3. 访问C语言变量用%0,%1…%9。最多10条

操作数

格式:”约束”(c表达式)
“constraint” (C expression) //“=r”(result)

对于输出操作数一定要用 “=”或”+”修饰。 constraint主要用来指定操作数的寻址类型 (内存寻址或寄存器寻址),也用来指明使用哪个寄存器。
=表示纯粹输出、+表示输入输出都可、&只位于=+之后表示不可再为输入分配该内容。
多个操作数用,隔开
在汇编模板部分,我们按顺序用数字去引用操作数。%0-%9

%0表示引用那个内容,内容由后面的约束决定,同时执行前和执行后会进行表达式与指定内容的交互。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
asm ( "leal (%1,%1,4), %0" //结尾的l表示32bit,%0=%1+%1*4 %0引用第一个操作数,%1引用第二个
: "=r" (five_times_x) //r表示任何输入输出的寄存器,=表示只用于输出
: "r" (x) //r表示任何输入输出的寄存器,编译器自己选择
);
asm( "lea (%0,%0,4),%0" //使用%0引用第一个操作数
: "=r" (five_times_x) //输出第一个操作数,寄存器编译器自己选择
: "0" (x) //输入也使用0号引用
);
asm ( "leal (%%ecx,%%ecx,4), %%ecx" //使用ecx寄存器
: "=c" (x) //c表示指定编译器使用rcx/ecx/cx/cl寄存器输出
: "c" (x) //c表示指定编译器使用rcx/ecx/cx/cl寄存器输入
);

对于输入输出操作数中出现的寄存器不用写损坏说明

常用constraint约束:
r:操作数将被存储在通用寄存器中,gcc自己选择

q:从eax、ebx、ecx、edx中指派一个寄存器
a:rax,eax, ax, al
b:rbx,ebx, bx, bl
c:rcx,ecx, cx, cl
d:rdx,edx, dx, dl
S:rsi,esi, si
D:rdi,edi, di

m:使用寄存器传递数据往往是更改完数据再写回c变量的内存中,直接使用内存可以省去步骤。

1
asm (“sidt %0” : : “m”(loc) ); //使用sidt指令获取中断向量表的位置存于loc变量(内存)中。

匹配constraint:在某些情况下,一个变量可能被用来传递输入也用来保存输出。+指的是一个输出操作数即可输入又可输出。

1
2
//inc 操作数+1 l指定32bit
asm (“incl %0” :”=a”(var) : “0”(var) ); //输入变量先被读入eax中,incl执行之后,%eax被更新并且保存到变量var中。这里的constraint ”0”就是指定使用和第一个输出相同的寄存器

m: 使用一个内存操作数,内存地址可以是机器支持的范围内
o: 使用一个内存操作数,但是要求内存地址范围在在同一段内
V: 内存操作数,但是不在同一个段内。
i: 使用一个立即整数操作数(值固定);也包含仅在编译时才能确定其值的符号常量。
F: 浮点类型的立即数
g: 除了通用寄存器以外的任何寄存器,内存和立即整数。

损坏说明

如果某个指令改变了某个寄存器的值,我们就必须在asm中第三个冒号后的Clobber List中标示出该寄存器。为的是通知GCC,让其不再假定之前存入这些寄存器中的值依然合法。输入输出寄存器不用放Clobber List中,因为GCC能知道asm将使用这些寄存器。其他使用到的寄存器,无论是显示还是隐式的使用,必须在clobbered list中标明。
如果指令中以无法预料的形式修改了内存值,需要在clobbered list中加上”memory”。从而使得GCC不去缓存在这些内存值。

示例:

1
2
3
4
5
6
7
asm( "movl %0,%%eax;
movl %1,%%ecx;
call _foo"
: /*no outputs*/
: "g" (from), "g" (to)
: "eax", "ecx" //汇编部分修改了这俩个寄存器
);

防止优化

内联汇编一般与volatile一起使用。用于阻止编译器优化

1
__asm__ __volatile__ (...);

示例

示例1:
俩数相加:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int main(void)
{
int foo = 10, bar = 15;
__asm__ __volatile__ ( "addl %2, %0 \n\t"
"movl $1,%0 \n\t" //自己加着尝试的,每行必须一组""与\n\t
: "=r"(foo)
: "0"(foo), "r"(bar)
);
printf("%d\n", foo);
return 0;
}
//另一个实现
//要移除该原子操作可以删除lock指令。在输出部分“=m”指出直接输出到内存my_var。类似的,”ir”是指my_int是一个整型数并且要保存到一个寄存器中
__asm__ __volatile__ (
" lock \n"
" addl %1,%0; \n"
: "=m" (my_var)
: "ir" (my_int), "m" (my_var) //m是指定地址,因此就锁定到my_var的地址使用了,编译器先确定入出部分的使用,再在代码中%0-9处引用
: /* no clobber-list */
);

lock前缀的指令,使cpu多处理器下互斥的使用这个地址。当指令执行完毕,这个锁定动作也会消失。可以与:BT、ADD、OR、ADC、SBB、AND、SUB、XOR、NOT、NEG、INC、DEC等一起使用。

看下它的返汇编:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
.text:000000000000064A ; int __cdecl main(int argc, const char **argv, const char **envp)
.text:000000000000064A public main
.text:000000000000064A main proc near ; DATA XREF: _start+1D↑o
.text:000000000000064A
.text:000000000000064A var_8 = dword ptr -8
.text:000000000000064A var_4 = dword ptr -4
.text:000000000000064A
.text:000000000000064A ; __unwind {
.text:000000000000064A push rbp
.text:000000000000064B mov rbp, rsp
.text:000000000000064E sub rsp, 10h
.text:0000000000000652 mov [rbp+var_8], 0Ah
.text:0000000000000659 mov [rbp+var_4], 0Fh
.text:0000000000000660 mov eax, [rbp+var_8]
.text:0000000000000663 mov edx, [rbp+var_4]
.text:0000000000000666 add eax, edx
.text:0000000000000668 mov eax, 1
.text:000000000000066D mov [rbp+var_8], eax
.text:0000000000000670 mov eax, [rbp+var_8]
.text:0000000000000673 mov esi, eax
.text:0000000000000675 lea rdi, format ; "%d\n"
.text:000000000000067C mov eax, 0
.text:0000000000000681 call _printf
.text:0000000000000686 mov eax, 0
.text:000000000000068B leave
.text:000000000000068C retn
.text:000000000000068C ; } // starts at 64A
.text:000000000000068C main endp

示例2:

1
2
3
4
5
__asm__ __volatile__ ( "decl %0; sete %1" //将my_var-1,如果为0就将cond置1
: "=m" (my_var), "=q" (cond)
: "m" (my_var)
: "memory" //代码将改变内存值
);

示例3:
设置和清除寄存器中的某一位

1
2
3
4
5
__asm__ __volatile__( “btsl %1, %0” //BTS(Bit Test and Set): 位测试并置位
: “=m” (ADDR)
: “Ir” (pos)
: “cc”
);

示例4:
在Linux中,系统调用是用GCC内联汇编的形式实现的。

1
2
3
4
5
6
7
8
9
10
11
12
#define _syscall3(type,name,type1,arg1,type2,arg2,type3,arg3) \
type name(type1 arg1,type2 arg2,type3 arg3) \
{ \
long __res; \
__asm__ volatile ( "int $0x80" \
: "=a" (__res) \ //返回
: "0" (__NR_##name),"b" ((long)(arg1)),"c" ((long) arg2)), \ //输入 __NR_##name表示调用号
"d" ((long)(arg3))); \
__syscall_return(type,__res); \
}`
所有带三个参数的系统调用都会用上面这个宏来执行。这段代码中,系统调用号放在eax中,参数分别放在ebx,ecx,edx中,最后用”int 0x80”执行系统调用。返回值放在eax中。