Gcc内联汇编2

Contents
1. 介绍 3
2. 概要 3
3. GCC汇编格式 3
1) 源操作数和目操作数方向 3
2) 寄存器命名 4
3) 立即数 4
4) 操作数大小 4
5) 内存操作数 4
4. 基本形式内联汇编 4
5. 扩展形式内联汇编 5
5.1 汇编模板 6
5.2 操作数 6
5.3 Clobber List 7
5.4 Volatile… 8
6. 深入constras 8
6.1 常用constras 8
6.2 constra修改标记 10
7.常用窍门技巧 10
8.结束语 13
9. 参考文献 13



1. 介绍

[主要是版权/反馈/勘误/感谢等信息没有翻译--译者注, 本文中方括号中都是译者注]


2. 概要

我们现在学习GCC内联汇编那么内联汇编到底是什么
[我们首先先来看看内联有什么好处]
我们可以让编译器将代码插入到者代码中指出在代码中具体什么位置被执行这种就是内联内联似乎很像个宏
确他们的间有很多相似的处

那么内联到底有什么好处呢

内联降低了开销[不仅仅节省堆栈] 如果某些实参相同那么返回值定是相同这就可能给编译器留下了简化空间返
回值相同了就不必把内联代码插入到者代码中[直接用这个返回值替换就好了]这样可以减少代码量视区别情况而
定声明个是内联使用关键字 inline

现在我们回到内联汇编上来内联汇编就是些汇编语句写成内联它方便快速对系统编程非常有用我们主要目标是
研究GCC内联基础格式和使用思路方法声明个内联汇编我们使用关键字 asm

内联汇编重要性首先体现在它操作C语言变量和输出值到C语言变量能力由于这些特性内联汇编常被用作汇编指
令和它C的间接口


3. GCC汇编格式

GCC (GNU Compiler for Linux) 使用AT&T UNIX汇编语法.这里我们将用AT&T汇编格式来写代码如果你不熟
悉AT&T汇编语法也没有关系下面将有介绍AT&T和Intel汇编语法有很多区别的处我将给出主要区别点

1) 源操作数和目操作数方向
AT&T和Intel汇编语法相反Intel语法中第个操作数作为目操作数第 2个操作数作为源操作数相反在AT&T语法中
第个操作数是源操作数第 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


4. 基本形式内联汇编

基本形式内联汇编格式非常直观:
asm("assembly code");
例如:
asm("movl %ecx, %eax"); /* 把 ecx 内容移动到 eax */
__asm__("movb %bh , (%eax)"); /* 把bh中个字节内容移动到eax 指向内存 */

你可能注意到了这里使用了 asm 和 __asm__关键字. 2者皆可这样如果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不知道你已经将寄存器内容改了这将给我们带来麻烦尤其在编译器作了些优化情况下如果不告诉
GCC编译器将认为寄存器中事实上已经被改掉了内容没有被改过将当作它没有被改过而继续执行 我们能做就是
不要使用这些带来其他附加影响语句或者当我们退出时候还原这些内容否则只有等待崩溃了这里提到这种情况
就是我们将要在下节中阐述扩展形式内联汇编




5. 扩展形式内联汇编

前面介绍基础形式内联汇编思路方法只涉及到嵌入汇编指令在高级形式中我们将可以指定操作数它允许我们指
定输入输出寄存器[内联使用这些寄存器作为存储输入输出变量]和中涉及到clobbered寄存器列表[clobbered
registers:内联汇编可能要改变其内容寄存器]也并不是定要要显式指明使用具体寄存器我们也可以把它留给
GCC去选择这样GCC还可能更好进行优化处理高级内联汇编基本格式如下:
asm ( assembler template
: output operands /* optional */
: input operands /* optional */
: list of clobbered registers /* optional */
);

其中assembler template包含汇编指令部分括号中每个操作数用C表达式常量串描述区别部分的间用冒号分开
相同部分中每个小部分用逗号分开操作数多少被限定为10或者由机器决定个最大值[这句话翻译不好贴出原文:
The total number of operands is limited to ten or to the maximum number of operands in any
instruction pattern in the machine description, whichever is greater.]如果没有输出部分但是有输入部分就
必须在输出部分的前连续写两个冒号
例如 :
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区分操作数和寄存器操作数只需要个%前缀
4) 在第 3个冒号后面clobbered register部分 %eax 介绍说明在内联汇编代码中将要改变eax中内容GCC不要用
他存储其他值

当这段代码执行结束后"b"值将会被改掉它被指定作为输出操作数换句话说在"asm"内部对b改动将影响到
asm外面.

下面我们将对各个部分分别进行详细讨论:

5.1 汇编模板
汇编模板部分包含嵌入到C中汇编指令格式如下:
每条指令放在个双引号内或者将所有指令都放着个双引号内每条指令都要包含个分隔符合法分隔符是换行符
(/n)或者分号用换行符时候通常后面放个制表符"/t"我们已经知道为什么使用换行符+制表符了[前面部分有解
释]其中访问 C操作数用%0,%1…等等

5.2 操作数
C语言表达式 [大多情况是C变量] 将作为"asm"内部使用操作数每个操作数都以双引号开始对于输出操作数还
要写个修改标志(=)constra和修改标志都放在双引号内接下来部分就是C表达式了[放在括号内].举例来说:
标准形式如下:
"constra" (C expression) [ 如: "=r"(result) ]
对于输出操作数还有个修改标志(=) constra主要用来指定操作数寻址类型 (内存寻址或寄存器寻址)也用来指明
使用哪个寄存器
如果有多个操作数的间用逗号分隔
在汇编模板中每个操作数都用数字引用[这些操作数]引用规则如下如果总共有n个操作数(包括输入输出操作数
)那么第个输出操作引用数字为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使用

6. 深入constras

此时你可能理解了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"指定使用用和第个输出相同寄存器就
是说输入变量应该只能放在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如何工作

8.结束语

这篇文章讲述了GCC内联汇编基础内容旦你理解了基础原则你自己步步看下去就没有什么困难了我们通过些例
子可以更好帮助我们理解些在内联汇编中常用特性

GCC内联是个很大主题这片文章要讲还远远不够但大多数我们提到语法都可以在官方文档GNU Assembler中看
到完整constra可以在GCC官方文档中找到

当然Linux内核大范围内使用了GCC内联因此我们可以从中找到各种各样例子这对我们很有帮助

你可能感兴趣的:(汇编,list,gcc,input,编译器,linux内核)