MIPS指令集:内嵌汇编asm语法介绍

目录

一、内嵌汇编基本格式

二、输入操作数和输出操作数

三、破坏描述

四、有名操作数和指定寄存器

五、操作数的修饰符:约束字符

六、内嵌汇编实例:mips系统调用syscall

七、理解 asm volatile("": : :"memory")的含义 


       内嵌汇编(Assembly)是可以直接插入在c/c++语言中汇编程序。它实现了汇编语言和高级语言的混合编程。当在高级语言中要实现一些高级语言没有的功能,或者提高程序局部代码的执行效率时,都可以考虑内嵌汇编的方式。

         内嵌汇编标识为asm()。asm是c/c++中的内嵌汇编关键字,或称模板。用于通知编译器,接下来的()内的代码是内嵌汇编程序, 需要特殊处理。()内部的有自己专门的语法格式。内嵌汇编实现了c/c++语言和汇编语言的混合编程。比如你可以在一个c语言文件中使用MIPS汇编指令MOVE完成两个数/地址的拷贝:

  asm("move %0,%1\n\t" :"=r"(ret) :"r"(src) );

       上面的内嵌汇编指令功能类似于c语言中的赋值操作:ret = src 。 这里src和ret都是c语言中的变量,src在内嵌汇编中作为输入操作数。ret在内嵌汇编中作为输出操作数。“=g”中的“=”符号表明这是个输出操作数。%0,%1称为占位符(顾名思义,占位符就是先占住一个固定的位置,等着你再往里面添加内容的符号),代表指令的操作数,分别代表c语言变量的ret和src。 内嵌汇编指令"move %0,%1\n\t"中的move还不是真正的MIPS汇编指令,MIPS中的move指令的2个操作数是寄存器,而此处move的操作数是c语言中的变量。C变量与寄存器的对应关系由GCC编译器自动处理,处理后的结果就是2条load指令加载变量到某2个寄存器,然后再执行move指令操作。扩展后的真实汇编指令大概是下面这样:

lw t1,src
lw t2,ret
move t2,t1

         可以看出使用内嵌汇编,我们就省去了加载变量到寄存器的过程,也不用考虑使用哪个寄存器的问题。方便我们更快捷的编写程序。

 

一、内嵌汇编基本格式

asm(
    内嵌汇编指令
    :输出操作数
    :输入操作数
    :破坏描述
);

       内嵌汇编以 asm(); 格式表示,里面分成4个部分,内嵌汇编指令、输出操作数、输入操作数、破坏描述。各部分之间使用“:”分割。其中内嵌汇编指令是必不可少的,但可以为空。其他3部分根据程序需要可选。如果只有内嵌汇编指令时,后面的“:”可以省略。例如:

asm("break" );

同时asm是__asm__的别名,所以上面语句也可以写成

__asm__(“break”);

再看下面的内嵌汇编:

asm("daddu %0,%1,%2\n\t" 
     :"=r"(ret) 
     :"r"(a),"r"(b)
);

其中"daddu %0,%1,%2\n\t" 就是内嵌汇编指令,指令由指令操作符和指令操作数组成。操作符就使用MIPS汇编指令中的助记符,操作数可以是%0,%1,%2形式的占位符,来表示c语言中变量ret、a和b。指令操作数也可以是寄存器。使用寄存器做指令操作数时,寄存器前面需要符号$。例如:

asm("move $31,%0\n\t" 
    :        /*此处的:不能省略*/
    :"r"(a)
);

上面这条指令实现了把c语言变量a的值存入通用寄存器ra($31)。

注意:内嵌汇编程序中如果没有输出部分,但是有输入部分,那么输出部分的“:”不能省略。同时asm模板里面可以使用/**/或者//添加注释。

asm模板里可以有多条内嵌汇编指令。每条指令都以" "为单位。多条指令可以使用" ;"号、\n\t或者换行来分割。

