本篇文章是基于C和汇编的
首先采用调用0x80中断的方式进行系统调用:
void prints(char *str){
int i = 0;
while(str[i] != '\0'){
i++;
}
asm("movl $4, %%eax \n\t"
"movl $1, %%ebx \n\t"
"movl %0, %%ecx \n\t"
"movl %1, %%edx \n\t"
"int $0x80 \n\t"
::"m"(str), "m"(i)
);
return;
}
int main(){
char *str1 = "ab"; //字符串内容存储在堆上
char str2[] = "ab"; //字符串内容存储在栈上
prints(str1); //正常显示
prints(str2); //不显示
}
这里的关键在于Linux系统0x80号中断是32位系统调用。其接收4个参数,寄存器ecx中的参数为要显示的内容的内存首地址,而在64位系统下用gcc编译,生成的程序默认是64位的,64位程序的内存地址(也就是C语言中的指针)是64位的,我们把内存地址传给ecx,ecx只有32位,所以地址的高32位会丢失,系统按照错误的地址去寻址,肯定找不到内容。
那么为何堆上的数据可以显示,段上的不行呢,因为Linux系统在给程序分配内存空间的时候,堆的内存位置位于低内存地址,地址虽然是64位的,但是高32位全为0;而堆栈却位于高内存地址,高32位不为零,因此只能正确寻找到堆上的数据。
0x80中断传入不正确的地址会怎样:并不会报错,没有任何提示,但是会返回一个错误码(负数),存在eax中,具体错误码可以在头文件中查看(头文件中是正数):
/usr/include/asm-generic/errno-base.h
/usr/include/asm-generic/errno.h
通过echo $?
可以看到程序返回242(0xF2),也就是-14(负数代表调用出错,绝对值是错误码;若正确调用会返回正确输出的字符数)。查看errno-base.h中定义:
#define EFAULT 14 /* Bad address */
和上面分析的一样:传入了错误的内存地址。
实际上printf正确调用也会返回写入(标准输出)的字符数(返回值用eax寄存器传递),错误调用返回负数,这点和系统调用的返回值类似。
我们怎么修改我们的程序呢?64位程序不再用int $0x80
进行系统调用,可以考虑用syscall
指令,这是64位系统的ABI调用方式,系统调用号和0x80不同,参数传递的寄存器约定也不一样。
详情可以查看:http://blog.rchapman.org/posts/Linux_System_Call_Table_for_x86_64/
修改后的程序:
void prints(char *str){
long i = 0; //这里也可以用int,因为用int后面会编译为`movl ..., %edx`,这个语句会把edx高32位清零。
while(str[i] != '\0'){
i++;
}
asm("syscall \n\t"::"a"(1), "D"(1), "S"(str), "d"(i));
}
int main(){
char *str = "ab\n";
char str1[] = "cd\n";
prints(str);
prints(str1);
}
syscall参数寄存器约定:
系统调用功能号:rax
参数列表按顺序分别是:rdi、rsi、rdx、r10、r8、r9
普通函数参数寄存器约定:
参数列表按顺序分别是:rdi、rsi、rdx、rcx、r8、r9