C与汇编混合编程
c文件与汇编文件
section .data
str: db "asm_print say hello world!", 0xa, 0 ;0xa为换行符,0为字符串结束标志
str_len equ $-str
section .text
extern c_print
global _start ;将符号导出为全局属性
_start:
;调用c代码中函数 c_print
push str ;传参
call c_print ;调用c函数
add esp, 4 ;回收栈空间
;退出程序
mov eax, 1
mov ebx, 0
int 0x80
global asm_print ;相当于 `asm_print(str, size)`
asm_print:
push ebp
mov ebp, esp
mov eax, 4 ;write 系统调用号
mov ebx, 1 ;文件描述符
mov ecx, [ebp+8] ;第1个参数
mov edx, [ebp+12] ;第2个参数
int 0x80 ;发起中断
pop ebp ;恢复ebp
ret
extern void asm_print(char*, int);
void c_print(char* str) {
int len = 0;
while (str[len++])
;
asm_print(str, len);
}
程序入口在汇编代码的 _start 函数
内联汇编
分为基础内联汇编和拓展内联汇编。使用AT&T语法。
问题:
- 汇编代码与c代码就寄存器等资源的使用冲突如何解决?
- 汇编代码如何使用c代码中定义的变量?
解决方法:
- 使用拓展内联汇编。提供一个模板,让编译器处理。
推荐使用纯汇编文件再链接
AT&T 风格汇编
指令名后加上了操作数大小后缀,1字节b、2字节w、4字节l
内存寻址差异:
Intel 语法,立即数就是普通数字,内存地址使用中括号包含
AT&T语法,立即数为$符加数字,内存地址就是普通数字
内存寻址格式:
segreg{段基址}:base_address+offset_address+index*size
Intel:segreg:base_address(offset_address, index, size)
AT&T:segreg:[base+index*size+offset]
各种寻址方式都可以使用这种格式的某部分进行表达
基本内联汇编
格式:asm [volatile] ("assembly code")
asm 与 volatile 都是gcc的关键字,使用 -O 参数可以指定优化级别,使用volatile关键字保证编译器不会修改代码。
汇编代码必须位于圆括号中,且要用双引号括起来。对于 assembly code
- 必须用小括号包裹,无论一条还是多条指令
- 一堆小括号不能跨行,如果跨行需要用 '\' 转义
- 指令之间用 '\n' '\r\n' 进行分割
即使指令分布在多个双引号中,gcc也会将其合并,所以除了最后一条指令必须有分隔符。
int main(int argc, char const *argv[])
{
asm("mov $9, %rax \n" "push %rax \n" "pop %rax");
return 0;
}
char* str = "hello,world\n";
int count = 0;
void main() {
asm(
// "pusha \n"
"mov $4, %rax \n"
"mov $1, %rbx \n"
"mov str, %rcx \n" // 书上的例子,但编译时提示无法使用 str 变量。 ???
"mov $12, %rdx \n"
"int $0x80 \n"
"mov %rax, count \n"
// "popa"
);
}
扩展内联汇编(32位)
asm [volatile] ("asembly code" : output : input : clobber/modify)
括号内分为4部分,assembly code、output、input、clobber/modify。其中任何一个部分都可以省略。省略一个部分时保留:占位,如果省略的是后面一个或多个连续的部分,分隔符也不用保留。
- assembly code:汇编指令,和基本内联汇编一样
其中内联汇编用于帮c代码完成某些功能,C代码要为其提供参数和存放输出结果的空间。 - output:用于指定汇编代码的数据如何输出给C代码使用。汇编结束运行后,想将结果存储到C变量中,就使用此输出位置。
格式为:”操作数修饰符约束名称“(C变量名) 其中引号和圆括号不能少
操作数修饰符通常为“=”。多个操作数间用逗号“,”分隔 - input:用于指定C中数据如何输入给汇编。
格式为:"[操作数修饰符] 约束名"(C 变量名)
其中引号和圆括号不能少,操作修饰符为可选项。多个操作数间用逗号','分隔。 - clobber/modigy:汇编代码执行后会破坏一些内存或寄存器资源,通过这项通知编译器,可能造成寄存器或内存数据破坏,这样gcc就知道哪些寄存器或内存需要提前保护起来。
assembly code 中引用所有操作数是经过gcc转换过的复本,“原件”在c变量中。其中单个 % 用于表示占位符。
上述“要求”又叫“约束”,其将C代码中操作数映射为汇编中使用的操作数,实际就是描述C中操作数如何变汇编操作数。这些约束作用域是input和output部分。
约束分4大类:
- 寄存器约束
- 内存约束
- 立即数约束
- 通用约束
1. 寄存器约束
a:寄存器 eax/ax/al
b: ebx/bx/bl
c: ecx/cx/cl
d: edx/dx/dl
D: edi/di
S: esi/si
q: eax/ebx/ecx/edx
r: eax/ebx/edx/edx/esi/edi
g: 可以存放到任意地点(寄存器或内存)
A: 把eax和edx组合成64位整数
f: 表示浮点寄存器
t: 第1个浮点寄存器
u: 第2个浮点寄存器
#include <stdio.h>
int main() {
int in_a = 1, in_b = 2, out_sum = 0;
asm("add %%ebx, %%eax" : "=a"(out_sum) : "a"(in_a), "b"(in_b));
printf("sum is %d\n", out_sum);
return 0;
}
/*
其中寄存器前缀是两个%
对于输入变量,用约束名a为变量in_a指定了eax,为in_b指定了ebx
对于输出,使用约束名指定了eax,修饰符‘+’表示只写
*/
2. 内存约束
要求gcc将变量地址作为内联汇编的操作数,不用寄存器中转。
m: 表示操作数可用于任意一种内存形式
o: 操作数为内存变量,但访问它是通过偏移量的形式访问,即包含 offset_address 的格式
#include <stdio.h>
int main() {
int in_a = 1, in_b = 2;
printf("in_b is %d\n", in_b);
asm("mov %b0, %1" ::"a"(in_a), "m"(in_b));
printf("in_b now is %d\n", in_b);
return 0;
}
/*
作用是用in_a的值替换in_b
把 in_a 施加寄存器约束a,告诉gcc把变量 in_a 放到寄存器 eax 中,对 in_b 施加内存约束 m,告诉gcc把 in_b 的指针作为内联代码的操作数
“%1”为序号占位符,这里代表 in_b 的指针
“%b0”使用数据的低8位
*/
/*
0000000000001139 <main>:
1139: 55 push %rbp
113a: 48 89 e5 mov %rsp,%rbp
113d: 48 83 ec 10 sub $0x10,%rsp
1141: c7 45 fc 01 00 00 00 movl $0x1,-0x4(%rbp) # 变量 in_a
1148: c7 45 f8 02 00 00 00 movl $0x2,-0x8(%rbp) # 变量 in_b
114f: 8b 45 f8 mov -0x8(%rbp),%eax
1152: 89 c6 mov %eax,%esi
1154: 48 8d 05 a9 0e 00 00 lea 0xea9(%rip),%rax # 2004 <_IO_stdin_used+0x4>
115b: 48 89 c7 mov %rax,%rdi
115e: b8 00 00 00 00 mov $0x0,%eax
1163: e8 c8 fe ff ff call 1030 <printf@plt>
1168: 8b 45 fc mov -0x4(%rbp),%eax
116b: 88 45 f8 mov %al,-0x8(%rbp) # 内联汇编,将al写入in_b
116e: 8b 45 f8 mov -0x8(%rbp),%eax
1171: 89 c6 mov %eax,%esi
1173: 48 8d 05 96 0e 00 00 lea 0xe96(%rip),%rax # 2010 <_IO_stdin_used+0x10>
117a: 48 89 c7 mov %rax,%rdi
117d: b8 00 00 00 00 mov $0x0,%eax
1182: e8 a9 fe ff ff call 1030 <printf@plt>
1187: b8 00 00 00 00 mov $0x0,%eax
118c: c9 leave
118d: c3 ret
*/
3. 立即数约束
i: 整数
F: 浮点数
I: 0 ~ 31
J: 0 ~ 63
N: 0 ~ 255
O: 0 ~ 32
X: 任何类型立即数
4. 通用约束
0 ~ 9:用在input部分,表示可与output和input中第n个操作数用相同的寄存器或内存。
有时对寄存器没有要求,用哪个都可以。使用 r 作为约束,此时不知道gcc为操作数分配了哪个寄存器,或内存约束的操作数没有名称可用。
为了解决对操作数的引用,扩展内联汇编提供了占位符,用于代表约束指定的操作数(寄存器、内存、立即数),更多的是在内联汇编中使用占位符来引用操作数。分序号占位符和名称占位符两种。占位符使用 % 作前缀,寄存器使用 %% 作前缀。
1. 序号占位符
对output和input中操作数按照从左到右的次序从0开始编号,格式为 %0 ~ %9。注意,指代的是汇编中的操作数,而不是圆括号中的C变量。
例如:asm("add %2, %1":"=a"(out_sum): "a"(in_a), "b"(in_b));
"=a"(out_sum) 序号为 0,%0对应 eax
"a"(in_a), 序号为 1,%1对应 eax
"b"(in_b) 序号为 2,%2对应 ebx
在%和序号之间可以使用b指定使用0~7位例如 al,用 h 指定 8 ~ 15 例如 ah。
#include <stdio.h>
int main() {
int in_a = 0x12345678, in_b = 0;
asm("mov %1, %0" : "=m"(in_b) : "a"(in_a));
printf("double word in_b is 0x%x\n", in_b);
in_b = 0;
asm("movw %w1, %w0" : "=m"(in_b) : "a"(in_a));
printf("word in_b is 0x%x\n", in_b);
in_b = 0;
asm("movb %b1, %b0" : "=m"(in_b) : "a"(in_a));
printf("low byte in_b is 0x%x\n", in_b);
in_b = 0;
asm("movb %h1, %0" : "=m"(in_b) : "a"(in_a));
printf("high byte in_b is 0x%x\n", in_b);
return 0;
}
/*
$ gcc tmp.c && ./a.out
double word in_b is 0x12345678
word in_b is 0x5678
low byte in_b is 0x78
high byte in_b is 0x56
*/
2. 名称占位符
在output和input中使用 [名称]"约束名"(C 变量)
的方式为操作数显示起名,在汇编代码中使用 %[名称]
的形式使用。
#include <stdio.h>
int main() {
int in_a = 18, in_b = 3, out = 0;
asm("divb %[divisor] \n movb %%al, %[result]"
: [result] "=m"(out)
: "a"(in_a), [divisor] "m"(in_b));
printf("result is %d\n", out);
return 0;
}
// result is 6
无论使用哪种占位符,它都是指代C变量经过约束后、由gcc分配的对应于汇编代码中的操作符,与C变量本身无关。
约束中还有以下几种操作数类型修饰符,用来修饰所约束的操作数:内存、寄存器,分别在 output 和 input 中有以下几种。
output中有以下3种:
- = 表示操作数只写
- + 操作数可读写,告诉gcc其被先读入再被写入
- & 表示操作数要独占所分配的寄存器,只供 output 使用,任何 input 所分配的寄存器不能与此相同。当有多个修饰符时,&与约束名紧挨着。
input种
- % 该操作数可与下一个操作数互换
clobber/modify 部分
用于通知gcc修改了哪些寄存器或内存。在output和input种约束指定的寄存器gcc必然知道,所以需要在 clobber/modify 中通知的寄存器和内存不在 output 和 input 种出现过。
例如: asm("mov %%eax, %0 \n movl %%eax, %%ebx":"=m"(ret_value)::"bx");
一个寄存器可以使用 al / ax / eax / qax 。
如果修改了内存且没有直接在汇编语句中出现,要在 clobber/modigy 中 “memory”声明。另一个原因是清除寄存器缓存:内存中的变量在寄存器中有缓冲,可以在定义变量时使用 volatile 关键字解决此问题;在扩展内联汇编中使用“memory”。