赞
踩
我们学会了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汇编程序的编写规则。
我们平时编程时经常会使用到libc库中的内存拷贝功能,接口如下:
void *memcpy(void *dest, const void *src, size_t n);
函数的功能是从源内存地址src的起始位置开始拷贝n个字节到目标内存地址dest中。这里编写MIPS汇编程序实现如下:
- //memcpy.S
-
- #include <sys/asm.h>
-
- #include <sys/regdef.h>
-
- .section .text
-
- LEAF(my_memcpy)
-
- .set push
-
- .set mips64
-
- dadd v0,zero,a0
-
- daddiu t1,zero,0
-
- loop:
-
- beq t1,a2,exit
-
- nop
-
- lb t2,0(a1)
-
- sb t2,0(a0)
-
- daddiu t1,t1,1
-
- daddiu a0,a0,1
-
- daddiu a1,a1,1
-
- j loop
-
- nop
-
- exit:
-
- jr ra
-
- nop
-
- .set pop
-
- END(my_memcpy)
上面的第一行引用的头文件asm.h里定义了宏LEAF和END,分别代表了一个函数的开始和结束。具体定义如下:
- #define LEAF(symbol) \
-
- .globl symbol; \
-
- .align 2; \
-
- .type symbol,@function; \
-
- .ent symbol,0; \
-
- symbol: .frame sp,0,ra;
- # define END(function) \
-
- .end function; \
-
- .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)中,然后地址移位。就是指令:
- lb t2,0(a1)
-
- sb t2,0(a0)
-
- daddiu a0,a0,1
-
- daddiu a1,a1,1
复制一个字节后,通过”j loop”跳转到loop标识处重新判断、拷贝字节。随着t1在后面的不断累加(daddiu t1,t1,1)到a2大小,指令就会执行到exit。
这个memcpy.S写好后,我们可以通过编写一段c代码调用测试它。c代码如下:
- //main.c
-
- #include <stdio.h>
-
- int main(){
-
- char* str = "function test \n";
-
- char dest[100];
-
- my_memcpy(dest,str,13);
-
- printf("%s \n",dest);
-
- return 0;
-
- }
编译运行命令如下:
- # gcc main.c memcpy.S -o out
-
- # ./out
-
- function test
汇编器指令(Assembler Directives),是汇编语言中使用的一些操作符和助记符,还包括一些宏指令(如nop、dla、li等)。用于告诉汇编程序如何进行汇编,它既不控制机器的操作也不被汇编成机器代码,只能为汇编程序所识别并指导汇编如何进行,也称伪指令。
所有汇编器指令的名称都以句点('.')开头,以“;”或者换行结尾。这些名称对大多数目标都不区分大小写,通常用小写字母书写。一些常用的的指令分类和功能如下:
符号/数据定义相关的指令:
“.set mark,0x3”设定常数,类似c语言中的宏定义。
- .rept 3
-
- .int 0
-
- .endr
这就相当于目标文件中会分配12个Byte的空间(int大小为4Byte*3次)。
- .macro label l
-
- \l:
-
- .endm
汇编控制相关的指令:
“.set push”和“.set pop”分别用于设置的保存和恢复,表明其间的.set mips64设 置仅仅对当前这段代码起效。
“.set noat” 防止汇编器将汇编代码翻译成用到at($1) 寄存器的指令序列。
“.set nomacro” 防止汇编器将单个汇编语句翻译成多个指令。
“.set norecorder” 防止汇编器打乱代码次序。通常和“.set reorder”成对出现。
.section .text 表明接下来的这段代码放到目标文件的.text段(代码段)。
.section .data 表明接下来的这段代码放到目标文件的.data段(数据段)。
关于段的概念请参考ELF文件格式说明,在本书最后一张将有介绍。
上面列举的汇编伪指令是和目标机器无关的伪指令。还有一些和目标机器相关的伪指令。下面列举一些MIPS体系架构相关的伪指令
比如以MIPS的“dla $4,sym”宏指令为例(加载sym地址到寄存器$4),在用n64汇编器汇编出来的结果是:
- lui $4,%highest(sym)
-
- lui $1,%hi(sym)
-
- daddiu $4,$4,%higher(sym)
-
- daddiu $1,$1,%lo(sym)
-
- dsll32 $4,$4,0
-
- 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”指令,结果就是:
- lui $4,%hi(sym)
-
- daddiu $4,$4,%lo(sym)
这里“dla $4,sym”被叫做宏指令更合适,就是可以被汇编器根据实际情况扩展成多条机器指令的伪指令。这里“dla $4,sym”指令在32位机器上扩展成2条机器指令,在64位机器上被扩展成6条机器指令。
n是一个从0到5的数字,或是数字32或64。1到5,32或64使汇编器从源程序中的这一点开始接受相应ISA级别的指令。比如.set mips3 告诉汇编器下面的指令是MIPS IV(64位指令集,兼容32位指令)中的指令。
更多的汇编指令可以参考GNU 汇编器开源社区https://sourceware.org/binutils/docs/as/的第7部分Assembler Directives。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。