首页 > 编程语言 >C 语言宏 + 内联汇编实现 MIPS 系统调用

C 语言宏 + 内联汇编实现 MIPS 系统调用

时间:2024-04-12 21:13:22浏览次数:25  
标签:__ 汇编 int sys syscall 内联 MIPS ID asm

目录

笔者最近作业要求练习 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 ↩︎

标签:__,汇编,int,sys,syscall,内联,MIPS,ID,asm
From: https://www.cnblogs.com/RainbowC0/p/18132081

相关文章

  • 汇编语言简易教程(8):寻址模式
    汇编语言简易教程(8):寻址模式寻址模式是使用正在访问(读取或写入)的数据项的地址来访问内存中的值的受支持方法。这可能包括变量的名称或数组中的位置。基本的寻址模式包含:寄存器立即数内存寻址注意事项使用[]需要注意:访问内存的唯一方法是使用方括号([]'s)。省略括号......
  • 汇编语言简易教程(9):程序栈
    汇编语言简易教程(9):程序栈在计算机中,栈是一种数据结构,其中项目以相反的顺序添加,然后从栈中删除。也就是说,最近添加的项目是第一个被删除的项目。这通常称为后进先出(LIFO).堆栈在编程中大量使用,用于在过程函数调用期间存储信息。下一章提供有关堆栈的信息和示例将项目添加......
  • 汇编语言简易教程(8):寻址模式
    汇编语言简易教程(8):寻址模式寻址模式是使用正在访问(读取或写入)的数据项的地址来访问内存中的值的受支持方法。这可能包括变量的名称或数组中的位置。基本的寻址模式包含:寄存器立即数内存寻址注意事项使用[]需要注意:访问内存的唯一方法是使用方括号([]'s)。省略括号......
  • 汇编语言简易教程(9):程序栈
    汇编语言简易教程(9):程序栈在计算机中,栈是一种数据结构,其中项目以相反的顺序添加,然后从栈中删除。也就是说,最近添加的项目是第一个被删除的项目。这通常称为后进先出(LIFO).堆栈在编程中大量使用,用于在过程函数调用期间存储信息。下一章提供有关堆栈的信息和示例将项目添加......
  • 汇编语言简易教程(5):环境构建
    汇编语言简易教程(5):环境构建最近在学习assembly64时,需要对程序进行编写->生成汇编代码->调试->执行.本文聚焦于如果在Windows环境下,尽可能精简并且完整的构建一个汇编环境.基于Windows11,WSLUbuntu22.04,vscode,其他的系统/WSL发行版本.您可以以本文作为......
  • 汇编语言简易教程(6):工具链以及调试器
    汇编语言简易教程(6):工具链以及调试器通常,用于创建程序的编程工具集称为工具链。就本文而言,工具链包括以下内容汇编器Assembler​连接器Linker​加载器Loader​调试器Debugger虽然工具链有很多选项,但本文使用了一组相当标准的开源工具,这些工具可以很好地协同工作并......
  • 汇编语言简易教程(1):简介
    汇编语言简易教程(1):简介本教程完全来自于学习asseble64时的读书笔记,因为原书是英文版且缺少翻译,因此本教程也可以作为原书的导读.WhatisAssemblyLanguage具体机器架构相关,在不同的架构上,指令会有差异(复杂指令集/精简指令集)低级语言(这里的低级指的是......
  • 汇编语言简易教程(3):编码表示
    汇编语言简易教程(3):编码表示'表示'指的是计算机如何在内存中表示/存储.计算机使用二进制(1/0)进行存储,但是由于空间的限制,任何表示方式只能表达一定范围,一定精度的数据.本章简要总结了整数、浮点和ASCII表示方案。假设读者已经普遍熟悉二进制、十进制和十六进制......
  • 汇编语言简易教程(2): 计算机体系概览
    汇编语言简易教程(2):计算机体系概览架构​​CPURAMBUSDEVICESecondaryStorage(SSD/DiskDrive)通常来说程序被存储在硬盘中,在实际需要运行时从硬盘加载到内存.主存通常来说是易失性存储,所以在断电时,存储内容会丢失.二级内存通常是不易失的,对于断电等行......
  • 汇编语言简易教程(4):基本语法
    汇编语言简易教程(4):基本语法以yasm语法为主注释​;​分号之后的所有内容全都是注释,没有实际作用.数值数值必须是10进制/16进制/八进制最终都会被转为16进制的数字,以0x​开头,例如127​->0x7f​当使用8进制的时候:511​->777q​默认基数(基数)为十进制,......