asm("dadd %0,%1\n\t"
    "dsub %0,%2\n\t"
    :"=r"(ret)
    :"r"(a),"r"(b)
);

 

二、输入操作数和输出操作数

         内嵌汇编中的操作数包括输出操作数和输入操作数,输出操作数和输入操作数里的每一个操作数都由一个约束字符串和一个带括号的c语言表达式或变量组成,比如“r”(src)。多个操作数之间使用“,”分割。内嵌汇编指令中使用%num的形式依次表示每一个操作数,num从0开始。比如:

asm("daddu %0,%1,%2\n\t"
    :"=r"(ret)         /* 输出操作数,也是第0个操作数%0 */
    :"r"(a),"r"(b)     /* 输入操作数,也是第1个操作数和第2个操作数 %1,%2 */
);

这里使用了daddu指令实现了c语言中ret=a+b的操作。两个输入操作数"r"(a)和"r"(b)之间使用“,”分割。%0代表操作数"=r"(ret)、%1代表操作数"r"(a)、%2代表操作数"r"(b)。

          每个操作数前面的约束字符串表示对后面c语言表达式或变量的限制条件。GCC会根据这个约束条件来决定处理方式。比如"=r"(ret)中的"=g"表示有两个约束条件,"="表明此操作数是输出操作数,"r"(b)中的"r"表示将变量b放入通用寄存器(relation相关联)。约束字符还有很多,有些还与特定体系结构相关,在下一节会详细列举。

输入操作数通常是c语言的变量,但是也可以是c语言表达式。比如:
   

asm("move %0,%1\n\t"
    :"=r"(ret)
    :"r"(&src+4)
);

这里输入操作数 &src+4 就是c语言表达式。执行的结果就是把&src+4的地址赋给ret。

         输出操作数必须是左值,GCC编译器会对此做检查。左值概念就是以赋值符号 = 为界,= 左边的就是左值,= 右边就是右值。输入操作数可以是左值也可以是右值。所以输出操作数必须使用"="标识自己。同时默认情况下输出操作数必须是只写(write-only)的,但是GCC不会对此做检查。这个特性有时会给我们带来麻烦。如果你要在内嵌汇编指令里把输出操作数当右值来操作,GCC编译时不会报错,但是程序运行后你可能无法得到你想要的结果。为此我们可以使用限制符"+"来把输出操作符的权限改为可读可写。例如:

asm("daddu %0,%0,%1\n\t"
    :"+r"(ret)
    :"r"(a)
);

这就实现了ret = ret+a的操作。 "+r"中的"+"就表示ret为可读可写。同时我们也可以使用数字限制符"0"达到修改输出操作符权限的目的。

asm("daddu %0,%1,%2\n\t"
     :"=r"(ret)
     :"0"(ret),"r"(a)
);

这里数字限制符"0"意思是第1个输入操作数ret和第0个输出操作数使用同样的地址空间。数字限制符只能用在输入操作数部分,而且必须指向某个输出操作数。

 

三、破坏描述

        破坏描述部分就是声明内嵌汇编中有些寄存器被改变。通常内嵌汇编程序中会使用到一些寄存器,并对其做修改。如果在破坏描述部分不做说明,那么gcc编译内嵌汇编时不会做任何的检查和保护。这可能就会导致程序出错或致命异常。例如:

asm("dadd %0,%1,%2\n\t"
    "move $31,%0\n\t"
    :"=g"(ret)
    :"r"(a),"r"(b)
);

上面程序完成ret=a+b,然后ret的值写入寄存器ra($31)。我们知道寄存器ra被用来做函数返回的。但是ra被改变,将导致函数无法正常返回。这时就需要在破坏描述部分添加声明来告诉编译器此寄存器的值被改变。MIPS的内嵌汇编中寄存器的使用以$num形式,num代表寄存器编号。在破坏部分声明就使用"$num"形式,多个声明之间使用“,”分开。例如:

