Linux asm系統調用:32位和64位的區別

最近在學習系統調用,一段用asm內聯彙編寫的簡單程序始終得不出正確的系統調用結果。經過提醒,我才瞭解到這是32位平臺和64位平臺的系統調用方法不同的原因。在此列出相關的程序和我的理解。

程序代碼和問題

首先看如下一段簡單的C程序(test.cpp):

#include <unistd.h>
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 <stdlib.h>
#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存儲在堆空間。

那麼,為什麼存儲位置的不同會造成完全不同的結果呢?

原因分析

位的方式編譯,即gcc test_asm_A.cpp -m32和gcc test_asm_B.cpp -m32,可以發現兩段代碼都能正確輸出。這說明,上述代碼按32位編譯,可以得到正確的結果。 如果沒有-m32標誌,則gcc默認按照64位方式編譯。32位和64位程序在編譯時有如下區別:

  • 32位和64位程序的地址空間範圍不同。
  • 32位和64位程序的系統調用號不同,如本例中的write,在32位系統中調用號為4,在64位系統中則為1。
  • 對於32位程序,應調用int $0x80進入系統調用,將系統調用號傳入eax,各個參數按照ebx、ecx、edx的順序傳遞到寄存器中,系統調用返回值儲存到eax寄存器。
  • 對於64位程序,應調用syscall進入系統調用,將系統調用號傳入rax,各個參數按照rdi、rsi、rdx的順序傳遞到寄存器中,系統調用返回值儲存到rax寄存器。

再看上面兩段代碼,它們都是調用int $0x80進入系統調用,卻按照64位方式編譯,則會出現如下不正常情形:

  • 程序的地址空間是64位地址空間。
  • 0x80號中斷進入的是32位系統調用函數,因此仍按照32位的方式來解釋系統調用,即所有寄存器只考慮低32位的值。

再看程序中傳入的各個參數,系統調用號(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位系統的系統調用代碼

最後,給出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;
}