批注:博文标题中的日期标记此文为我在当时发表在csai上的博客cgao.csai.cn,今搬至此处.
这几天,空余花了点时间看了一下NDS/GBA的ARM汇编,小发一篇文章,充饥一下我的博客.
下面的示例是GBA程序,非常简单,:
先在PN2(Programmer’s Notepad 2)里建个工程,主C文件内容:
//AsmTest.c
extern int Add_Func(int a,int b);
extern void memset16_func(unsigned short* dst,int ,int
hwcount);
int main()
{
*(unsigned int*)0×04000000 = 0×0403;//0×04000000
是控制寄存器的内存映射地址,这条语句类似于当年DOS 中的 INT 13H 的作用.
((unsigned short*)0×06000000)[136+80*240] = 0x03E0;
//0×06000000是VRAM,即显存的起始地址
while(1);
return 0;
}
程序很简单,main函数上面有两个外部函数的申明,这两个函数都是从单独的ARM汇编程序导出的,等会介绍,先来看看这个main函数的工作内容,就两
条语句,第一条设置GBA的显示模式为mode3并开启BG2背景层,准备往屏幕画点,第二条语句就是向VRAM,即显存里写入一个点,对应屏幕位置为
(136,80),颜色值为0x03E0(16位值,实为RGB为555的颜色).这篇文章是要讲ARM汇编的一点小知识,下面先来看看这个超简单的c文
件用GCC转成ARM汇编程序是什么样的,工具以DevkitARM为例说明,执行:
arm-eabi-gcc -mthumb-interwork -mthumb -O2 -Wall
-fno-strict-aliasing AsmTest.c -S
生成此c文件对应的汇编文件,因为这个C文件本身非
常简单,所以生成的汇编也不会复杂到哪里去,下面一起来看一下这个生成的汇编文件的内容,我在每行后面加了注释:
@AsmTest.s文 件内容
.cpu arm7tdmi
.fpu softvfp
.eabi_attribute 20, 1
.eabi_attribute 21, 1
.eabi_attribute 23, 3
.eabi_attribute 24, 1
.eabi_attribute 25, 1
.eabi_attribute 26, 1
.eabi_attribute 30, 2
.eabi_attribute 18, 4
.code 16
.file “AsmTest.c”
@以上几行我们不太关心,下面的才是重点
.text @说明这是一个代码段(code
section),其内容会被放在GBA的ROM或EWRAM内,视链接器说明而定
.align 2 @以2^2字节对齐
.global main
@全局符号 main
.code 16
@下面的代码是16位指令,即thumb指令
.thumb_func @说明这是一个thumb指令集写成的函数
.type main, %
@函数名
main: @main函数的入口
push {lr}
@先将link register即r14压栈
ldr r2, .L5 @
r2=1027=0×0403,读入.L5标示符所指的第一个位置的数到r2,看下面.L5入口第一个数是用.word前缀,说明是一个字(32bit)
mov r3, #128
@ r3=128
lsl r3, r3,
#19 @
r3=r3<<19=128*2^19=67108864=0×04000000,即GBA的控制寄存器的地址
str r2, [r3]
@ *r3=r2,即对应C文件中的*(unsigned int*)0×04000000 = 0×0403;
@第一条语句完成
mov r3, #248
@ r3=248
ldr r2, .L5+4
@
r2=100701968,注意,存储器地址都是以字节为单位编址的,这里.L5+4就是.L5的地址跳到下一个字的位置,即跳过4个字节,就是第二字,
也就是100701968
lsl r3, r3,
#2 @ r3=r3<<2=248*2^2=992=0x03E0
strh r3, [r2]
@
*r2=r3,将0x03E0赋给地址100701968,这个100701968怎么来的看后面解释,就是C文件里的第二条语句((unsigned
short*)0×06000000)[136+80*240] = 0x03E0;
.L2:
b .L2 @
死循环,对应while(1);
.L6:
.align 2 @
字对齐 32bit
.L5:
.word 1027 @
1027=0×0403
.word 100701968 @ =((unsigned short*)0×06000000)[136+80*240]的地址
.size main,
.-main
.ident “GCC:
(devkitARM release 23b) 4.3.0″
果然不难吧,只要知道ARM的那几条指令,就很容易理解.
这里先说说100701968这个数是怎么来的,其实100701968就是((unsigned
short*)0×06000000)[136+80*240]的地址,我们来算一下:
起始地址是0×06000000=100663296,偏移量是136+80*240=19336,但由于基址指向的是unsigned
short*类型地址,即为2Byte为单位,所以偏移的地址数为19336*2=38672,再加上基址,即
100663296+38672=100701968,这就对了.
再说一下,生成的汇编程序是thumb指令,其指令字长为16bit,而不是ARM的32bit,为什么要生成thumb而不生成32bit的
ARM指令
呢?因为GBA的CPU访问ROM(烧录卡)的总线是16bit,生成thumb指令的程序可使CPU每一个周期取一条指令,如果是ARM指令则得花两个
周期,其取指周期就会慢一倍,当然总体速度不一定会慢那么多,这还和数据量,所执行指令的复杂性有关。总之,如果ROM总线是32位的话,用ARM指令也
许就更合适些。
OK,C/C++文件转汇编文件的简要分析到这里,下面来看一下C和汇编的混合编程.
C/C++和汇编的混合编程主要有这几种方式: 1.C/C++内嵌汇编 2.C/C++调用汇编程序(函数)
3.汇编调用C函数
下面针对第二种情况进行简要介绍,刚好以前我的跨平台3D引擎需要一个高速的memset的16位版,即以16bit(半字)为单位进行memset.我
随便写了一个,ARM指令的,非thumb,如下:
@memset_16.s
.code 32 @ ARM指令,即32位指令字长
.align 2 @ 32位,即4字节对齐
.text @ 说明下面是代码段(不是数据段)
.global memset16_func @ 要想被C调用,一定要定义为global标识符,即全局可见
memset16_func: @ 函数入口
@ r0=dst r1= r2=hwcount,即r0为目标地址,r1为要set的值,r2为要set多少次
cmp r2,#0
bxeq lr @ 如果要set的个数为0,则不执行任何操作,直接返回
sub r0,#2 @ r0-=2
LoopR:
strh r1,[r0,#2]! @ 相当于C语言的:*((unsigned signed*)r0++)=r1
subs r2,#1 @ r2-=1,计数器减1
bne LoopR @ 如果计数器还没减到0,则继续循环
bx lr @ 函数返回
这样,这个单独的ARM汇编函数就完成了,接下来就要将它汇编成二进制.o文件,执行:
arm-eabi-gcc -c memset_16.s -mthumb-interwork -mthumb -O2 -Wall
-fno-strict-aliasing -o memset_16.o
这样就生成了.o文件,后面的工作和其它正常情况一下,就是将.o文件链接成一个elf文件,再用objcopy去掉elf里的debug信息,生
成.gba或.bin文件.
现在简单说一下如何在C/C++调用这个汇编文件里的函数.其实,最上面的C文件里main()上面的函数声明就是最关键的一步,这个函数声明要注意几
点:
1.如果是C调用,则前面一定要加上extern,如果是C++调用,则前面加extern “C”
2.函数返回值视实际函数而定,没有就写void
4.函数名一定要和汇编文件的.global标识定义的那个一样.
3.这是最重要一点,函数的参数,一般都用AAPCS规范来处理,简单地说就是,如果函数参数不超过三个的话,第一,二,三个参数传到汇编函数里时,分别
被赋给了r0,r1,r2寄存器,如果参数超过三个,就用栈,具体就看AAPCS吧.
好了,写个超简单的C函数测试一下:
extern void memset16_func(unsigned short* dst,int ,int
hwcount);
int main()
{
*(unsigned int*)0×04000000 = 0×0403;
//((unsigned short*)0×06000000)[136+80*240] = 0x03E0;
memset16_func((unsigned
short*)0×06000000+(240<<5),0x001f,240);//在屏幕的第2^5行,即第32行画一条颜色值为
0x001f的线.
//((unsigned short*)0×06000000)[120+96*240] = 0x7C00;
while(1);
return 0;
}
编译并运行,下面是VBA模拟器上的运行效果:
几点说明:
1.写命令行来编译文件是件头痛的事,可以用make,附件内有本例的工程文件,下载:Demo.rar
2.这里的memset_16,是粗糙版,如果每次调用memset_16中的,hwcout比较大(比如大于8),就可以考虑用这种方法处理来提高效
率:将r3-r10均赋为要set的值,并用strmia指令处理,然后再处理剩下的尾巴.这个留到有空再写了.