asm("dadd %0,%1,%2\n\t"
    "move $31,%0\n\t"
    :"=g"(ret)
    :"r"(a),"r"(b)
    :"$31"
);

破坏描述符除了寄存器还有“memory”。它的作用见本章最后一节。

 

四、有名操作数和指定寄存器

         从gcc的3.1版本之后,内嵌汇编支持有名操作数。就是可以在内嵌汇编中为输入操作数、输出操作数取名字,名字形式是[name],放在每个操作数的前面,然后汇编程序模板里面就可以使用%[name]的形式,而不是上面%num形式。例如:

asm("daddu %[out],%[in1],%[in2]\n\t"
     :[out]"=g"(ret)
     :[in1]"r"(a),[in2]"r"(b)
);

这里给c语言变量ret取名为out、变量a和b取名为int1和in2。这里的别名要求可以是大小写字母、数字、下划线等,但是你必须确保同一汇编程序中没有两个操作数使用相同的别名。

当然,你可以仅输出或者输入中的部分操作数去名字。例如:

asm("daddu %[out],%1,%2\n\t"
    :[out]"=g"(ret)
    :"r"(a),"r"(b)
);

这里我只给第0个操作数"=g"(ret)取名字。那么后面的第1个操作数和第2个操作数在使用时还是序列号形式%1、%2。  

        有时候我们需要在指令中使用指定的寄存器;比如系统调用时需要将系统调用号放在v0寄存器,参数放入a0-a7寄存器。那么这是我们可以在c语言声明变量时使用指定寄存器功能。例如:

register int sys_id asm("$2") = 5001;

这里使用关键字register声明了一个寄存器变量 sys_id,并通知GCC编译器使用$2(v0)寄存器来加载sys_id变量。

 

五、操作数的修饰符:约束字符

        约束字符就是输入操作数和输出操作数前面的修饰符。约束字符可以说明操作数是否可以在寄存器中,以及哪种寄存器;操作数是否可以是内存引用,以及哪种地址;操作数是否可以是立即常数,以及它可能具有的值。本节介绍常用的约束字符信息。

 

  • “r” 通知汇编器可以使用通用寄存器中的任意一个来加载操作数。最常用的一个约束。
  • “g” 允许使用任何通用寄存器、内存或立即整数操作数。
  • “i”通知汇编器这个操作数是个立即数(一个具有常量值)。例如:
#define DEFAULT 1

asm("li %0,%1\n\t" 
    :"=r"(ret) 
    :"i"(-TCP_MSS_DEFAULT)
);

此处的"i"也可以使用"g"代替。

  • “n”同约束字符“i”。
  • “m”内存操作数,用在访存指令的地址加载和存储。例如
  • “o”内存操作数,用在访存指令的地址加载和存储。在MIPS架构中功能同“m”。
  • “+”修改操作数的权限为可读可写,通常只修饰输出操作数。
  • “&”
  • “I”在MIPS架构下用于标识此操作数是一个有符号16位的常数。“I”用于算术指令。例如:

          asm("daddiu $2,$2,%0\n\t" : :"I"(0x3) );

 

六、内嵌汇编实例:mips系统调用syscall

      本例中,我编写了**test.c**文件,使用内嵌汇编实现了open、close、write等3个系统函数的内嵌汇编实现。具体代码如下:

/* test.c */

void exit(){
/* sys_exit (unsigned long error_code) */
    asm(
    "li    $2,5058\n\t"  //sys_exit syscall id is 5058 -> v0
    "li    $4,0\n\t"     //exit code                   -> a0
    "syscall   \n\t");
}


int open(const char* fileName,unsigned long flags, unsigned long mode){
/* sys_open (const char *filename, unsigned long flags, unsigned long mode) */
    int fd = 0;

    asm(
    "li    $2,5002\n\t"         //sys_open syscall id is 5002 -> v0
    "move    $4,%1\n\t"       //filename            -> a0
    "move    $5,%2\n\t"       // flags            -> a1
    "move    $6,%3\n\t"       // mode            -> a2
    "syscall      \n\t"
    "sw     $2,%0\n\t"
    :"=m"(fd)                         //输出部分 对应于%0
    :"r"(fileName),"r"(flags),"r"(mode)  //输入部分 对应于%1 %2 %3
    :"$2","$4","$5","$6");  
    return fd;
}

