最近在学习系统调用,一段用asm内联汇编写的简单程序始终得不出正确的系统调用结果。经过提醒,我才了解到这是32位平台和64位平台的系统调用方法不同的原因。在此列出相关的程序和我的理解。
首先看如下一段简单的C程序(test.cpp
):
#include
int main(){
char str[] = "Hello\n";
write(0, str, 6);
return 0;
}
这段程序调用了write函数,其接口为:
int write(int fd /*输出位置句柄*/, const char* src /*输出首地址*/, int len /*长度*/)
fd为0则表示输出到控制台。因此上述程序的执行结果为:向控制台输出一个长度为6的字符串"Hello\n"
。
在控制台调用gcc test.cpp
,可以正确输出。
为了更好地理解在汇编代码下的系统调用过程,可把上述代码改写成内联汇编的格式(具体语法可参考上一篇博客:用asm内联汇编实现系统调用):
//test_asm_A.cpp
int main(){
char str[] = "Hello\n";
asm volatile(
"int $0x80\n\t"
:
:"a"(4), "b"(0), "c"(str), "d"(6)
);
return 0;
}
其中,4是write函数的系统调用号,ebx/ecx/edx是系统调用的前三个参数。
然而,执行gcc test_asm_A.cpp
编译后,再运行程序,发现程序没有任何输出。一个很奇怪的问题是,如果采用如下test_asm_B.cpp
的写法,则程序可以正常地输出:
//test_asm_B.cpp
#include
#include <
int main(){
char *str = (char*)malloc(7 * sizeof(char));
strcpy(str, "Hello\n");
asm volatile(
"int $0x80\n\t"
:
:"a"(4), "b"(0), "c"(str), "d"(6)
);
free(str);
return 0;
}
两段代码唯一的区别,是test_asm_A.cpp
中的str
存储在栈空间,而test_asm_B.cpp
中的str
存储在堆空间。
那么,为什么存储位置的不同会造成完全不同的结果呢?
经过提醒,将上述代码用32位的方式编译,即gcc test_asm_A.cpp -m32
和gcc test_asm_B.cpp -m32
,可以发现两段代码都能正确输出。这说明,上述代码按32位编译,可以得到正确的结果。
如果没有-m32
标志,则gcc默认按照64位方式编译。32位和64位程序在编译时有如下区别:
int $0x80
进入系统调用,将系统调用号传入eax
,各个参数按照ebx
、ecx
、edx
的顺序传递到寄存器中,系统调用返回值储存到eax
寄存器。syscall
进入系统调用,将系统调用号传入rax
,各个参数按照rdi
、rsi
、rdx
的顺序传递到寄存器中,系统调用返回值储存到rax
寄存器。再看上面两段代码,它们都是调用int $0x80
进入系统调用,却按照64位方式编译,则会出现如下不正常情形:
再看程序中传入的各个参数,系统调用号(4),第1个和第3个参数(0和6)都是32位以内的,但是str的地址是64位地址,在0x80系统调用中只有低32位会被考虑。
这样,test_asm_A.cpp
不能正确执行,而test_asm_B.cpp
可以正确执行的原因就很明确了:
test_asm_A.cpp
中,str存储在栈空间中,而栈空间在系统的高位,只取低32位地址,得到的是错误地址。test_asm_B.cpp
中,str存储在堆空间中,而堆空间在系统的低位开始,在这样一个小程序中,str地址的高32位为0,只有低32位存在非零值,因此不会出现截断错误。可见,test_asm_B.cpp
正确执行只是一个假象。由于堆空间从低位开始,如果开辟空间过多,堆空间也进入高位的时候,这段代码同样可能出错。
最后,给出64位系统下可正确输出的asm系统调用代码:
//test_asm_C.cpp
int main(){
char str[] = "Hello\n";
//注意:64位系统调用中,write函数调用号为1
asm volatile(
"mov %2, %%rsi\n\t"
"syscall"
:
:"a"(1), "D"(0), "b"(str), "d"(6)
);
return 0;
}