TQ2440开发板学习纪实(9)--- 利用Undefined异常模拟BLX指令

在博文 《紧急求助!ARM-GCC对于函数指针调用的编译有错误?》中,我提到了GCC在编译函数指针调用的时候,会生成绝对地址跳转指令BLX。而S3C2440A这款CPU不支持BLX指令,从而导致陷入Undefined异常。

本文就利用这个Undefined异常,来模拟BLX指令,从而让使用BLX指令的程序可以正常运行在S3C2440上。

1 Undefined异常的处理流程

ARM9在执行未定义指令时,会跳转到0x00000004处执行,进入Undefined模式,并把下一条指令的地址存入LR。

2 指令模拟的思路

利用Undefined异常处理的一个非常重要的作用就是用来扩展CPU指令集,模拟执行硬件不能直接支持的指令。原理非常简单,在Undefined异常处理中,通过(LR-#4)这个地址就可以获取未定义的指令,然而根据不同的指令写出不同的等价程序即可。

例如本节将要实现的BLX指令,下面是源码事例:

ldr r3, [pc, #32]
ldr r3, [r3]
mov r0, r2
blx r3

其中blx r3的等效源码为:

mov lr, pc
bx r3

需要注意的是:

  • 进入Undefined异常处理时,CPU处于Undefined模式,此时的堆栈与异常前模式不同;
  • 我们的模拟指令程序应该运行在原模式下,而不是Undefined模式下,否则无法获取原来的寄存器环境;
  • 运行模拟指令程序前应保存所有用到的寄存器,运行返回前恢复。否则就会破坏原来的执行环境;
  • 保存寄存器的通用方法就是入栈,而入栈保存需要一定的技巧来保证返回的同时恢复所有寄存器;
  • 在Undefined模式下需要保存LR(此时存放的是原指令的下一条指令),此时不能通过入栈保存,因为Undfined的栈是独立的。变通方式,是存放LR的值到内存中。

3 BLX模拟实现

/* save the instruction's address after the undefined one. */
    .global AddrUnd
AddrUnd:
    .word  0x00000000
UndHandler:
    sub sp, sp, #8
    str r0, [sp]

    ldr r0, =AddrUnd
    str lr, [r0]

    ldr r0, =asm_und_handler
    str r0, [sp, #4]

    ldmfd sp!, {r0, pc}^

这段代码非常简单,实现的功能是保存原指令的下一条指令地址到AddrUnd中,然后跳转到asm_und_handler执行。注意此次跳转也会导致运行模式的变化。

另外为了不破坏寄存器环境,必须采用ldmfd sp!, {r0, pc}的形式来跳转,否则无法恢复r0的值。

实际的指令模拟在asm_und_handler中实现:

.global asm_und_handler

asm_und_handler:
    sub sp, sp, #4 /* reserved space for AddrUnd */
    stmfd sp!, {r0-r12}
    ldr r0, =AddrUnd
    ldr r0, [r0]
    str r0, [sp, #52]

    ldr r0, [r0, #-4] /* get the undefined instruction */
    bic r1, r0, #0xF
    ldr r2, =0xE12FFF30
    cmp r1, r2

    beq handle_BLX

    ldmfd sp!, {r0-r12, pc}

handle_BLX:
    ldr r3, =0xFFFFFFF0
    bic r0, r0, r3

    cmp r0, #0
    beq BLX_R0
    cmp r0, #1
    beq BLX_R1
    cmp r0, #2
    beq BLX_R2
    cmp r0, #3
    beq BLX_R3
    cmp r0, #4
    beq BLX_R4
    cmp r0, #5
    beq BLX_R5
    cmp r0, #6
    beq BLX_R6
    cmp r0, #7
    beq BLX_R7
    cmp r0, #8
    beq BLX_R8
    cmp r0, #9
    beq BLX_R9
    cmp r0, #10
    beq BLX_R10
    cmp r0, #11
    beq BLX_R11
    cmp r0, #12
    beq BLX_R12

    ldmfd sp!, {r0-r12, pc}

BLX_R0:
    ldmfd sp!, {r0-r12, lr}
    bx r0
BLX_R1:
    ldmfd sp!, {r0-r12, lr}
    bx r1
BLX_R2:
    ldmfd sp!, {r0-r12, lr}
    bx r2
BLX_R3:
    ldmfd sp!, {r0-r12, lr}
    bx r3
BLX_R4:
    ldmfd sp!, {r0-r12, lr}
    bx r4
BLX_R5:
    ldmfd sp!, {r0-r12, lr}
    bx r5
BLX_R6:
    ldmfd sp!, {r0-r12, lr}
    bx r6
BLX_R7:
    ldmfd sp!, {r0-r12, lr}
    bx r7
BLX_R8:
    ldmfd sp!, {r0-r12, lr}
    bx r8
BLX_R9:
    ldmfd sp!, {r0-r12, lr}
    bx r9
BLX_R10:
    ldmfd sp!, {r0-r12, lr}
    bx r10
BLX_R11:
    ldmfd sp!, {r0-r12, lr}
    bx r11
BLX_R12:
    ldmfd sp!, {r0-r12, lr}
    bx r12

因为实际中BLX 可以有
BLX r0
BLX r1

BLX r12
等12中方式,所以必须对每种方式都加以模拟。

需要注意的是,必须要在执行 bx r12之前确保寄存器环境与原指令运行时的环境完全相同,还是使用了ldmfd的方式来解决这个问题。因为ldmfd要求按照顺序加载寄存器,所以需要一点小技巧了合理安排栈中寄存器的分布。

本例中,对于除了BLX之外的未定义指令,直接忽略并跳转到下一条指令执行。

4 测试

GCC的C语言编译器对于函数指针形式的调用,会产生处BLX汇编指令,下面就是C源码和编译产生的汇编码。

 int(*f)(const char*) = puts;
 f("Hello BLX\n");

对应的汇编码:

    ldr r3, .L3+24
    str r3, [fp, #-8]
    ldr r3, [fp, #-8]
    ldr r0, .L3+28
    blx r3

S3C2440A这款CPU不支持BLX指令,如果不进行模拟实现,那么就无法实现函数调用的功能。幸好我们上面刚刚实现了BLX的模拟,此时该测试程序顺利执行,并打印出了Hello BLX。

Initialize Keys...[OK]
Initialize LEDs...[OK]
Initialize Beep...[OK]
Hello BLX
I'm idle, waiting for your order
I'm idle, waiting for your order

5 完整源码

本文对应的完整源码v1.0

猜你喜欢

转载自blog.csdn.net/smstong/article/details/53944794