背景
我们知道linux提供了很多系统调用给应用程序使用,但linux并没有将这些系统调用封装成C语言接口,而是提供一个软中断,然后应用程序需要通过软中断带上系统调用编号和参数去调用接口,这对于应用程序相当的不友好。这时候libc库便登上了历史舞台,它将这些系统调用封装成一个个函数,形象生动地提供给应用程序使用,从此之后,应用程序只需要调用那些write(), read(), exit()等便能达到目的,而不是通过软中断去使用这些接口。。
然后,当我们熟悉了这些符号接口的时候,却不知道背后是怎么工作的,所以今天我们就来搞事情!我们将干掉LIBC,直接调用系统的接口,看看怎么通过软中断去调用这些接口。
代码
//#include <unistd.h>
char* str = "Hello World\n";
void print()
{
asm( "movl $12,%%edx \n\t"
"movl %0,%%ecx \n\t"
"movl $0,%%ebx \n\t"
"movl $4,%%eax \n\t"
"int $0x80 \n\t"
::"r"(str):"edx","ecx","ebx");
}
void main()
{
//write(0, str, 12);
print();
}
我们选择的write接口看起来很简单哦,哈哈
注意:这段代码只能在linux i386(32位)版本上编译运行,x64我还不知道怎么玩 :=(
分析
write函数原形如下:
ssize_t write (int fd, const void * buf, size_t count);
它有3个参数(文件句柄、写入缓冲区,写入字节数),这3个参数分别传递给ebx, ecx, edx寄存器。
我们来逐行看汇编代码:
movl $12,%%edx \n\t
解析: 将数字12直接赋值给寄存器edx,12是字符串“Hello World\n”的长度。$符号可以直接引用数字,%%引用寄存器。 \n\t用于换行,如果汇编语句比较多,可以用它来换行。
movl %0,%%ecx \n\t
解析: 这行的含义是将字符串指针赋值给ecx寄存器。%号是引用C语言输入的内容,%0表示第0个(最多9个)输入或输出的内容,这里就是str字符串。
movl $0,%%ebx \n\t
解析:直接将0赋值给ebx寄存器,这里是write函数的第一个参数——文件句柄,0是标准输出。
movl $4,%%eax \n\t
解析:这行是设置中断号,write系统调用的中断号是4,存储到eax寄存器中。
int $0x80 \n\t
解析:这行就简单啦,调用系统的软中断,搞定。
::"r"(str):"edx","ecx","ebx"
解析:最后一行费解一点!我们先来看下AT&T汇编语言的基本形式
可以看出,一段汇编代码分为4个部分,每个部分之间用“:”隔开,如果中间那个部分没有语句,也必须用“:”隔开,但如果最后那部分没有语句,可以不用增加“:”
- instruction(指令).每条指令之后最好使用"\n\t"结尾,这样在gcc产生汇编格式比较好
- output operand(输出操作数),每个输出部分使用,分隔."="作为修饰符,"m"表示存放位置/约束符,()里面表示对应C程序值。
- input operand(输入操作数),这个部分和输出是一样的。
- clobber(
不知道怎么翻译),这个部分是告诉gcc在这条指令里面我们会修改什么值。
套用这套形式,可以看出,第一个“:”前也就是int $0x80 及之前的语句是指令;
第二个“:”和第一个“:”之间是空的,说明我们的输出操作数是空的;
第三个“:”和第二个“:”之间是输入操作数,这里我们将str指针作为参数输入给了“r”寄存器,在指令中我们可以通过%0~%9来引用输出/输入的值。输入/输出按从左到右,从上到下的规则匹配0~9;我们在前面的指令中使用%0引用了str指针,并把它的值赋给了ecx寄存器。
第三个“:”之后告诉gcc我们修改了什么,这里我们修改了ebx,ecx,edx寄存器
具体的汇编语法请参考下面的链接!到此我们应该理解透了write系统调用怎么调了,所以我们是不是可以翻一下LIBC的其他函数,再搞搞事情,哈哈~
参考资料
AT&T汇编语言语法:GCC内嵌汇编 (dirtysalt.github.io)