- 混合编程
- 内联汇编
- 扩展内联汇编
- 寄存器约束
- 内存约束
- 参考文献
混合编程写在前面:
之前都是讲的汇编理论,今天将汇编应用在实战之中。主要有两种方式:混合编程和内联汇编。
混合编程就是单独的汇编文件和单独的C语言文件分别编译成目标文件之后,一起链接成可执行文件。
假设我们要写一个打印函数,既然是分别编译那么肯定是分两部分了。C语言部门相信大家都有基础,就不做多解释了,直接贴代码:
extern void asm_print(char *, int);
void c_print(char* str)
{
int len = 0;
while(str[len++])
;
asm_print(str, len);
}
值得注意的是,这里我们从外部引用了一个asm_print函数了,这个函数就在汇编文件中定义。其主要原理是触发0x80中断调用 4 号子功能号write将输出定位到屏幕stdout。
注:如何进行系统调用
Linux要触发0x80中断需要用到int 0x80这个指令,那么怎么去指定我要调用哪个子功能号呢?这个时候需要利用寄存器eax传入子功能号了,当然这个是在触发中断之前了。有些子功能号需要参数,记得在触发中断之前,通过寄存器传入参数。mov eax,4 int 0x80下表附上Linux调用号大全,图源来自虫虫搜奇
我们可以看到asm_print这个函数有两个参数,一个是打印字符串的地址,一个额是字符串的长度。这两个参数都是我们需要压入栈的:
mov eax,4 ;调用write
mov ebx,1 ;输出指向stdout
mov ecx,[ebp+4] ;获取地址
mov edx,[ebp+8] ;获取字符串长度
int 0x80
基本有技术的我们都讲完了,下面正式上代码,文件名叫C_with_S_S.S:
section .data
str: db "hello Gos!",0xa,0 ;结尾添加' '
str_len equ $-str
section .text
extern c_print
global _start
_start:
;c调用约定
push str ;传参
call c_print ;调用哈数
add esp,4 ;回收栈空间
;退出程序
mov eax,1 ;1子功能号是exit
int 0x80
global asm_print
;模拟C库函数write
asm_print:
push ebp
mov ebp,esp
mov eax,4 ;调用write
mov ebx,1 ;输出指向stdout
mov ecx,[ebp+8]
mov edx,[ebp+12]
int 0x80
pop ebp
ret
这里我们可以看到程序入口_start在汇编文件中定义。之后将参数压栈,指向C语言中函数c_print,而C语言中又反过来调用汇编文件中真正打印的函数asm_print。最后执行exit系统调用退出程序。
代码已经写完了,我们现在来编译一下:
[ik@bogon kernel]$ gcc -m32 -c -o C.o C_with_S_c.c [ik@bogon kernel]$ nasm -f elf -o S.o C_with_S_S.S [ik@bogon kernel]$ ld -m elf_i386 -o test.bin C.o S.o [ik@bogon kernel]$ chmod +x test.bin [ik@bogon kernel]$ ./test.bin
最终执行结果也正如我们所料,打印出了hello,Gos!
[ik@localhost C_with_S]$ ./test.bin hello Gos!内联汇编
内联汇编即在C语言中嵌入汇编代码,直接编译成可执行文件。而这正是利用了gcc强大的asm机制在代码中直接嵌入汇编代码,所以称为gcc inline assembly。
内联汇编最基本的内联形式格式为:
asm [volatile]("汇编代码")
注:
asm 和 __asm__是一样的,是gcc定义的宏;同理的是volatile。#define __asm__ asm #define __volatile__ volatile
这里要注意几个规则:
- 无论有多少条指令,指令必须在双引号之中
- 如果指令过多,可以用接续符
- 指令之间用分号或者换行符进行分割
所以,刚刚的asm_print函数等同于如下代码:
int count = 1;
char *str = "hello world!n";
void main()
{
asm("push %rax;
push %rbx;
push %rcx;
push %rdx;
movl $4,%eax;
movl $1,%ebx;
movl str,%ecx;
movl $13,%edx;
int $0x80;
mov %eax,count;
pop %rax;
pop %rbx;
pop %rcx;
pop %rdx;
");
}
这个直接编译就可以了运行了:
[ik@localhost C_with_S]$ ./inline.bin hello world!扩展内联汇编
可以看到,为了不破坏其他函数正在寄存器中保存的数据,我们必须手动备份寄存器,太累了。这就有了扩展内联汇编,其格式如下:
asm [volatile]("汇编代码":output:input:clobber/memory)
寄存器约束
我们先不管最后的clobber/memory,前面多加了一个输入input和输出output。
输入的就是我们要压入的参数,其需要遵守寄存器约束:
a:表示寄存器eax b:表示寄存器ebx c:表示寄存器ecx d:表示寄存器edx D:表示寄存器edi S:表示寄存器esi q:表示以下任意四个寄存器之一:eax/ebx/ecx/edx r:表示任意六个通用寄存器之一:eax/ebx/ecx/edx/edi/esi g:表示可以存放到任意地点 A:把eax和edx组合成64位数 f:表示浮点寄存器 t:表示第一个浮点寄存器 u:表示第二个浮点寄存器
那么我们先不管输出,但看输入,以上代码就可以改写成如下:
asm("int $0x80"
: output
: "a"(4), "b"(1), "c"(str), "d"(13));
这里相当于把 4 传给eax,1 传给 ebx,str 传给 str,d 传给 edx;之后调用int $0x80。gcc会帮我们备份好这四个寄存器,不会干扰到其他函数的运行。
注:
基础内联汇编中用单个%前缀修饰寄存器,而在扩展编程需要用两个%,这个原因与后面的内存约束有关系
刚刚讲了输入,我们现在来讲一下输出,输出与输入相比会多一个=,比如说下面便表示count = %eax
"=a"(count)
所以上面的代码我们可以写成如下形式:
asm("int $0x80"
: "=a"(count)
: "a"(4), "b"(1), "c"(str), "d"(13));
而除了=之外,output还有三种:
- =:表示赋值
- +:表示可读写,其修饰的寄存器作为输入,也作为输出
- &:表示此寄存器独占,其他的不许用
刚刚我们也提到内存约束。内存约束时要求gcc直接将位于input和output中的C变量的内存地址作为内联汇编代码的操作数,不需要寄存器做中转。其约束如下:
m:表示操作数可以使用任意一种内存形式 o:操作数作为内存变量,但访问时通过偏移量的形式访问
下面的例子很好的展示了内存约束的作用:
#includeusing namespace std; int main() { int a = 1; int b = 2; asm("movl %%eax,%1" ::"a"(a), "m"(b)); cout << b << endl; }
[ik@localhost C_with_S]$ ./mem.bin 1
参考文献注:
这里的%1表示第一个参数是b,相当于代码 a = b 的效果。
[1] 操作系统真相还原



