关于函数指针值和函数入口地址不同引发的思考

关键字 :BXBLXBLS32K3

        在调试 S32K3 的时候,遇到外部传入代码在 ram 执行的情况,在这个过程中发现,函数指针值被赋值后和函数入口地址是不一致的,比函数入口地址大了 1,这种现象决定研究一下。

一、 查看代码

        在赋值过程中,函数指针值就已经大于函数入口地址,所以先查看了跳转代码,看不同的地址值是如何跳转的,如下:

      (1)使用函数指针执行时,使用的是 BLX:

      (2)直接调用函数时,使用的是 BL:

         可以看到,这里面跳转指令是不一样的。那么这种现象就应该比对一下跳转指令的不同。

1.1 跳转指令

         跳转指令有很多个,用于今天的主要看 BL、BLX、BX这三个。在这个之前说明一下一些指令的操作:

BranchTo(bits(32) address)操作如下:

         _R[RName_PC]=address;
Return;

 

         另外:一般3级或5级流水线前面是 取指、译码、执行,也就是,执行的时候,PC值是执行指令地址+4(仅指一个指令 16 bit )。

1.1.1 BL

          BL:BL <label>,后面是跟一个立即数,这个立即数执行BL 指令时的 PC 值到 label 的偏移值(有符号),可以看出是一个相对地址,在指令的 Operation 可以看到(只取一部分,另一部分替代):

         next_addr=PC;
LR=next_addr<31:1>:’1’;
BranchTo( (PC+label)<31:1>:’0’ );

 

         从上面可以看到,这个指令会改变 LR 寄存器值,指向 BL 指令下一个指令(注意 BL 指令是 4字节的指令,不是2字节,所以这里 PC+4 存到 LR 就是指的下一条指令),然后将跳转的地址赋值到 PC 上,实现跳转。

注意一点的是:范围在 ±16MB。一般这个不用于可变函数入口的跳转,比如函数指针,变一下,那么代表需要重新计算;甚至一些编译器编译出来的是会跳转到中转的代码地址,在中断代码这里直接用 BX 跳转到想要的地址,这样很明显会导致问题变得很复杂。

看到这里,肯定有人会想,那如果进行函数嵌套,LR 寄存器如果做,经过实验,如果发生函数嵌套,函数会在最开始的时候就使用 push 对 push 进行压栈。

1.1.2 BLX

BLX:BLX <Rm>,后面跟的是寄存器,如通用寄存器,lr。其跳转的是 Rm 指定的绝对地址,在指令的 Operation 可以看到(只取一部分,另一部分替代):

         Target=R[m];
next_addr =PC -2;
LR=next_addr<31:1>:’1’;
EPSR.T=Target<0>; // EPSR.T=1 是表示使用 Thumb指令集,ARMv7-M只支持 Thumb
BranchTo( Target<31:1>:’0’ );

 

         从上面可以看到,同样是会改变 LR 寄存器值,指向 BLX 指令下一个指令的位置,然后地址最后一位为 1 是为了保持 Thumb 指令集,最后跳到想要的绝对地址中。这个使用的是绝对的地址,范围是任意范围内,也就是说,其优势在于无论 R[m] 怎么变化都可以,只要是有效的地址。

1.1.3 BX

BX:BX <Rm>,后面跟的是寄存器,如通用寄存器,lr。其跳转的是 Rm 指定的绝对地址,在指令的 Operation 可以看到(只取一部分,另一部分替代):

         If CurrentMode == Mode_handler && address<31:28>==’1111’ then

ExceptionReturn(address<27:0>);
else
EPSR.T=address<0>;
BranchTo( Target<31:1>:’0’ );

 

        先看模式,模式一般是有 Thread 和 Handler 两种,一般我们代码在 Thread 模式执行,handler 是进入异常的时候的模式

        然后看 else 里面的,我们当前是正常的调用,不是进入异常,所以是执行这个分支,可以看到,地址最后一位为1 是为了保持 Thumb 指令集,最后跳到想要的绝对地址中,这里很特殊的地方在于没有改变 LR 寄存器,也就是说,使用这个指令,就不再期望跳转回来。

1.1.4 为什么函数指针加一

         从上面总结可以看到,为什么地址不一致是因为 BL 和 BLX 处理的差异, BLX 对应的寄存器的值最后一位一定要为 1,是因为指令集为 Thumb 指令集(ARMv7-M 只支持 Thumb

指令集),否则就会出错。

二、引申-将代码放在数组执行

         那么了解了上面的知识之后,由此引发了第二个思考,如何将代码放在数组里面执行,这样岂不是可以实现代码不放在工程内就可以执行(外部传入)。(当然可以用一个空函数放在 RAM 执行,但是这个非常麻烦,需要在空函数里面填充大量代码占位)

         这里有两种方式(针对 S32DS 下的 S32K3 平台,其他可以看汇编语句尝试):

2.1 使用函数指针

         使用函数指针是一种很常见的方式,但是需要注意的是,这个是用 BLX 作为跳转指令,将数组地址赋值给函数指针的时候,最后一位一定要为 1,很明显这里因为是数组,,需要手动操作添加改变,例如下面数组放了一个执行空指令消耗时间的函数:

uint32_t testArray[4]={0xBF004B02,0xD1FC3B01,0xBF004770,0x000C3500};
typedef void (* testDelay)(void);
uint32_t * p;
testDelay test;

// 为了数组最后一位为1执行的操作
uint32_t p2=(uint32_t)testArray;
p2|=1;
p=(uint32_t *)p2;
test =p;
(*test)();

 

2.2 直接用 跳转指令+数组地址

         如下,为了使用跳转指令跳转到数组代码里面,使用了以下方式:

void intoOutterRamExcCode(uint32_t * p){
__asm(
"orr r0, #1\n\t"
"add sp,#8\n\t"
"bx r0"
);
}

/* testArray 就是存放代码的数组 */
intoOutterRamExcCode((uint32_t *)testArray);

         根据分析,其实这种方式执行方式比较有趣,比如,由于 BX 是不保存 LR 的,也就是不会跳转回来,但为什么能正常执行呢?这是因为调用 intoOutterRamExcCode 的时候会保存 LR 寄存器,其实数组代码里面执行的返回是返回到这一个 LR 寄存器里面的地址,也就是说, "bx r0"  这一语句后面的所有指令都是不会执行。

         注意这里的 "orr r0, #1" 指令是为了保持地址最后一位为 1。还有为什么要对 sp 进行操作呢?因为这是看编译知道一进来就会把栈顶减小 8,但因为指令 "bx,r0" 之后的不再执行,所以我们必须手动修改,这个值要根据实际情况来。

          总得来说,这种方式不如用函数指针执行数组代码方便,比如传参超过 4 个,比如对栈顶指针的修改等等。

         当然,这里目前无法使用 BLX,原因还需要继续深入了解。

三、参考文档

ARM:《DDI0403E_d_armv7m_arm.pdf》 

★博文内容均由个人提供,与平台无关,如有违法或侵权,请与网站管理员联系。

★文明上网,请理性发言。内容一周内被举报5次,发文人进小黑屋喔~

评论