void close(int fd){
/* sys_close        (int fd) */
    asm(
    "li    $2,5003\n\t"  //sys_close syscall id is 5003 -> v0
    "move    $4,%0\n\t"     //fd           -> a0
    "syscall      \n\t"
    :
    :"r"(fd)
    :"$2","$4");
}


void write(int fd,const void *buf, unsigned long count){
/* sys_write (int fd, const void *buf, unsigned long count) */
    asm(
    ".set noreorder \n\t"      
    "li    $2,5001\n\t"      //sys_write syscall id is 5001 -> v0
    "move    $4,%0\n\t"         //fd          -> a0
    "move    $5,%1\n\t"       // buf            -> a1
    "move   $6,%2\n\t"        //cout           -> a2
    "syscall      \n\t"
    :
    :"r"(fd),"r"(buf),"r"(count)
    :"$2","$4","$5","$6");
}

int main(){
    const char* fileName = "/home/sunguoyun/file.txt";
    unsigned long flags = 0x00000002 | 0x00000100; //O_RDWR | O_CREAT
    unsigned long mode = 0644;
    int fd = open(fileName,flags,mode);
    write(fd,"hello,world\n",12);
    close(fd);
    exit();
}
```

上面例子中,没有使用glic库,而是直接通过syscall系统调用实现open、write、close功能。整个程序功能很简单。入口函数为main()。实现了打开/home/sunguoyun/file.txt文件(如果不存在则创建),并向此文件写入“hello,world”字符串。

现在使用gcc命令编译并运行:

$ gcc -fno-builtin test.c -o test
$ ./test
$ cat /home/sunguoyun/file.txt
hello,world

我们使用gcc命令编译test.c文件,生成最终的二进制可执行文件test。其中"-fno-builtin"参数功能是避免test.c文件中close文件和gcc的同名内建函数的冲突。
使用“./test”命令执行后,可以通过cat命令查看到file.txt文件已经创建,里面的内容正是刚才写入的“hello,world”。

 说明:mips的系统调用规则如下:

  1. 系统调用号(内核中实现的每个系统函数对外都有对应的系统调用号,具体号可以参考×××)存放在v0寄存器。
  2. 形参存放到a0到a7寄存器,如果行参多于8个,则需要通过栈传递。
  3. 系统调用指令为syscall。
  4. 函数返回值存放在v0寄存器。

 

七、理解 asm volatile("": : :"memory")的含义 

       asm volatile("": : :"memory")是我们平时经常遇到的内嵌汇编格式。其中有一个关键字volatile和一个破坏描述“memory”。当然这两个关键字不是必须同时出现的,使用时要根据情况。

        MIPS是多级流水线架构。编译器优化就会依赖这个特性在编译时调整指令顺序,让没有相关性的指令可以乱序执行,以充分利用CPU的指令流水线,提高执行速度。如果内嵌汇编的指令中直接使用了某些寄存器或内存。GCC编译器在优化后很可能带来错误。这种情况我们可以使用volatile关键字来修饰。volatile用于告诉编译器,严禁将此处的asm汇编语句与它之前和之后的c语句在编译时重组合。

        如果你的内嵌汇编中使用了一段未知大小的内存,或者使用的内存用于在多线程。那么请务必使用约束字符“memory”。“memory”就是通知GCC编译器,此段内嵌汇编修改了memory中的内容,asm之前的c代码块和之后的c代码块看到的memory可能是不一样的,对memory的访问不能依赖之前的缓存,需要重新加载。

 

 

 

 

 

你可能感兴趣的:(MIPS/ARM体系结构/汇编)