这显得很古怪,不过在gcc知道程序员拿这些寄存器做些什么后,这确实能够对gcc的优化操作有所帮助。表5-3中是一些可能会用到的寄存器加载代码及其具体的含义。
表5-3 常用寄存器加载代码说明
代 码
说 明
代 码
说 明
a
使用寄存器eax
m
使用内存地址
b
使用寄存器ebx
o
使用内存地址并可以加偏移值
c
使用寄存器ecx
I
使用常数0~31
d
使用寄存器edx
J
使用常数0~62
S
使用esi
K
使用常数0~255
D
使用edi
L
使用常数0~65535
q
使用动态分配字节可寻址机寄存器(eax、ebx、ecx、或edx)
M
使用常数0~3
r
使用任意动态分配的寄存器
N
使用1字节常数
g
使用通用有效的地址即可(eax、ebx、ecx、edx或内存变量)
O
使用常数0~31
A
使用eax与edx联合(64位)
下面的例子不是让程序员自己指定哪个变量使用哪个寄存器,而是让gcc为程序员选择。
01 asm("leal (%1, %1, 4),
%0"02 : " =r"(y)03 : "0"(x));
第一句汇编语句leal(r1,r2,4),r3语句表示r1+r2*4→r3,。这个例子可以非常快地将x乘5。其中“%0”,“%1”是指gcc自动分配的寄存器。这里“%1”代表输入值x要放入的寄存器,“%0”表示输出值寄存器。输出寄存器代码前一定要加入等于号。如果输入寄存器的代码时0或为空时,则说明使用与相应输出一样的寄存器。所以。如果gcc将r指定为eax的话,那么上面汇编语句的含义即为:“leal(eax,eax,4),eax”。
注意:在执行代码时,如果不希望汇编语句被gcc优化而改变位置,就需要在asm符号后面添加volatile关键词:asm
volatile(……);
或者更详细地说明为:_
_asm_ _ _ _volatile_ _(……);
一、AT&T 格式Linux 汇编语法格式
1.在 AT&T
汇编格式中,寄存器名要加上 '%' 作为前缀;而在 Intel 汇编格式中,寄存器名不需要加前缀。 AT&T
格式
Intel
格式
pushl �x
push eax
2.在 AT&T 汇编格式中,用 '$' 前缀表示一个立即操作数;而在 Intel
汇编格式中,立即数的表示不用带任何前缀。例如:
AT&T
格式
Intel
格式
pushl $1
push 1
AT&T 和 Intel
格式中的源操作数和目标操作数的位置正好相反。在 Intel 汇编格式中,目标操作数在源操作数的左边;而在
AT&T 汇编格式中,目标操作数在源操作数的右边。例如:
AT&T
格式
Intel
格式
addl $1, �x
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
5.在 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
在 AT&T 汇编格式中,内存操作数的寻址方式是
section:disp(base, index, scale)
而在 Intel 汇编格式中,内存操作数的寻址方式为:
section:[base + index*scale + disp]
由于 Linux 工作在保护模式下,用的是 32
位线性地址,所以在计算地址时不用考虑段基址和偏移量,而是采用如下的地址计算方法:
disp + base + index * scale
下面是一些内存操作数的例子:
AT&T
格式
Intel
格式
movl -4(�p), �x
mov eax, [ebp - 4]
movl array(, �x, 4), �x
mov eax, [eax*4 + array]
movw array(�x, �x, 4), %cx
mov cx, [ebx + 4*eax + array]
movb $4, %fs:(�x)
mov fs:eax, 4
linux内核嵌入式汇编总结(1)
在linux内核中有很多的嵌入式汇编代码。
嵌入汇编的基本格式为:
asm("汇编语句":输出寄存器:输入寄存器 :会被修改的寄存器);
其中”汇编语句”是程序员写汇编指令的地方;”输出寄存器”表示当这段嵌入式汇编执行之后,哪些寄存器用于存放输出数据。这些寄存器会分别对应一个C语言表达式或一个内存地址;“输入寄存器”表示在开始执行汇编代码时,这里指定的一些寄存器中应存放的输入值,它们也分别对应着一个C变量或常数值。下面用例子来说明嵌入式汇编语句的使用方法。
我们在下面列出了一段代码作为例子来详细解说,
01 #define get_seg_byte(seg,addr)
\ 02 ({
\ 03 register char __res;
\ 04 __asm__("push %%fs;
\ 05 mov %%ax, %%fs;
\ 06 movb %%fs: %2, %%al;
\ 07 pop %%fs"
\ 08 :" =a" (__res)
\ 09 :"" (seg),"m" (* (addr)));
\ 10 __res; })
这段10行代码定义了一个嵌入式汇编语言宏函数。通常使用汇编语句最方便的方式是把它们放在一个宏内。用圆括号括住的组合语句(花括号中的语句)可以作为表达式使用,其中最后的变量__res(第10行)是该表达式的输出值。
因为是宏语句,需要在一行上定义,因此这里使用反斜杠'\'将这些语句连成一行。这条宏定义将被替换到宏名称在程序中被引用的地方。第1行定义了宏的名称,也就是宏函数名称get_seg_byte(seg,addr)。第3行定义了一个寄存器变量_
_res。第4行上的_ _asm_
_表示嵌入式语句的开始。从第4行到第7行的4条AT&T格式的汇编语句。 第8行是输出寄存器,这句的含义是在这段代码运行结束后将eax所代表的寄存器的值放入_
_res变量中,作为本函数的输出值,“=a”中的“a”称为加载代码,“=”表示这是输出寄存器。第9行表示在这段代码开始运行时将seg放到eax寄存器中,“”表示使用与上面同个位置的输出相同的寄存器。而(*(addr))表示一个内存偏移地址值。为了在上面汇编语句中使用该地址值,嵌入式汇编程序规定把输入和输出寄存器统一按顺序编号,顺序是从输出寄存器序列从左到右从上到下以“%0”开始,分别记为%0、%1、……%9。因此,输出寄存器的编号是%0(这里只有一个输出寄存器),输出寄存器前一部分(””(seg))的编号是%1,而后部分的编号是%2。上面第6行上的%2即代表(*(addr))这个内存偏移量。
现在我们来研究4~7行上的代码的作用。第一句将fs段寄存器的内容入栈;第二句将eax中的段值赋给fs段寄存器;第三句是fs:(*(addr))所指定的字节放入al寄存器中。当执行完汇编语句后,输出寄存器eax的值将被放入_
_res,作为该宏函数的返回值。
01 asm("cld\n\t"02 "rep\n\t"03 "stol"04 :
05 :
"c"(count-1), "a"(file_value),
"D"(dest)06 :
"�x", "�i");
1~3行这三句是通常的汇编语句,用以清方向位,重复保存值。第4行说明这段嵌入汇编程序没有用到输出寄存器。第5行的含义是:将count-1的值加载到ecx中,dest放到edi中。为什么要让gcc编译程序去做这样的寄存器值的加载,而不让我们自己做呢?因为gcc在它进行寄存器分配时可以进行某些优化工作。例如fill_value值可能已经在eax中。如果是在一个循环语句中的话,gcc就可能在整个循环操作中保留eax,这样就可以在每次循环中少用一个movel语句。最后一行的作用是告诉gcc这些寄存器中的值已经改变了。
通过上面分析,我们知道,宏名称中的seg代表一指定的内存段值,而addr表示一内存偏移地址量。到现在为止,我们应该很清楚这段程序的功能了吧!该宏函数的功能是从指定段和偏移值的内存地址处取一个字节。再看下一个例子。