当前位置:   article > 正文

MIPS指令集:汇编源程序(.S文件)编写_mips代码实例

mips代码实例

        我们学会了MIPS汇编语言后,编写汇编程序还需要一个“框架”,用“框架”的目的是让我们更加容易的编写汇编程序,把更复杂的任务,比如符号解析、地址重定向、对齐等工作交给工具链去完成。这个框架有2种方式,汇编源代码文件(以.S为后缀)和内嵌汇编(在.c文件中嵌入汇编语言的方式)。本篇介绍.S为后缀的汇编源程序的编写格式。

        我们可能经常遇到以.S后缀结尾和.s后缀结尾的文件。它们都是汇编源文件(可以作为gcc as汇编器的输入)。区别在于.S是GCC编译的汇编源代码文件。编译后生成的输出文件就是.s。之前介绍GCC编译过程时,我们知道一个.c文件的编译过程如下图白色框架部分所示。那么如果我们编写了.S文件,gcc就就省略了.c文件到.i文件的过程,可以直接对.S进行编译产生对应的.s文件。流程就如下图所示:

 

                          

图1:汇编源文件的编译过程

 

上图中的hello.S就是汇编源程序,里面可以按找汇编源程序的语法规则,使用MIPS机器指令和伪指令编写程序。本章将通过一个memcpy.S文件来了解MIPS汇编程序的编写规则。

 

一、汇编程序实例:memcpy.S

我们平时编程时经常会使用到libc库中的内存拷贝功能,接口如下:

void *memcpy(void *dest, const void *src, size_t n);

函数的功能是从源内存地址src的起始位置开始拷贝n个字节到目标内存地址dest中。这里编写MIPS汇编程序实现如下:
 

  1. //memcpy.S
  2. #include <sys/asm.h>
  3. #include <sys/regdef.h>
  4. .section .text
  5. LEAF(my_memcpy)
  6. .set push
  7. .set mips64
  8. dadd v0,zero,a0
  9. daddiu t1,zero,0
  10. loop:
  11. beq t1,a2,exit   
  12. nop
  13. lb t2,0(a1)
  14. sb t2,0(a0)
  15. daddiu t1,t1,1
  16. daddiu a0,a0,1
  17. daddiu a1,a1,1
  18. j loop
  19. nop
  20. exit:
  21. jr ra
  22. nop
  23. .set pop
  24. END(my_memcpy)

上面的第一行引用的头文件asm.h里定义了宏LEAF和END,分别代表了一个函数的开始和结束。具体定义如下:

  1. #define LEAF(symbol)                            \
  2. .globl symbol;                         \
  3. .align 2;                              \
  4. .type symbol,@function;               \
  5. .ent symbol,0;                       \
  6. symbol: .frame sp,0,ra;
  1. # define END(function)                \
  2. .end function;         \
  3. .size function,.-function

        以“.”开头的都是仅仅给汇编器看的汇编指令,用于指导汇编器如何汇编,也称伪指令。宏定义LEAF(symbol)代表为叶子函数,叶子函数就是指不再调用其他子函数的函数,叶子函数通常使用宏定义LEAF(symbol)开头,使用宏定义END(symbol)结束。和叶子函数相对应的称为非叶子函数,非叶子函数通常使用宏定义ENTRY(symbol)开头,也使用宏定义END(symbol)结束。

         上面第二行引用的头文件regdef.h里面定义了t2、a0等寄存器的习惯名称。

         程序中的“loop :”代表的是标号 。标号都以“:”结尾,标号用于定义函数的入口点、中间的分支、数据存储位置。这里标号loop和下面的标号exit分别代表分支。

        程序中的指令“beq t1,a2,exit”。其中a2就是参数n,表示要拷贝的字节数。t1功能是计数器,初始值为0(daddiu t1,zero,0)。如果t1和a2相等,那么指令跳转到exit标识处执行返回操作。否则就要拷贝src(a1)中的一个字节到dest(a0)中,然后地址移位。就是指令:

  1. lb t2,0(a1)
  2. sb t2,0(a0)
  3. daddiu a0,a0,1
  4. daddiu a1,a1,1

        复制一个字节后,通过”j loop”跳转到loop标识处重新判断、拷贝字节。随着t1在后面的不断累加(daddiu t1,t1,1)到a2大小,指令就会执行到exit。

       这个memcpy.S写好后,我们可以通过编写一段c代码调用测试它。c代码如下:

  1. //main.c
  2. #include <stdio.h>
  3. int main(){
  4.     char* str = "function test \n";
  5.     char dest[100];
  6.     my_memcpy(dest,str,13);
  7.     printf("%s \n",dest);
  8. return 0;
  9. }

编译运行命令如下:
 

  1. # gcc main.c memcpy.S -o out
  2. # ./out
  3. function test

 

