因为需要在QNX Momentics中混合C语言和ARM汇编进行开发,于是初步了解了一下APCS。和大家分享一下我自己的学习过程,然后通过一个形象一点的类比讲述我自己的理解。
刚开始的时候我是不知道有APCS这回事,所以第一次从C语言中调用ARM汇编函数的时候返回就出错了。也正因为出错才去网上查找资料,知道了有APCS这回事。
简单来讲,APCS是ARM汇编中调用过程中的一种约定,过程调用方和被调用方讲好了各自该干什么,从而保证各自的代码能正常工作。一个形象的类比就是你和你一个同事轮流使用一个办公台,约好了剪刀用完了放回左上角的笔筒里,这样大家要用剪刀的时候就不用去找。也就是说,如果哪天你的同事被干掉了,这个办公台就你一人用,你的剪刀爱放哪放哪,自己能找着就行(找不着就怨自己喽)。回到实际的ARM汇编编程中,如果你的汇编代码不调用其他人的过程,你的过程也不想被别人调用,你是可以扔掉APCS的。只是,哪能保证不调用人家的过程呢,所以还是老老实实照着APCS做吧。
理解了APCS产生的原因后,我就开始学习APCS的具体规定。网上的资料很多,百度一下APCS出来一大堆,不过里面一堆复杂的描述,看了半天也缓不过神来。最后找到一个尚观的视频,叫做《ARM指令与ARM汇编入门》,讲解的非常清晰,终于理解了APCS,视频链接如下:
http://video.sina.com.cn/v/b/49256568-1855531553.html
视频刚开始有主讲老师的自我介绍,不过听不太清,好像是叫“倪老师”,或者是“李老师”。这个“倪老师”是个大牛,对ARM指令非常熟悉,对于APCS也有非常清晰的认识。视频讲解的时候, “倪老师”在Linux环境下使用vi如行云流水般输入着ARM汇编指令,真实令人叹为观止。建议学习ARM汇编的同学们去看看那个视频。
下面将我从视频中学习到的内容结合我自己的理解讲述一下,应该不算是侵权吧,真有侵权的地方也请“倪老师”指出,一定删除相关内容。
理解APCS需要一些基本的汇编知识,包括什么是寄存器,什么是堆栈,ARM汇编的基本语法等。我自己是因为很多年前还没毕业的时候使用X86汇编写过一个五子棋程序,所以学习起来还算是可以跟得上。对这些知识不太了解的同学可以先跳过以下内容,等以后掌握一些ARM汇编基础知识后再来看,或者只是初步看一看,留个印象就好。
在ARM体系的CPU里有37个寄存器,其中可见的寄存器有15个通用寄存器(叫r0-r14),还有其它的状态寄存器和一个程序计数器(叫r15 或者是pc)。
在汇编里需要使用上面提到的通用寄存器保存各种数据,如果寄存器不够用,还会使用堆栈保存数据。APCS就是规定各个过程如何使用这些寄存器和堆栈,从而保证过程在相互调用的时候可以正常工作。
为了形象理解,我们还是拿公用的办公台作比喻,不过这次讨论的不是剪刀的位置,而是办公台的抽屉。
你可以把寄存器想象成办公台的抽屉,现在这个特殊的办公台有一溜抽屉,整整16个,对应我们上面提到的15个通用寄存器和一个PC寄存器。为了和ARM 32位的寄存器严格对应起来,我们想象每个抽屉都只能放4张A4纸(把一个字节想象成一张A4纸)。
另外,办公台旁边还有一个文件架,每格可以放一张A4纸,这就是我们的“堆栈”了。办公台的示意图如下:
为了对应CPU的使用情况,我们对这个办公台的使用做一些规定:
打开抽屉拿文件比打开文件架拿文件要快(寄存器的访问速度比堆栈快)
在文件架上存放文件只能由上往下依次存放,第14个抽屉(编号r13)里记录了目前存放到第几个格子。往文件架多放一张A4纸文件,第14个抽屉里的数字减一,从文件架里拿出来一张A4纸文件,第14个抽屉里的数字加一。(就是堆栈的使用要求)
第16个抽屉指定下一个使用办公台的是哪个员工。(就是PC寄存器的作用)
一个员工的名字要4张A4纸来写,这名字也太长了吧!情节需要,情节需要而已。(ARM指令是32位的,占4个字节)
我们来看看一群人共用这个办公台会出现什么问题。
第一个出现的问题就是下一个该由谁使用办公台的问题。
比如A员工使用了这张办公台,使用了一定的时间后在第16个抽屉(R15,PC)中放了个纸条,写了员工B的名字。按照使用规定,员工B会叫来使用这张办公台。员工B使用完办公台以后,如果有需要,他当然可以指定由员工C来使用这个办公台。如果他没有需要呢,就是员工B使用完办公台以后就不需要给其他员工了,这时办公台该交给谁使用呢?一种合理的做法是谁交给员工B的就交回给谁,在上面的情况中就是员工A了。不过员工B并不知道上一个使用者是谁,所以员工B使用了办公台以后就不知道该给谁了。随便叫一个过来?计算机世界里可没有随便的概念,说不定叫出个恐龙来。
解决这个问题的方法是做一个约定,上一个使用者在第15个抽屉(R14,lr)里放上自己的名字,让下一个使用者知道办公台应该交回给谁使用。
好,时光倒流,再来一遍:
A员工使用了这张办公台,使用了一定的时间后先在第15个抽屉(R14,lr)里放上自己的名字,就是“A员工”,然后在第16个抽屉(R15,PC)中放个纸条,写了员工B的名字。按照使用规定,员工B会叫来使用这张办公台。员工B使用完办公台以后,要交还给上一个使用者了,只需要将第15个抽屉(r14,lr)里的名字放入第16个抽屉(r15, pc)就好了。“A员工”的名字被放入了第16个抽屉,按照规定,A员工被叫来使用办公台。搞定!
员工B对应的ARM汇编代码就是:
mov pc,lr @将lr寄存器里的值拷贝到pc寄存器中。
第二个问题是第15个抽屉重复使用的问题。
对于上面情况,问题是否解决了呢,其实不是。
来看看,办公台由员工A交给了员工B,这时第15个抽屉(R14,lr)中是“A员工”的名字,如果员工B使用了以后需要交给员工C呢?按照约定,员工B需要在第15个抽屉(R14,lr)中先放上自己的名字“B员工”,然后再叫员工C过来。可是,抽屉里不是放着“A员工”的名字吗?
简单的解决方法是先把“A员工”的名字放到旁边抽屉里,旁边不是还有一溜抽屉嘛。
好,时光再次倒流,再来一遍:
A员工使用了这张办公台,使用了一定的时间后先在第15个抽屉(R14,lr)里放上自己的名字,就是“A员工”,然后在第16个抽屉(R15,PC)中放个纸条,写了员工B的名字。按照使用规定,员工B会叫来使用这张办公台。
员工B使用办公台的时候先将写着“A员工”的纸放到第1个抽屉里(r0),然后在第15个抽屉(R14,lr)里放上自己的名字“B员工”,最后在第16个抽屉(R15,PC)里放上员工C的名字,将办公台交给员工C使用。等员工C将办公台交回给员工B之后,员工B可以将第1个抽屉(r0)里的纸条(写着“A员工”)放回第16个抽屉(R15,PC)里就可以了。
员工B对应的ARM汇编代码如下:
mov r0,lr
mov lr,”返回地址”
mov pc,”子函数方法地址”
mov lr, r0
mov pc , lr
第三个问题是抽屉的使用权问题。
聪明的同学们很快会发现上面的问题,员工B在第1个抽屉保留了写着“A员工”的纸条,然后将办公台交给了员工C使用,员工C可不知道第1个抽屉装着什么,说不定一不小心就第1个抽屉的东西到了。
这里就有一个抽屉使用权的问题,让我们再做一些约定:第1个抽屉到第4个抽屉(r0~r3)大家可以随便使用,不管里面有什么东西,要用的时候将抽屉清空就好了;而第5个抽屉到第12个抽屉(r4~r11)里的东西不能乱动,交到你手里什么样子,交回给上一个使用者时就应该是什么样子。
本来觉得有了这些约定就安逸了,第1个到第4个抽屉不能放重要的东西,会被别人扔掉的,那就将重要的东西放第5个到第12个抽屉吧,别人会确保我的抽屉是原样的。仔细一想就有问题了,我要是使用第5个到第12个抽屉,我将办公台交回给上一个使用者的时候这些抽屉就不是原来的样子了,交不了差。
如果这时候再来时光倒流。。。。
A员工一上来就傻眼了,办公台上写着:第1个抽屉到第4个抽屉会被人倒掉,第5个抽屉到第12个抽屉是人家的东西,不能动。什么破公司,A员工骂一句就要交辞职报告了。
不要冲动,不要冲动,旁边不是还有个文件架嘛!既然第5个抽屉到第12个抽屉是人家的东西,你使用前先将抽屉里的东西搬到文件架上,等你用完了抽屉,将东西从文件架上搬回来就好了嘛。
留住了A员工,再来次时光倒流:
A员工开始使用这张办公台,他很规矩地将第5个抽屉到第12个抽屉里的东西搬到了文件架上,而且按规定在第14个抽屉(r13,SP)里记录了当前的文件架使用位置,在第15个抽屉(R14,lr)里放上自己的名字,就是“A员工”,然后在第16个抽屉(R15,PC)中放个纸条,写了员工B的名字。按照使用规定,员工B会叫来使用这张办公台。
员工B使用办公台的时候也是很规矩地将第5个抽屉到第12个抽屉里的东西搬到了文件架上,而且按规定在第14个抽屉(r13,SP)里记录了当前的文件架使用位置,接着将写着“A员工”的纸放到第5个抽屉里(r4),然后在第15个抽屉(R14,lr)里放上自己的名字“B员工”,最后在第16个抽屉(R15,PC)里放上员工C的名字,将办公台交给员工C使用。
等员工C将办公台交回给员工B之后,员工B可以将第5个抽屉(r4)里的纸条(写着“A员工”)放到第1个抽屉(r0),然后将A员工的东西从文件架上搬回到第5个抽屉到第12个抽屉里,最后是将第1个抽屉(r0)里的纸条(写着“A员工”)放回第16个抽屉(R15,PC)里就可以了。
好晕呀,真难为这些员工了,使用这种办公台的员工伤不起呀!操作寄存器的人伤不起呀!
员工B对应的ARM汇编代码如下:
sub sp,sp, #12 @开辟三个寄存器的空间,存放r4,r5,r6,样例而已,r7之后的就不存了
str r4, [sp] @r4入栈
str r5, [sp, #4] @r5入栈
str r6, [sp, #8] @r6入栈
@ 后面应该还有r7,r8…等等地操作,这里省略
mov r5,lr @ 将返回地址保留在r5中,因为r5不会被子函数破坏。
@调用子函数
Mov r0,r4 @将r4中的返回地址放到r0中,因为要开始恢复r4,r5,r6了
ldr r6, [sp, #8] @r4出栈
ldr r5, [sp, #4] @r5出栈
ldr r4, [sp] @r6出栈
mov pc , r0 @将r0中的返回地址给pc,退出
第四个问题是文件架使用位置的问题。
上面的使用方法还是有问题呢! 员工A从员工B那里接手办公台就怒了:“我在第14个抽屉记录的文件架使用位置怎么不对呀”,原来员工B使用了第14个抽屉,忘了将第14个抽屉的恢复原样了。
于是又多了一条约定,第14个抽屉(R13,SP)里的东西也要保持原样。
这就有点麻烦了,第14个抽屉(R13,SP)记录的是文件架的使用位置,一旦使用文件架就得使用第14个抽屉(R13,SP),现在又要保留第14个抽屉的东西,这不是难为人吗?
想来想去,员工们想了个办法,使用的时候将第14个抽屉(R13,SP)的东西也搬到文件架上,不过有个问题,是先搬东西还是先做记录呢,怎么做都有问题。再看看抽屉,好像只剩第13个抽屉可以用了:
第1个抽屉到第4个抽屉(r0~r3):虽然是可以随便用,不过说不定上个人留了什么东西我自己需要的呢,先不动
第5个抽屉到第12个抽屉(r4~r11):是别人的东西,一会要搬到文件架上的
第14个抽屉:记录的就是文件架的使用位置
第15个抽屉:上一个使用者的名字
第16个抽屉:不能乱动的,叫别人过来的时候才用
好,就先将第14个抽屉的东西放到第13个抽屉上,然后在第14个抽屉记录新开了个文件架的格子,再将第13个抽屉的东西搬到新开的文件架格子上。为了方便,第15个抽屉的东西也搬到文件架上,剩下的就是依次将第5个抽屉到第12个抽屉的东西搬到文件架上。当使用完了再按顺序将文件架的东西搬回来就好了。
时光倒流就不来了,再来要吐了,对应ARM汇编代码如下:
mov ip,sp
sub sp,sp, #12
str lr, [sp]
str ip, [sp, #4]
str fp, [sp, #8]
sub fp, ip, #4
@子函数代码
ldr lr, [fp, #-8]
ldr ip, [fp, #-4]
ldr fp, [fp,#0]
mov sp, ip
mov pc , lr
对着一堆的抽屉讲了一通,大家可能看地时候好像整明白了,回过头看汇编代码又忘了。上面的例子只是希望通过形象的方式让大家理解,最终能在脑海里方便记忆的还是汇编代码和对应的注释。
@----------------------------------------------------
@程序刚开始,
@r0到r3(a1到a4)可能会有参数,不能动
@r4到r11(v1到v8)是父函数的东西,需要保留,不能动
@r13(SP)是堆栈指针
@r14(lr)是返回地址
@r15(PC)更是不能随便动能
mov ip,sp @将Sp(堆栈指针,r13)放在ip中,ip就是r12,目前唯一可以使用的寄存器
sub sp,sp, #12 @ 堆栈指针减12,开出相当于3个寄存器空间的堆栈。
str lr, [sp] @r14入栈,里面是返回地址
str ip, [sp, #4] @ r12入栈,里面是父函数的堆栈指针
str fp, [sp, #8] @ r11入栈,里面是需要为父函数保留的东西,一会要用r11,所以将它入栈
sub fp, ip, #4 @ r11(就是fp)等于ip+4,就是自己的栈底,以后可以方便地使用堆栈
@添加自己的代码
@下面是准备退出了,注意堆栈使用时是基于fp寻址的,
@因为此时SP可能在自己的代码中调整过,不可靠。
ldr lr, [fp, #-8] @ lr出栈,里面是返回地址
ldr ip, [fp, #-4] @ ip出栈,里面是父函数的堆栈指针
ldr fp, [fp,#0] @fp出栈,里面是需要为父函数保留的东西
mov sp, ip @令sp等于ip,帮父函数恢复堆栈指针
mov pc , lr @令pc等于lr,就是等于返回地址,退出子函数,返回到父函数。
@------------------------------------
最后很开心地告诉大家,以上的分析和代码都只是为了让大家理解APCS,其实大家在使用过程中只要按模式在过程(或者说是函数)开始和结束的时候加上标准的APCS代码就好了。下面是简化过的APCS标准代码,有兴趣的同学可以研究一下,没兴趣研究的话也没有问题,写一个过程的时候直接拷贝上去就好了。
mov ip,sp
stmfd sp!, {fp,ip,lr,pc}
sub fp, ip, #4
@......子函数代码
sub sp, fp, #12
ldmfd sp,{fp, sp, pc}
能看完这篇文章难为你了,去休息一下吧,ARM汇编的路还很长!!!