这篇文章是我的同事原创的,本人转载至此,作为笔记学习记录下,方便以后温故知新
引言:
因为某些原因,不得不在之前抵触多年的PowerPC架构上适配一下RTOS的代码,本人最喜欢的CPU架构是Arm,所以特别抵触一些奇奇怪怪的CPU架构,其中抵触比较强烈的便是今天的主角了,当然了,我抵触的原因是他们架构的指令集,中断,CPU内核机制等设计上,用汇编操作起来特别的繁琐等,一不留神可能还会产生一些bug等,好在阅读了PPC的内核手册和汇编手册等,逐渐接受了这个架构。
PowerPC:
PPC当年是IBM,摩托罗拉,苹果等三大巨头牵头合作完成的芯片架构,目的是为了对抗英特尔的X86架构的芯片,只不过后来苹果自己转投了英特尔阵营,摩托罗拉感觉也支撑不住了,后来也是IBM在苦苦支撑着,后面也发展了嵌入式的PPC架构等平台,也就是我们现在所熟悉的MPC5xxx等芯片。
好了,咱们切入今天的正题,首先咱们今天先了解一下PPC的寄存器组吧,我们可以从NXP S32DS Power安装目录下获取到一些帮助文档:
比如咱们先看这个二进制接口规范:
我们可以看到通用寄存器是R0-R31,有32个,其中咱们并未发现有PC程序寄存器,是的,我也发现了PPC貌似没有这个概念,因为咱们今天的东西是SysCall这个内容,所以首先要了解清楚二进制接口规范的函数形参传递定义是什么,从上面这个图中,咱们可以得出一个函数的形参是从R3-R6开始存放的,也就是比如int a(int a1,int a2,int a3,int a4);也就是a1-a4对应这寄存器的R3-R6,然后函数的返回值是放进R3中传递出来,然后咱们在继续看一下,R7-R10,看解释的意思,大概得出,超过4个形参的参数便开始依次放入到R7-R10中,所以总共一个函数可以有R3-R10寄存器可以存放最大8个C语言函数的形参,如果超过8个,我个人猜测只能存进栈中了,有了这些信息,咱们直接上代码:
咱们依旧是用一个可变参数的定义来实现SysCall,可以看到Id是占用了8个字节在32位系统中占用了两个字的长度,也就是还剩下6个,所以这个地方咱们最大支持6个参数传递,咱们写一个测试SysCall调用,来反汇编看一下:
上面是这一段C语言版本,然后反汇编后的汇编部分,我们直接看我框出来的部分汇编,根据TestSysCall的函数内部调用,我们可以看到Os_SysCallId_TestSysCall占用了两个字,可以看到R3=0,R4=72,Os_SysCallId_TestSysCall=72,然后接着是aa变量,可以看到aa变量是2个字的变量,我们可以看到aa的内容被取出来放进了r5,r6中了,然后接着是0x2222放进了R7中,然后就是R8放了0x33334444,最后就是R9放了一个0x55556666的地址,然后就开始跳Os_SysCall函数了,这个和我们预期一样。
咱们看一下Os_SysCall函数是怎么实现的,
这个函数实现只有两行,其实我啥都没有处理,就直接执行sc触发软中断跳转了,这个地方呢我们需要先科普一个知识点,就是PPC的中断系统,
在PPC的中断系统中,分为两个中断体系,一个是CPU内核中断,一个是由内核中断号4号开始下挂的外设中断,这个外设中断,不在咱们今天的讨论范围内,咱们今天中断介绍一下CPU内核中断。
叫IVPR,我们可以看到有一节全是0,也就是我们定义的这个中断向量的位置是要地址对齐的,我们可以从下面图中看到p_excpvecd的偏移是16个字节,也就是我们在这个中断向量中存放的是指令,它不是我们认知的存放的是中断函数的地址,而是需要执行的指令,意思就是当我们触发了这个中断,CPU会直接跳转到这个地址上开始执行,所以我们的中断函数代码只有16个字节大小,当然了,一个中断函数只有16个字节这是不可能的,所以这个地方我们存放的是中断函数跳转的代码。
我们可以看一下,这个内核中断向量的定义:
我们可以看到是一个跳转,然后我们看一下如何将这个内核中断向量给初始化到这个CPU上去,我们接着往下看:
我们可以看到IVPR在SPR=63上,这个地方解释一下SPR,其实这个SPR也是属于CPU寄存器组,只不过和通用寄存器组不通,通用寄存器组是直接暴露在外面的,汇编是可以直接比如R1,R2这样寻址读写的,但是SPR不行它的操作接口只有指定的汇编才可以比如mfspr R3,63就是读取IVPR地址到R3寄存器中,写就是mtspr 63,R3将R3的内容写入IVPR,这些操作都是需要汇编来完成。好了,这样我们就完成了内核中断向量的地址初始化了,我们接下来看一下SysCall中断函数:
我们可以看到这个中断函数分为三大部分,第一是进入函数后保存所有的CPU寄存器组的上下文,对应着后面要出这个中断函数恢复所有的上下文。
第二部分则是重头戏了,我们可以看到第二部分由5个部分组成,第一我们先取出Tab,这个我们看一下定义:
这是一个函数指针数组,每一个长度是一个字,所以我们从C语言传递进来的SysCall ID=72,如果要寻址成功就要72*4(long)这就是第二部分了,这样我们就可以根据Tab+Syscall_Id(72)*4找到这个api的函数了,第三部分就是我们重新准备好进入内核api函数的时候他们的max:6个C语言参数了,我们先看一下内核的这个函数:
我们可以看到它是跟传送进来一样的顺序参数定义,少了syscall_id索引,所以我们第三部分需要从刚刚进中断保存上下文的栈中取出我们之前保存的R5-R10重新排序放进现在的R3-R8中,这样我们调用函数前的参数传递就准备好了,接下来就是第四部分进行跳转了,PPC的跳转不像arm有直接的寄存器跳转指令,它非常的奇葩,只能先放进r0中然后去跳,这一点着实坑了我一把,然后就会顺利进入了Os_Sys_TestSysCall函数执行了,我们可以看到Os_Sys_TestSysCall函数会检查一下所有传递进来的参数,是否是预期的,然后我们还会传递一个返回值出去,刚刚之前说过,返回值会放进R3中,所以我们可以从第五部分看到,会将R3放进之前进入中断后上下文保存的栈的R3的位置上,这样我们出这个中断的时候,恢复上下文保存,就可以顺利将R3的内容覆盖到CPU的R3上,不然我们最后的R3在出中断的时候会被原来进中断的数值覆盖掉。
好了,以上就是PPC的一个SysCall的整个过程了,然后我们只要重新制作一个用户态的api就可以了:
比如这三个用户态的api,然后我们在写一个函数名不一样,但是形参和返回值定义一摸一样的api就可以了。