二、伪指令(汇编器指令)

        汇编器指令(Assembler Directives),是汇编语言中使用的一些操作符和助记符,还包括一些宏指令(如nop、dla、li等)。用于告诉汇编程序如何进行汇编,它既不控制机器的操作也不被汇编成机器代码,只能为汇编程序所识别并指导汇编如何进行,也称伪指令。

         所有汇编器指令的名称都以句点('.')开头,以“;”或者换行结尾。这些名称对大多数目标都不区分大小写,通常用小写字母书写。一些常用的的指令分类和功能如下:

符号/数据定义相关的指令:

  • .global symbol 声明symbol为全局变量。
  • .lobal symbol 声明symbol为局部变量。作用范围仅在当前文件内。
  • .extern symbol 声明一个外部符号。
  • .set symbol ,expression常量设置。比如

“.set mark,0x3”设定常数,类似c语言中的宏定义。

  • .byte 定义一个字节(8位)的地址空间。类似的指令还有.int、.long 、.word。空间大小依赖具体系统。
  • .string “STR”也是用于字符串定义。但是有尾端区分。
  • .ascii “string”... 将字符串组合成连续的地址。还有一个“.asciz”。.asciz会在字符串后自动添加结束符\0。
  • .rept count 重复count次扩展后面的指令,以.endr结尾。例如:
  1. .rept 3
  2. .int 0
  3. .endr

这就相当于目标文件中会分配12个Byte的空间(int大小为4Byte*3次)。

  • .macro name args 宏定义,name 为宏名称,args为参数,以.endm结尾。例如:
  1. .macro label l
  2. \l:
  3. .endm

汇编控制相关的指令:

  • .set symbol 汇编语言控制指令。例如:

“.set push”和“.set pop”分别用于设置的保存和恢复,表明其间的.set mips64设   置仅仅对当前这段代码起效。

“.set noat” 防止汇编器将汇编代码翻译成用到at($1) 寄存器的指令序列。

“.set nomacro” 防止汇编器将单个汇编语句翻译成多个指令。

“.set norecorder” 防止汇编器打乱代码次序。通常和“.set reorder”成对出现。

  • .text 告诉汇编器,把此后产生的代码放到目标文件中的“.text”段。
  • .section name 告诉汇编器,把此后产生的代码放到目标文件中的name段。比如:

.section .text 表明接下来的这段代码放到目标文件的.text段(代码段)。

.section .data 表明接下来的这段代码放到目标文件的.data段(数据段)。

关于段的概念请参考ELF文件格式说明,在本书最后一张将有介绍。

  • .ent 标识函数的起始点。
  • .end 标识函数的结尾,和.ent一样仅仅用于调试。
  • .size 表示在函数表中,function和所用指令的字节数一同列出。
  • .align n对指令或者数据的存放地址指定对齐方式。n取值因系统而异。
  • .type symbol @function 标识symbol 为函数名称。

 

三、MIPS相关的汇编伪指令

上面列举的汇编伪指令是和目标机器无关的伪指令。还有一些和目标机器相关的伪指令。下面列举一些MIPS体系架构相关的伪指令

  • .sym32 设置加载地址为32位

比如以MIPS的“dla $4,sym”宏指令为例(加载sym地址到寄存器$4),在用n64汇编器汇编出来的结果是:

  1. lui     $4,%highest(sym)
  2. lui     $1,%hi(sym)
  3. daddiu  $4,$4,%higher(sym)
  4. daddiu  $1,$1,%lo(sym)
  5. dsll32  $4,$4,0
  6. daddu   $4,$4,$1

也就是把sym地址按64位来处理,%highest(sym)获取的是sym的高16位(bit63-bit48)、%higher(sym)获取的是sym的bit47-bit32。%hi(sym)获取的是sym的bit31-bit16、%lo(sym)获取的是sym的低16位(bit15-bit0)。而如果要想指定sym32位处理,那么需要添加”.set sym32”指令,结果就是:

  1. lui     $4,%hi(sym)
  2. daddiu  $4,$4,%lo(sym)

这里“dla $4,sym”被叫做宏指令更合适,就是可以被汇编器根据实际情况扩展成多条机器指令的伪指令。这里“dla $4,sym”指令在32位机器上扩展成2条机器指令,在64位机器上被扩展成6条机器指令。

 

  • .set mipsn 兼容的指令版本

         n是一个从0到5的数字,或是数字32或64。1到5,32或64使汇编器从源程序中的这一点开始接受相应ISA级别的指令。比如.set mips3 告诉汇编器下面的指令是MIPS IV(64位指令集,兼容32位指令)中的指令。

  • .cpsetup .cpload
  • .set gp=64 和.set fp=64 允许指定目标文件中寄存器的大小。默认情况是.set gp=default .set fp=default。
  • .set hardfloat 开启使用硬件浮点指令。与其对应的是.set softfloat
  •  

更多的汇编指令可以参考GNU 汇编器开源社区https://sourceware.org/binutils/docs/as/的第7部分Assembler Directives。

 

 

 

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/weixin_40725706/article/detail/271906
推荐阅读
相关标签
  

闽ICP备14008679号