第十一夜:在C语言中使用内联汇编

把这个部分作为单独的环节来复习,是因为内联汇编在安全工程师的C语言编程当中用处是比较广泛的。无论是开发shellcode,调试exp,或是对代码做免杀,做代码虚拟机(虽然我在这方面并不擅长,但是大概看过一些其实现方式)都需要用到内联汇编。

另外对于高手来说,巧妙运用C语言内联汇编,有时是提高程序运行效率的很好的方式,或是在开发程序时,对调试起到一定的帮助。

不过,要想熟悉内联汇编,首先要熟悉汇编。

内联汇编的基本格式

这里以Linux下,GCC作为编译器,AT&T汇编作为内联汇编格式:

简单内联汇编

1
__asm__("汇编语句")

拓展内联汇编

1
2
3
4
5
__asm__ ( 汇编语句
: 输出对象 (可选)
: 输入对象 (可选)
: 可能受影响的寄存器 (可选)
);

简单内联汇编和拓展内联汇编的区别是简单内联汇编只包括指令,而扩展内联汇编包括操作数。

如果希望确保编译器不会在asm内部优化指令,可以在asm后使用关键字volatile。如果程序需要与 ANSI C 兼容,则应该使用 __asm____volatile__,而不是 asm 和 volatile。

内联汇编的常见用法

通过内联汇编把a的值赋给b:

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>

void main(){
int a=10, b;
asm ("movl %1, %%eax;"
"movl %%eax, %0;"
:"=r"(b) /* 输出 */
:"r"(a) /* 输入 */
:"%eax" /* 破坏的寄存器 */
);
printf("%d",b);
}
  • 在本例中,b是输出操作数,由%0引用,a是输入操作数,由%1引用。
  • “r”是操作数的约束,它指定将变量a和b存储在寄存器中。注意,输出操作数约束应该带有一个约束修饰符”=”,指定它是输出操作数。
  • 要在asm内使用寄存器%eax,%eax的前面应该再加一个%,因为asm使用%0、%1等来标识变量。任何带有一个%的数都看作是输入/输出操作数,而不认为是寄存器。
  • 第三个冒号后的修饰寄存器%eax告诉将在asm中修改GCC %eax的值,这样GCC 就不使用该寄存器存储任何其它的值。
  • movl %1, %%eax将a的值移到%eax中, movl %%eax, %0将%eax的内容移到b中。
  • 因为b被指定成输出操作数,因此当asm的执行完成后,它将反映出更新的值。换句话说,对asm内b所做的更改将在asm外反映出来。

需要注意几点第一就是所有寄存器都使用%%作为前缀,第二在这个部分新增了%0%9的占位符来表示用户填充的数据。那么占位符,占的是什么位呢,谁来填充,怎么填充?%0%9的占位符会用输出部分和输入部分指定的寄存器或变量按照出现的顺序依次填充。如果不够填充则会出现编译错误的情况。

invalid 'asm': operand number out of range

https://www.it610.com/article/3871091.htm

使用内联汇编输出HelloWorld

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//编译方式:gcc -m32 -o test test.c
void main(){
char * s="Hello World\n";

asm ( "movl $4, %%eax\n\t" //eax包含系统调用号,调用0x80中断来执行这个函数
"movl $1, %%ebx\n\t" //ebx包含要写入的文件描述符,1表示当前终端
"movl %0, %%ecx\n\t" //ecx表示字符串的开头地址
"movl $12, %%edx\n\t" //edx是要输出的字符长度
"int $0x80\n\t"
: //输出值,为空表示没有输入值
:"m"(s) //输入参数,从%0~%9
: //"%eax", "%ebx", "%ecx", "%edx"
);
}

注意
在编写内联汇编时如果遇到以下报错:

error: invalid 'asm': operand number out of range

很有可能是输入输出的操作数出了问题。说明输入输出操作数超出了范围。

在本例中,如果将输入参数从”m”类型的内存参数,改为”r”类型的寄存器参数,则会报此错。

具体原因尚在研究中。今后在使用时需要多留心参数类型的区别。

另外以上程序再编译时需要采用32位架构,否则会无法运行(因为使用的全都是32位寄存器)。

内联汇编中使用多个输入参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/*编译:gcc -m32 -o test test.c */

void main(){
char * s1="Hello";
char * s2="World";
char * s3="\n";

asm volatile (
"pushl %0\n\t"
"call printf\n\t"

"pushl %1\n\t"
"call printf\n\t"

"pushl %2\n\t"
"call printf\n\t"
: //输出值,为空表示没有输入值
:"m"(s1), "m"(s2), "m"(s3)
:
);
}

总结

内联汇编还有很多的用法是需要琢磨的,也会随着对C语言理解的深入以及汇编的水平,探索出越来越高明的用法。

需要注意以下几点:

  1. asm内汇编代码分割符,参考资料[2]中提到说最好采用”\n\t”,不过我在使用”;”也没有遇到bug。
  2. asm的输入输出参数之间采用逗号分割,在汇编代码中调用是从%0开始编号的,从0%~9%
  3. 如果输出值为空,也需要用冒号占位

参考资料

1. Linux 中 x86 的内联汇编
2. 内联汇编基础学习

第十二夜:使用C语言探索堆栈 第十夜:C语言常见的Windows系统调用
Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×