最近在看“程序员的自我修养”,看到了gcc内嵌汇编,静态链接那章的示例程序比较有趣,于是准备学习一下AT&T语法的gcc内嵌汇编。以前学微机原理的时候学习过汇编,现在基本上还给了老师,还是复习一下吧。
像大家一样先来介绍一下AT&T语法与Intel asm语法的不同(顺便也学学基本知识):
在 AT&T 汇编格式中,寄存器名要加上 '%' 作为前缀;而在 Intel 汇编格式中,寄存器名不需要加前缀。例如:
AT&T 格式 |
Intel 格式 |
pushl %eax |
push eax |
在 AT&T 汇编格式中,用 '$' 前缀表示一个立即操作数;而在 Intel 汇编格式中,立即数的表示不用带任何前缀。例如:
AT&T 格式 |
Intel 格式 |
pushl $1 |
push 1 |
AT&T 和 Intel 格式中的源操作数和目标操作数的位置正好相反。在 Intel 汇编格式中,目标操作数在源操作数的左边;而在 AT&T 汇编格式中,目标操作数在源操作数的右边。例如:
AT&T 格式 |
Intel 格式 |
addl $1, %eax |
add eax, 1 |
在 AT&T 汇编格式中,操作数的字长由操作符的最后一个字母决定,后缀'b'、'w'、'l'分别表示操作数为字节(byte,8 比特)、字(word,16 比特)和长字(long,32比特);而在 Intel 汇编格式中,操作数的字长是用 "byte ptr" 和 "word ptr" 等前缀来表示的。例如:
AT&T 格式 |
Intel 格式 |
movb val, %al |
mov al, byte ptr val |
在 AT&T 汇编格式中,绝对转移和调用指令(jump/call)的操作数前要加上'*'作为前缀,而在 Intel 格式中则不需要。
远程转移指令和远程子调用指令的操作码,在 AT&T 汇编格式中为 "ljump" 和 "lcall",而在 Intel 汇编格式中则为 "jmp far" 和 "call far",即:
AT&T 格式 |
Intel 格式 |
ljump $section, $offset |
jmp far section:offset |
lcall $section, $offset |
call far section:offset |
与之相应的远程返回指令则为:
AT&T 格式 |
Intel 格式 |
lret $stack_adjust |
ret far stack_adjust |
基本的的内嵌格式:(每行用双引号括起来,有多行的话用“\n\t”分开)
asm("assembly code");
比如:
asm("movl %ecx %eax"); /* moves the contents of ecx to eax */
__asm__ ("movl %eax, %ebx\n\t"
"movl $56, %esi\n\t"
"movl %ecx, $label(%edx,%ebx,$4)\n\t"
"movb %ah, (%ebx)");
注:使用asm或__asm__开头都是可以的。
扩展asm格式:(Extended asm)
asm ( assembler template
: output operands /* optional */
: input operands /* optional */
: list of clobbered registers /* optional */
);
OR
asm("汇编语句"
:输出寄存器
:输入寄存器
:会被修改的寄存器);
如果没有输出的话,也需要使用“:”,那一行空着就行了:
asm ("cld\n\t"
"rep\n\t"
"stosl"
: /* no output registers */
: "c" (count), "a" (fill_value), "D" (dest)
: "%ecx", "%edi"
);
解释一下上述代码的作用:(cld,rep,stosl的具体使用方式请参加后面的说明)这几条语句的功能是向buf中写上count个value值.将count的值加载到ecx寄存器中(加载代码是"c"),fill_value加载到eax中,dest放到edi中。 同时告知gcc,寄存器eax和edi的内容不再有效了(clobbered registers)。
进一步说明一下:
int a=10, b;
asm ("movl %1, %%eax \n\t"
"movl %%eax, %0 \n\t"
:"=r"(b)
:"r"(a)
:"%eax"
);
printf("Result: %d, %d\n", a, b);
b 是输出操作符,%0 就是对b的一个引用,a是输出操作符,被%1引用
r 是对操作符的一个限制,r告诉gcc使用寄存器来保存操作符。使用‘=’来指明输出操作符
寄存器前面需要使用两个‘%’,这帮助gcc区别操作符和寄存器,操作符前面只有一个‘%’
在第三个冒号之后的被改变的(the clobbered register) 寄存器 %eax 告诉gcc该寄存器会在asm中被修改,不要在该寄存器中存值。
这段代码的效果是把a的值赋给b。
输入: Result:10,10
操作数:
下面这个例子是将x的值扩大五倍之后存放到five_times_x中
int five_times_x = 0;
int x = 3;
asm ("leal (%1,%1,4), %0 "
: "=r" (five_times_x)
: "r" (x)
);
printf("After five times x is %d\n",five_times_x);
leal(%1,%1, 4),%0" :x + x * 4 -> five_times_x
这个段代码中,x是输入,我们并没有指定使用的寄存器,gcc自动选择不同的寄存器来完成这些操作。我们也可以让gcc将输入和输出放在同样的寄存器中,只要在代码中稍加约束就可以办到了:
int x = 3;
int five_times_x = 0;
asm ("leal (%0,%0,4), %0"
: "=r" (five_times_x)
: "0" (x)
);
printf("After five times x is %d\n",five_times_x);
这段代码中输入和输出都是使用相同的寄存器,但是我们不知道使用的是哪个寄存器。
也可以指定一个寄存器被输入和输出共用:
int x = 3;
int five_times_x = 0;
asm ("leal (%%ecx,%%ecx,4), %%ecx"
: "=c" (five_times_x)
: "c" (x)
);
printf("After five times x is %d\n",five_times_x);
常用的寄存器约束的缩写:
r:I/O,表示使用一个通用寄存器,由GCC在%eax/ %ax/ %al、%ebx/ %bx/ %bl、%ecx/ %cx /%cl、%edx/%dx/%dl中选取一个GCC认为是合适的;
q:I/O,表示使用一个通用寄存器,与r的意义相同;
g:I/O,表示使用寄存器或内存地址;
m:I/O,表示使用内存地址;
a:I/O,表示使用%eax/%ax/%al;
b:I/O,表示使用%ebx/%bx/%bl;
c:I/O,表示使用%ecx/%cx/%cl;
d:I/O,表示使用%edx/%dx/%dl;
D:I/O,表示使用%edi/%di;
S:I/O,表示使用%esi/%si;
f:I/O,表示使用浮点寄存器;
t:I/O,表示使用第一个浮点寄存器;
u:I/O,表示使用第二个浮点寄存器;
A:I/O,表示把%eax与%edx组合成一个64位的整数值;
o:I/O,表示使用一个内存位置的偏移量;
V:I/O,表示仅仅使用一个直接内存位置;
i:I/O,表示使用一个整数类型的立即数;
n:I/O,表示使用一个带有已知整数值的立即数;
F:I/O,表示使用一个浮点类型的立即数;
=: O 表示此Output操作表达式是只写的
+ :O 表示此Output操作表达式是可读可写的
&:O 表示此Output操作表达式独占为其指定的寄存器
%:I 表示此Input操作表达式中的C/C++表达式可以与下一个Input操作表达式中的C/C++表达式互换
一些例子:
例一:
int foo = 10, bar = 15;
__asm__ __volatile__("addl %%ebx,%%eax"
:"=a"(foo)
:"a"(foo), "b"(bar)
);
printf("foo+bar=%d\n", foo);
输出:foo+bar=25
例二:
int my_var = 10, my_int = 15;
__asm__ __volatile__(
" lock ;\n\t"
" addl %1,%0 ;\n\t"
: "=m" (my_var)
: "ir" (my_int), "m" (my_var)
);
printf("my_int + my_var = %d",my_var);
输出:my_int + my_var = 25
说明:这个加法是原子的,如果将第一句的‘lock’去掉,可以消除加法的原子性。 代码中使用‘=m’表明my_var是一个程序的输出,并存储在内存中。‘ir’表明my_int是一个整数并存储在寄存器中。
字符串拷贝函数:
static inline char * strcpy(char * dest,const char *src)
{
int d0, d1, d2;
__asm__ __volatile__( "1:\t lodsb\n\t" //1:只是一个跳转标志
"stosb \n\t"
"testb %%al,%%al\n\t" //判断字符串是否复制结束
"jne 1b" //如果字符串未结束,跳转到1:处
: "=&S" (d0), "=&D" (d1), "=&a" (d2)
: "0" (src),"1" (dest)
: "memory");
return dest;
}
函数将esi寄存器指向的内容拷贝到edi中,到遇到0时停止。使用“&S”,“&D”,“&a”约束,表明寄存器esi,edi,eax的内容当函数执行之后不可用。
lodsb 指令:从esi 指向的源地址中逐一读取一个字符,送入AL 中; (然后,可以先判断这个字符是什么字符,如0dh,0ah 之类等,再执行相应的操作);
stosb 指令:一般跟随在lodsb 指令后面,将AL 中的字符逐一写入edi 指向的目的地址;
如果是lobsw ,表明要处理的是字,而不是字符;则采用的相应指令是:stosw ;那么要判断的寄存器是AX,而不是AL 了.
如果是lobsd ,表明要处理的是双字;则采用的相应指令是: stosd ;这时候,要判断的寄存器就是EAX 了.
代码中:
: "=&S" (d0), "=&D" (d1), "=&a" (d2)
表明esi内容来源于参数0,esi内容来源于参数1
而:
: "0" (src),"1" (dest)
表明了参数0即是函数参数列表中的src,参数1即是函数参数列表中的dest。
清除方向标志,在字符串的比较,赋值,读取等一系列和rep连用的操作中,di或si是可以自动增减的而不需要人来加减它的值,cld即告诉程序si,di向前移动,std指令为设置方向,告诉程序si,di向后移动
#define mov_blk(src, dest, numwords) \
__asm__ __volatile__ ( \
"cld\n\t" \
"rep\n\t" \
"movsl" \
: \
: "S" (src), "D" (dest), "c" (numwords) \
: "%ecx", "%esi", "%edi" \
)
先说搬移字串。搬移字串指令有两种,分别是 MOVSB 和 MOVSW,先说 MOVSB。MOVSB 的英文是 move string byte,意思是搬移一个字节,它是把 DS:SI 所指地址的一个字节搬移到 ES:DI 所指的地址上,搬移后原来的内容不变,但是原来 ES:DI 所指的内容会被覆盖而且在搬移之后 SI 和 DI 会自动地址向下一个要搬移的地址。
一般而言,通常程序设计师一般并不会只搬一个字节,通常都会重复许多次,如果要重复的话,就得把重复次数 ( 也就是字串长度 ) 先记录在 CX 寄存器,并且在 MOVSB 之前加上 REP 指令,REP 是重复 (repeat) 的意思。这种写法很是奇怪,一般而言汇编语言源文件的每一行都只有一个指令,但 REP MOVSB 却可以在同一行写两个指令,当然分开写也是一样的。
对于cld 和 movsl 的使用可以参考:
http://www.cnblogs.com/cykun/archive/2010/10/27/1862940.html