Linux内嵌汇编
基本内嵌汇编
1、GNU的C编译器使用asm关键字指出汇编语言编写的源代码段落。
2、在基本asm格式,汇编代码通过C全局变量名称整合输入值和输出值。
asm(“assembly code”)
- 指令必须在引号内;
- 如果指令超过一条,必须使用新行分隔符分隔汇编语言代码的每一行;
- asm段不能使用局部变量,数据变量必须声明为全局的(代码示例中的a,b是声明的全局变量);
- volatile修饰符,表示不希望编译器优化该内嵌汇编段。
代码示例:
asm volatile(“movl a, %edi\n\t”
"movl b ,%esi\n\t"
"movl $1, %eax\n\t"
"movl $0, %ebx\n\t"
"int $0x80");
3、如果使用ANSI C编写代码
- 关键字__asm__替代asm
- 关键字__volatile__替代volatile
扩展asm格式
asm(“assembly code” : output locations : input operands : changed registers);
- 汇编代码:使用和基本asm格式相同的语法内联汇编代码
- 输出位置:包含内联汇编代码的输出值的寄存器和内存位置的列表
- 输入操作数:包含内联汇编代码的输入值的寄存器和内存位置的列表
- 改动的寄存器:内联代码改变的任何其他寄存器的列表
指定输入和输出值
1、输入值和输出值列表格式:
"constraint"(variable)
其中variable是程序声明的C变量,在扩展asm格式中,局部和全局变量都可以使用。constraint定义把变量存放哪里(对于输入值)或者从哪里传送变量(对于输出值),使用它定义把变量存放在寄存器还是内存位置中。
2、约束是单一字符的代码,约束代码如下表所示
约束 | 描述 |
---|---|
a | 使用%eax,%ax或者%al寄存器 |
b | 使用%ebx,%bx或者%bl寄存器 |
c | 使用%ecx,%cx或者%cl寄存器 |
d | 使用%edx,%dx或者%dl寄存器 |
S | 使用%esi或者%si寄存器 |
D | 使用%或者%di寄存器 |
r | 使用任何可用的通用寄存器 |
q | 使用%eax,%ebx,%ecx或者%edx寄存器之一 |
A | 对于64位值使用%eax和%edx寄存器 |
f | 使用浮点寄存器 |
t | 使用第一个(顶部)浮点寄存器 |
u | 使用第二个浮点寄存器 |
m | 使用变量的内存位置 |
o | 使用偏移内存位置 |
V | 只使用直接内存位置 |
i | 使用立即整数值 |
n | 使用值已知的立即整数值 |
g | 使用任何可用的寄存器或者内存位置 |
3、输出值还包含一个约束修饰符,指示编译器如何处理输出值,如表所示
输出修饰符 | 描述 |
---|---|
+ | 可以读取和写入操作数 |
= | 只能写入操作数 |
% | 如果必要,操作数可以和下一个操作数切换 |
& | 在内联函数完成之前,可以删除或者重新使用操作数 |
案例:
把C变量data1存放到EDX寄存器中,把data2存放到ECX寄存器中,内联汇编代码结果存放在EAX寄存器中,然后传送给变量result
asm("assembly code" : "=a"(result) : "d"(data1), "c"(data2));
使用寄存器
1、如果输入值和输出值被赋值给寄存器,那么内联汇编代码可以像平常一样使用寄存器,在扩展asm格式中,汇编代码引用寄存器,必须使用两个百分号符号(%%)。
案例:
asm("imull %%edx, %%ecx\n\t"
"movl %%ecx, %%eax"
: "=a"(result)
: "d"(data1), "c"(data2));
使用占位符
2、扩展asm格式提供占位符(placeholder),可以在内联汇编代码中使用它引用输入值和输出值。占位符是前面加上百分号符号(%)的数字。按照内联汇编代码列出的每个输入值和输出值在列表中的位置,每个值被赋予一个从零开始的数字,然后就可以在汇编代码中使用占位符表示值。
案例:
asm("assembly code"
: "=r"(result)
: "r"(data1), "r"(data2));
- %0将表示包含变量值result的寄存器
- %1将表示包含变量值data1的寄存器
- %2将表示包含变量值data2的寄存器
3、必须把输入值和输出值声明为内联代码中汇编指令需要的正确存储元素(寄存器或者内存)。
案例:
asm("imull %1, %2\n\t"
"movl %2, %0"
: "=r"(result)
: "r" (data1), "r"(data2));
引用占位符
1、如果内联汇编代码中的输入值和输出值共享程序中相同的C变量,可以指定使用占位符作为约束值。
案例:
0标记通知编辑器使用第一个命名的寄存器存放输出值data2
asm("imull %1, %0"
: "=r"(data2)
: "r"(data1), "0"(data2));
替换占位符
1、GNU编译器允许声明替换的名称作为占位符,替换的名称在声明输入值和输出值的段中定义,格式如下:
%[name]“constraint”(variable)
案例:
使用替换的占位符名称的方式和使用普通的占位符相同。
asm("imull %[value1],%[value2]"
:[value2] "=r"(data2)
:[value1] "r"(data1), "0"(data2));
改动的寄存器列表
1、编译器假设输入值和输出值使用的寄存器会被改动,并且相应做出处理,不需要在改动的寄存器列表中包含这些值,否则,会产生错误信息。
2、如果内联汇编代码使用了没有初始化地声明为输入值或者输出值的任何其他寄存器,则要通知编译器,以便避免使用它们。
案例:
内联汇编代码使用EAX寄存器作为存储数据值的中间位置,没有声明为输入值或者输出值。所以必须在改动寄存器列表中包含它。
asm("movl %1, %%eax\n\t"
"addl %%eax, %0"
: "=r"(result)
: "r"(data1), "0"(result)
:"%eax");
3、如果在内联汇编代码之内使用了没有在输入值或输出值中定义的任何内存位置,那它必须被标记为被破坏的。在改动的寄存器列表中使用memory通知编译器这个内存位置在内联汇编代码中被改动。
使用内存位置
1、约束m用于引用输入值和输出值中的内存位置,也可以直接使用C变量的内存位置。
案例:
asm段把被除数的值加载到EAX寄存器中(DIV指令需要),除数存放在内存位置中。确定结果后,它被传送到堆栈中的内存位置中,而不是寄存器中。
asm("divb %2\n\t"
"movl %%eax, %0"
: "=m"(result)
:"a"(dividend), "m"(divisor));
处理跳转
1、内联汇编代码也可以包含定义其中位置的标签,实现一般的汇编条件分支和无条件分支,跳转到定义的标签。
案例:
asm("cmp %1, %2\n\t"
"jge greater\n\t"
"movl %1, %0\n\t"
"jmp end\n"
"greater:\n\t"
"movl %2, %0\n"
"end:"
: "=r"(result)
: "r"(a), "r"(b));
内联汇编代码使用标签有两个限制:
- 只能跳转到相同的asm段内的标签,不能从一个asm段跳转到另一个asm段中的标签。
- asm汇编段中的标签不能与其他汇编段的标签相同,因为内联汇编标签会被编码到最终的汇编代码中。
2、条件分支和无条件分支允许指定一个数字加上方向标志作为标签,方向标志指出处理器应该向那个方向查找数字型标签。
案例:
标签被替换为0: 和1:,JGE和JMP指令使用修饰符 f 指出从跳转指令向前查找标签。
asm("cmp %1, %2\n\t"
"jge 0f\n\t"
"movl %1, %0\n\t"
"jmp 1f\n"
"0:\n\t"
"movl %2, %0\n"
"1:"
: "=r"(result)
: "r"(a), "r"(b));
使用内联汇编代码
C宏函数
1、宏函数定义输入值和输出值,然后定义处理输入值并且生成输出值的函数,格式如下
#define NAME(input values, output values) (function)
2、输入值用逗号分隔用作函数输入变量的列表。
案例:
宏函数可能非常长的代码行,可以使用行继续字符(反斜线)分隔函数。
#define SUM(a, b, result) \
((result) = (a) + (b))
3、宏函数定义的变量完全独立于程序定义的结果变量,可以在宏函数中使用任何变量。
创建内联汇编宏函数
1、和C宏函数一样,可以声明包含内联汇编代码的宏函数。
案例:
JGE和JMP指令使用数字型标签,以便可以再程序中多次使用宏函数而不会出现重复的汇编标签
#define GREATER(a, b, result) ({ \
asm("cmp %1, %2\n\t" \
"jge 0f\n\t" \
"movl %1, %0\n\t" \
"jmp 1f\n" \
"0: \n\t" \
"movl %2, %0\n" \
"1:" \n
: "=r"(result) \
: "r"(a), "r"(b)); \
})
2、asm语句必须使用一对花括号,以便指出语句的开头和结尾,否则,每次在C代码中使用宏时,编译器都会产生错误信息。
创建汇编函数
被调用的函数必须保留EBX、EDX、ESI、EBP和ESP寄存器。
寄存器 | 状态 |
---|---|
EAX | 用于保存输出值,但是可能在函数返回之前被修改 |
EBX | 用于指向全局偏移表 |
ECX | 在函数中可用 |
EDX | 在函数中可用 |
EBP | C程序使用它作为堆栈基址指针,必须保留 |
ESP | 在函数中用于指向新的堆栈位置,必须保留 |
EDI | C程序使用它作为局部寄存器,必须保留 |
ESI | C程序使用它作为局部寄存器,必须保留 |
ST(0) | 用于保存浮点输出值,但是可能在函数返回之前被修改 |
ST(1)~ST(7) | 在函数中可用 |
C函数调用的汇编语言函数基本模板
这个模块可用C或C++韩式使用的汇编语言函数。
.section .text
.type func, @function
func:
pushl %ebp
movl %esp, %ebp
subl $12, %esp
pushl %edi
pushl %esi
pushl %ebx
<function code>
popl %ebx
popl %esi
popl %edi
movl %ebp, %esp
popl %ebp
ret
在C程序中使用汇编函数
使用整数返回值
最基本的汇编语言函数调用把32位整数值返回到EAX寄存器中,调用函数获取该值,并把返回值作为整数赋值给C变量,对于64位的长整数值,返回存放在EDX:EAX寄存器对中。
使用字符串返回值
返回字符串的函数返回指向字符串存储位置的指针,调用这个函数的C或C++程序必须使用指针变量保存返回值,然后通过指针访问字符串。
使用浮点返回值
C样式的函数不使用EAX寄存器,而是使用ST(0)寄存器在函数之间交换浮点值,函数把返回值存放在FPU堆栈中,然后调用程序负责把返回值弹出堆栈并且把它赋值给变量。
在C++程序中使用汇编函数
1、在C++程序中使用汇编语言函数的规则几乎和在C程序中使用规则相同。
2、C++程序中使用的所有函数都是使用C++样式的命名和调用约定,但是汇编语言函数使用C语言的调用约定,因此,必须通知编译器使用的哪些函数时C函数,通过extern语句完成,
案例:
extern "C"
{
<code block>
}
调试汇编函数
1、如果希望调试器进入C程序内的汇编语言函数中,就必须单独对函数进行汇编,并且在汇编器生成的目标代码文件中包含调试信息,然后把生成的目标文件和C程序编译在一起。
$as -gstabs -o square.o square.s
$gcc -gstabs -o inttest inttest.c square.o