C 語言宏 + 內聯彙編實現 MIPS 系統呼叫

RainbowC0發表於2024-04-12

目錄
  • 內聯彙編
  • 宏函式
  • 宏定義 Syscall 內聯彙編
  • 編譯測試

筆者最近作業要求練習 MIPS 彙編,熟悉 MIPS 彙編程式碼與 C 語言程式碼的對應關係。然而 SPIM/MARS 模擬器不能連結共享庫以呼叫外部函式(如 stdio.h 下的函式),只能透過系統呼叫實現。C 語言可以透過內聯彙編(Inline Assembly)實現系統呼叫而不借助任何外部函式,再將內聯彙編語句封裝成函式或宏函式,便於 C 程式呼叫。

內聯彙編

內聯彙編主要藉助關鍵字 asm__asm__ (C99) 實現。內斂彙編語句基本格式:

__asm__ [volatile](彙編語句[:[輸出結果]:[輸入引數][:異常檢測條件]]);

volatile 關鍵字用於防止編譯器最佳化更改此處彙編程式碼;彙編語句填入彙編程式碼字串;後面三類引數均可省,其中輸出結果填入一個存結果的變數,輸入引數填需要載入的變數或者供替換程式碼中佔位符的有關值;異常檢測可填入需要保持的暫存器,當暫存器被佔用時,編譯器會報錯。

另外一個用法是用在 register 型變數之後,可以指定該變數對應哪個暫存器,如:

register int sys_id __asm__("$2") = 4;

這樣對變數 sys_id 的取值/賦值等操作就等於對暫存器 $2 的讀寫操作。

以下是一些例子:

int a, b;
// a = a + b - 1;
// %0, %1, ... 就是佔位符
__asm__ volatile(
    "add %1,%1,%2\n\t"
    "addi %0,%1,-1"
    :"=r"(a)
    :"r"(a),"r"(b));
// a = b < 0;
__asm__ volatile(
    "slt %0, %1, $0"
    :"=r"(a)
    :"r"(b));
// printf("Hello");
register char *msg asm("$4") = "Hello";
__asm__ volatile(
    "jal printf"
    ::"r"(msg));
// 此處一定要有 "r"(msg),否則編譯器可能會認為變數 mmm 未被使用而忽略對該變數的賦值操作

宏函式

宏的本質就是程式碼段替換,只需要給一個程式碼段宣告一個名稱就可以在程式碼中反覆使用這一程式碼段。程式碼段可以是最基礎的字面常量等,也可以是稍複雜的多條語句(如宏函式)。當然,宏也可以簡化一些語句,甚至可以用宏實現 try-catch 語句[1]

常見宏函式的宣告形式如下:

// 無“返回值”型
#define 函式名([引數列表])\
{\
   程式碼段;\
}

// 有“返回值”型。這裡用到了括號的一個語法
#define 函式名([引數列表])\
({\
    程式碼段;\
    返回值(右值表示式);\
})

另外,引數列表是可選項,沒有型別限制,甚至也可以是程式碼段。

宏定義 Syscall 內聯彙編

SPIM 模擬器的 MIPS 系統呼叫引數:

服務 系統呼叫程式碼 引數 結果
print_int 1 $a0=integer
print_float 2 $f12=float
print_double 3 $f12=double
print_string 4 $a0=string
read_int 5 integer (in $v0)
read_float 6 float (in $v0)
read_double 7 double (in $v0)
read_string 8 $a0=buffer, $a1=length
sbrk 9 $a0=amount address (in $v0)
exit 10
print_char 11 $a0=char
read_char 12 char (in $v0)
open 13 $a0=filename(string), $a1=flags, $a2=mode file descriptor (in $a0)
read 14 $a0=file descriptor, $a1=buffer, $a2=length num chars read (in $a0)
write 15 $a0=file descriptor, $a1=buffer, $a2=length num chars written (in $a0)
close 16 $a0=file descriptor
exit2 17 $a0=result

用上述兩種宏函式定義方式定義其中幾個常用的系統呼叫,如下:

#define sys_open(pth, fg) ({\
    register int _ID_ __asm__("$2") = 13, _FG_ __asm__("$5") = fg;\
    register char *_PTH_ __asm__("$4") = pth;\
    __asm__ volatile("syscall"\
    :"=r"(_ID_):"r"(_ID_),"r"(_PTH_),"r"(_FG_));\
    _ID_;})

#define sys_print_string(str) {\
    register int _ID_ __asm__("$2") = 4;\
    register char *_STR_ __asm__("$4") = str;\
    __asm__ volatile("syscall"\
    ::"r"(_ID_),"r"(_STR_));}

#define sys_print_int(i) {\
    register int _ID_ __asm__("$2") = 1, _I_ __asm__("$4") = i;\
    __asm__ volatile("syscall"::"r"(_ID_),"r"(_I_));}

#define sys_read_int() ({\
    register int _ID_ __asm__("$2") = 5;\
    __asm__ volatile("syscall"\
    :"=r"(_ID_):"r"(_ID_));\
    _ID_;})

#define sys_read(fd, buf, len) ({\
    register int _ID_ __asm__("$2") = 14, _FD_ __asm__("$4") = fd, _LEN_ __asm__("$6") = len;\
    register char *_BUF_ __asm__("$5") = buf;\
    __asm__ volatile("syscall"\
    :"=r"(_ID_):"r"(_ID_),"r"(_FD_),"r"(_BUF_),"r"(_LEN_));\
    _ID_;})

#define sys_close(fd) {\
    register int _ID_ __asm__("$2") = 16, _FD_ __asm__("$4") = fd;\
    __asm__ volatile("syscall"::"r"(_ID_),"r"(_FD_));}

#define sys_exit() {\
    register int _ID_ __asm__("$2") = 10;\
    __asm__ volatile("syscall"::"r"(_ID_));}

編譯測試

老師推薦用線上平臺 https://godbolt.org 編譯測試,其實本地用 mips-linux-gnu-gcc 交叉編譯也行。將以上宏定義存為標頭檔案 mips-syscall.h,然後在程式碼中引用,進行簡單的測試:

#include "mips-syscall.h"

void main() {
  sys_print_string("Input a number: ");
  int n = sys_read_int();
  sys_print_string("The number is ");
  sys_print_int(n);
  sys_exit();
}

由於 SPIM/MARS 模擬器的執行入口和一般程式不太一樣,而且需要呼叫 exit 來結束程式,所以以上程式碼的駐韓數寫法比較怪。

本地交叉編譯,編譯器 mips-linux-gnu-gcc 12.3.0,編譯引數 -O2 -S -o m.s,去掉不相關欄位:

	.data
$LC0:
	.ascii	"Input a number: \000"
$LC1:
	.ascii	"The number is \000"
	.text
main:
	lw	$4,%got($LC0)($28)
	li	$2,4			# 0x4
	addiu	$4,$4,%lo($LC0)
	syscall
	li	$2,5			# 0x5
	syscall
	lw	$4,%got($LC1)($28)
	move    $3,$2
	li	$2,4			# 0x4
	addiu	$4,$4,%lo($LC1)
	syscall
	li	$2,1			# 0x1
	move	$4,$3
	syscall
	li	$2,10			# 0xa
	syscall
	jr	$31

可以看到已經成功編譯,同時宏函式也都被替換為相應的系統呼叫。再經過一些調整後得到 MARS 可用的程式碼,執行測試,結果如下:

Input a number: 9
The number is 9
-- program is finished running --

  1. https://zhuanlan.zhihu.com/p/245642367 ↩︎

相關文章