ARM / Thumb 指令混合编程之代码交织 ( interworking )

原文地址:http://blog.csdn.net/cfy_phonex/article/details/18698259


本文翻译整理自 http://stuff.mit.edu/afs/sipb/project/egcs/src/egcs/gcc/config/arm/README-interworking

针对ARM7T处理器,Cygnus GNU Pro ToolKit 支持在已编译的 ARN 指令和 Thumb 指令之间进行来回切换。

虽然原文只针对相对古老的 ARM7T 处理器进行介绍,但是其基本原理仍然适用于新的处理器系列。

例如 ARMv7 / Cortex-A系列。

当然,需要根据新的编译器进行相关命令行参数的正确选取。


一、针对 C 和 C++ 文件代码交织的显式支持


缺省情况下,如果一个文件没有使用任何特殊的编译命令行参数,编译生成的代码将不支持代码交织。

如果一个程序完全通过目标文件和库文件以要么全部是 ARM 指令,要么全部是 Thumb 指令的方式编译链接,

将能生成一个可工作的可执行文件。

如果尝试去链接的目标文件和库文件中,既包括 ARM 指令, 也包括 Thumb 指令, 链接器将产生警告信息并且

生成的可执行文件不能正常工作。


为了产生支持代码交织( interworking ) 的代码,需要使用如下编译参数:

[cpp]  view plain  copy
 print ?
  1. -mthumb-interwork  

如果一个程序所需要的所有目标文件和库文件都使用了该编译参数, 即使该程序的不同部分分别使用了 

ARM 指令和 Thumb 指令,链接生成的可执行文件也可以正确工作。 且链接器不会给出警告信息。

但是,需要注意的是,使用了 -mthumb-interworking 参数后,生成的代码大小将略微增大,执行效率略微降低。

这就是为什么代码交织支持必须使能一个特定编译开关的原因。


二、针对 arm 汇编文件代码交织的显式支持

如果汇编文件将被包括进一个支持代码交织的程序中,必须遵循如下规则:

  • 任何外部可见函数必须使用  BX 指令返回
  • 普通的函数调用可以使用 BL 指令。链接器将自动插入代码以便进行 ARM 和 Thumb 指令切换
  • 如果函数指针调用来自 ARM 代码, 应该使用 BX 指令。

          然而,下述指令不能在 Thumb 模式工作,因为 MOV 指令没有正确设置 LR 寄存器的 LSB 位。

[cpp]  view plain  copy
 print ?
  1. .code 32  
  2. mov lr, pc  
  3. bx  rX  
            因此,应该使用 branch-and-link 指令来调用 _call_via_rX 函数。

            其中, rX 表示包含有该函数地址的寄存器名称。

[cpp]  view plain  copy
 print ?
  1. .code 16  
  2. bl  _call_via_rX  

  • 所有在 Thumb 模式运行的外部可见函数, 必须在函数入口前使用 .thumb_func 伪指令。

[cpp]  view plain  copy
 print ?
  1. .code 16  
  2. .global function  
  3. .thumb_func  
  4. unction:  
  5. ...start of function....  

  • 所有汇编文件必须使用 -mthumb-interwork 参数进行编译。
    (如果使用gcc 编译,如果该汇编文件被指定为第一个文件, gcc 将自动传入 -mthumb-interwork 参数)

三、支持古老的且无法区别 代码交织的代码

如果要链接相对古老的代码, 或者通过不支持代码交织的编译器生成的代码,或者通过新编译器产生

但是没有使用 -mthumb-interwork 参数生成的代码,有两种编译参数可以处理这种情况。

  • 编译开关: -mcaller-super-interworking

       该参数允许在 Thumb 模式的函数指针调用正常工作,而无须考虑该函数指针是否指向老的,无法区别代码交织的代码。

       使用该开关将产生执行较慢的代码。

       注意: 

       没有编译开关用于允许 ARM 模式的函数指针调用被特别处理。

       对于借助于函数指针的调用,如果从支持代码交织的 ARM 代码调用不支持代码交织的 ARM 代码,该调用能正常工作而

       无须编译器考虑有任何考虑。

       对于借助于函数指针的调用,如果从支持代码交织的 ARM 代码调用不支持代码交织的 Thumb 代码,该调用不能正常工作。

       (实际上对于某些特定情况,也能工作; 但是无法保证一定能工作) 这是因为只有新编译器可以产生 Thumb 代码,

        并且该编译器已经使用编译开关来产生支持代码交织的代码。

  • 编译开关: -mcallee-super-interworking

        该参数允许无法区别 ARM 和 Thumb 的代码调用 Thumb 函数,无论是直接调用或借助于函数指针的调用。

        使用此参数将产生略大且执行稍慢的代码。

        注意:

        没有编译开关可以用于允许无法区别代码交织的 ARM 或者 Thumb 代码调用 ARM 函数。
       对于从无法区别代码交织的 ARM 代码调用支持代码交织的 ARM 函数,没有必要进行任何特殊处理。
       对于从不支持代码交织的 Thumb 代码调 ARM 函数,将不能正常工作。

       这种情况没有其他选择,除非能够重新编译成支持代码交织的 Thumb 代码。

       作为编译参数 -mcallee-super-interworking (因为它影响同一个文件中所有的外部可见函数)   的替换选择,

       可以对单个函数指定一个属性或者 declspec 符号。通过此方式来表示该特定函数可以支持从无法区别代码交织的代码里被调用。

       示例如下:

[cpp]  view plain  copy
 print ?
  1.     int function __attribute__((interfacearm))  
  2.     {  
  3.         ... body of function ...  
  4.     }  
  5.   
  6. or  
  7.   
  8.     int function __declspec(interfacearm)  
  9.     {  
  10.         ... body of function ...  
  11.     }  

四、dlltool的代码交织支持

Cygnus GNU Pro ToolKit 不支持对于 dlltool 的代码交织支持。


五、代码交织如何工作

ARM 和 Thumb 指令集的动态切换,是通过 BX 指令使用一个寄存器名作为参数来完成。 

程序控制权被转交给该寄存器中存储的地址 ( LSB 位被屏蔽 )。

如果 LSB=1, 则进入 Thumb 指令处理模式; 如果 LSB=0, 则进入 ARM 指令处理模式。


当 -mthumb-interwork 参数被指定, gcc 让所有函数都通过 BX 指令返回到其调用方。

因此只要返回地址的 LSB 位被正确初始化以表示调用者的指令集类型, 就能确保操作正确。


当函数被显式调用时( 而不是通过函数指针 ), 编译器生成 BL 指令。

BL 指令的 Thumb 版本具有在它存储返回地址后设置 LR 寄存器 LSB 位的特殊属性。如此,

一个后来的 BX 指令将能够正确的返回在 Thumb 模式的  BL 指令之后的那条指令。


BL 指令本身并不改变指令集模式。因此 如果 ARM 函数被一个 Thumb 函数调用, 或者反之,

有必要生成一些额外的指令来处理该(指令集模式)改变。

当被引用的函数之地址被保存到 BL 指令时, 链接器完成了该功能。

如果 BL 指令是一条 ARM 格式的 BL 指令,但是被引用的函数是一个 Thumb 函数, 链接器将

自动生成负责从 ARM 模式到 Thumb 模式切换的调用指令段。该指令段把指令段的地址放入

BL 指令中,同时也把被引用函数的地址也放入该指令段中。

类似的,如果 BL 指令是一条 Thumb 模式的 BL 指令, 并且被引用函数是一个 ARM 函数, 链接器

将生成负责从 Thumb 模式到 ARM 模式切换的调用指令段。该指令段把指令段的地址放入到

BL 指令中,同时也把被引用函数的地址也放入到该指令段中。


这就是为什么必须要在创建汇编文件时标记 .thumb_func 伪指令的原因。 

该伪指令允许汇编器对于 ARM 函数和 Thumb 函数进行区分。( GCC 的 Thumb 版本对于它生成的任何

Thumb 函数都自动插入该伪指令)


通过函数指针的调用不同与此。无论何时,只要有函数地址,链接器就检查被引用的函数类型。

如果它是 Thumb 函数, 链接器设置函数地址的 LSB=1。从技术角度而言,这样做使得该地址不正确;

因为现在该地址和函数起始入口相差了一个字节。但是这从来都不是一个问题。因为:

  • 通过使能代码交织特性,所有借助于函数指针的函数调用都是通过 BX 指令完成。
    这样在计算跳转地址时就忽略了 LSB 位。

  • 当函数地址被给定时,链接器总是设置 LSB 位。因此永远不会出现两个不同地址值的同一函数地址;
    不会因此比较这两个不同地址而发现他们不相等。

如上所述, 任何通过函数指针方式的函数调用都使用 BX 指令(只要代码交织特性被使能)。

对此,唯一的问题是如何计算从被调用函数的返回地址值。

对于 ARM 代码, 可以使用如下指令序列很容易实现这一点。


[cpp]  view plain  copy
 print ?
  1. mov lr, pc  
  2. bx  rX  

此处, rX 是存储有函数指针的寄存器名。 上述代码在 Thumb 指令集中不能工作。

这是因为  MOV 指令没有设置 LR 寄存器的 LSB 位。当被调用函数返回时, 它将返回到

ARM 模式而非 Thumb 模式。 相反的,对于 Thumb 模式,编译器生成如下序列:

[cpp]  view plain  copy
 print ?
  1. bl  _call_via_rX  

此处, rX 是存储有函数指针的寄存器名。  _call_via_rX 函数可表示如下:

[cpp]  view plain  copy
 print ?
  1. .thumb_func  
  2. call_via_r0:  
  3. bx  r0  
  4. nop  

BL 指令确保正确的返回地址被存储到 LR 寄存器中; 而 BX 指令将跳转到存储在 R0 中的函数地址,

BX 将根据需要切换模式。

六、caller-super-interworking如何工作

当参数 -mcaller-super-interworking 被指定时, 它改变了 Thumb 编译器生成的代码。

因此所有通过函数指针的调用(包括虚函数调用)都指向一个不同的指令段函数 (stub function)。

此时该生成代码如下:

[cpp]  view plain  copy
 print ?
  1. bl _interwork_call_via_r0  

注意:  编译器并不强制要求 R0 被用来保存函数地址。 任何寄存器皆可保存函数地址。

该 stub 函数可表示如下:

[cpp]  view plain  copy
 print ?
  1.     .code 16  
  2.     .thumb_func  
  3. _interwork_call_via_r0  
  4.     bx  pc  
  5.     nop  
  6.       
  7.     .code 32  
  8.     tst r0, #1  
  9.     stmeqdb r13!, {lr}  
  10.     adreq   lr, _arm_return  
  11.     bx  r0  

该stub函数首先切换至 ARM 模式,这是因为使用 ARM 指令执行某些必要操作要容易许多。

然后测试保存有函数地址的 R0 的 LSB 位是否为1。 如果 LSB=1, 被调用函数进入 Thumb 模式;

随后的 BX 指令将在调用该函数前切换至 Thumb 模式。

(注意: 被调用函数选择如何返回其调用者并不重要, 因为调用者和被调用者都是 Thumb 函数并且

模式切换是必要的。)

如果被调用函数是一个 ARM 函数,该 stub 函数设置返回地址的 LSB 位后将地址压栈, 用一个被称为

‘“_arm_return”的代码地址替换返回地址值, 然后执行 BX 指令来调用该函数。

"__arm_return" 代码示例如下:

[cpp]  view plain  copy
 print ?
  1. .code 32  
  2. arm_return:       
  3. ldmia   r13!, {r12}  
  4. bx  r12  
  5. .code 16  

该代码简单从栈中获取返回地址, 然后执行一条 BX 指令返回到调用者并且切换回 Thumb 模式。

七、callee-super-interworking如何工作

当参数 -mcaller-super-interworking 被指定时, Thumb 编译器假定它编译的每一个外部可见函数都被指定

有 (interfacearm) 属性。该属性的功能是放置一个特定的, ARM 模式头信息到该函数中。该函数能强制返回

到 Thumb 模式。

没有 __attribute__((interfacearm)):

[cpp]  view plain  copy
 print ?
  1.     .code 16  
  2.     .thumb_func  
  3. function:  
  4.     ... start of function ...  

具有 __attribute__((interfacearm)):
[cpp]  view plain  copy
 print ?
  1. .code 32  
  2. unction:  
  3. orr r12, pc, #1  
  4. bx  r12  
  5.   
  6. .code 16  
  7.               .thumb_func  
  8.       .real_start_of_function:  
  9.   
  10. ... start of function ...  

注意: 既然函数现在希望进入 ARM 模式,它不再带有针对其函数名的 .thumb_func 伪指令。

相应地, .thumb_func 伪指令被标记到一个新的标号  .real_start_of_<name> 上。(此处  <name> 是函数名 )

这样确实会带来副作用: 如果这个函数被从当前文件之外的 Thumb 代码中被调用, 链接器将产生一个

调用指令段以便从 Thumb 模式切换至 ARM 模式; 然后此指令段立即被能切换回 Thumb 模式的函数头部信息所覆盖。


而且,即使没有使用 -mthumb-interwork 编译开关进行编译,(interfacearm) 属性也强制了函数通过 BX 指令来返回 。

因此正确的模式将在函数退出时进行恢复。

八、代码示例

假定有如下测试文件:

[cpp]  view plain  copy
 print ?
  1. int func (void) { return 1; }  
  2.   
  3. int call (int (* ptr)(void)) { return ptr (); }  

根据不同的编译命令行参数,可以生成下列汇编器代码片断:


  • 无任何编译参数

[cpp]  view plain  copy
 print ?
  1. @ Generated by gcc cygnus-2.91.07 980205 (gcc-2.8.0 release) for ARM/pe  
  2.     .code   16  
  3.     .text  
  4.     .globl  _func  
  5.     .thumb_func  
  6. _func:  
  7.     mov r0, #1  
  8.     bx  lr  
  9.   
  10.     .globl  _call  
  11.     .thumb_func  
  12. _call:  
  13.     push    {lr}  
  14.     bl  __call_via_r0  
  15.     pop {pc}  

注意: 两个函数具有不同的退出方式。

特别地, cal() 函数使用 pop {pc} 方式返回。如果调用者是 ARM 函数, 它将不能工作。

  • 使用编译参数 -mthumb-interwork

[cpp]  view plain  copy
 print ?
  1. @ Generated by gcc cygnus-2.91.07 980205 (gcc-2.8.0 release) for ARM/pe  
  2.     .code   16  
  3.     .text  
  4.     .globl  _func  
  5.     .thumb_func  
  6. _func:  
  7.     mov r0, #1  
  8.     bx  lr  
  9.   
  10.     .globl  _call  
  11.     .thumb_func  
  12. _call:  
  13.     push    {lr}  
  14.     bl  __call_via_r0  
  15.     pop {r1}  
  16.     bx  r1  

两个函数都是通过 BX 指令返回。这意味着相比不支持代码交织的版本, 

call() 函数现在增加了2字节长度,并且执行慢几个机器周期。

  • 使用编译参数 -mcaller-super-interworking

[cpp]  view plain  copy
 print ?
  1. @ Generated by gcc cygnus-2.91.07 980205 (gcc-2.8.0 release) for ARM/pe  
  2.     .code   16  
  3.     .text  
  4.     .globl  _func  
  5.     .thumb_func  
  6. _func:  
  7.     mov r0, #1  
  8.     bx  lr  
  9.   
  10.     .globl  _call  
  11.     .thumb_func  
  12. _call:  
  13.     push    {lr}  
  14.     bl  __interwork_call_via_r0  
  15.     pop {pc}  

非常类似于第一个 non-interworking 的版本, 除了一个不同的stub被用于函数指针调用。

注意:汇编代码中 call() 函数不支持代码交织,因此不应该被从 ARM 代码中调用。

  • 使用编译参数 -mcallee-super-interworking

[cpp]  view plain  copy
 print ?
  1. @ Generated by gcc cygnus-2.91.07 980205 (gcc-2.8.0 release) for ARM/pe  
  2.     .code   16  
  3.     .text  
  4.     .globl  _func  
  5.     .code   32  
  6. _func:  
  7.     orr r12, pc, #1  
  8.     bx  r12  
  9.     .code   16  
  10.     .globl .real_start_of_func  
  11.     .thumb_func  
  12. .real_start_of_func:  
  13.     mov r0, #1  
  14.     bx  lr  
  15.   
  16.     .globl  _call  
  17.     .code   32  
  18. _call:  
  19.     orr r12, pc, #1  
  20.     bx  r12  
  21.     .code   16  
  22.     .globl .real_start_of_call  
  23.     .thumb_func  
  24. .real_start_of_call:  
  25.     push    {lr}  
  26.     bl  __call_via_r0  
  27.     pop {r1}  
  28.     bx  r1  

此时,两个函数都具有 ARM 编码方式。 两个函数都通过 BX 指令返回。

这些函数支持代码交织,因此可以安全的被 ARM 代码调用。

Call() 函数现在比原始的不支持代码交织的版本大10字节,相比增加了超过200%的大小。


如果稍微改变源代码,让 call() 函数具有一个 (interfacearm) 属性:

[cpp]  view plain  copy
 print ?
  1. int func (void) { return 1; }  
  2. int call () __attribute__((interfacearm));  
  3. int call (int (* ptr)(void)) { return ptr (); }  
  4. int main (void) { return printf ("result: %d\n", call (func)); }  

  • 无任何编译参数

[cpp]  view plain  copy
 print ?
  1. @ Generated by gcc cygnus-2.91.07 980205 (gcc-2.8.0 release) for ARM/pe  
  2.     .code   16  
  3.     .text  
  4.     .globl  _func  
  5.     .thumb_func  
  6. _func:  
  7.     mov r0, #1  
  8.     bx  lr  
  9.   
  10.     .globl  _call  
  11.     .code   32  
  12. _call:  
  13.     orr r12, pc, #1  
  14.     bx  r12  
  15.     .code   16  
  16.     .globl .real_start_of_call  
  17.     .thumb_func  
  18. .real_start_of_call:  
  19.     push    {lr}  
  20.     bl  __call_via_r0  
  21.     pop {r1}  
  22.     bx  r1  
  23.   
  24.     .globl  _main  
  25.     .thumb_func  
  26. _main:  
  27.     push    {r4, lr}  
  28.     bl  ___gccmain  
  29.     ldr r4, .L4  
  30.     ldr r0, .L4+4  
  31.     bl  _call  
  32.     add r1, r0, #0  
  33.     add r0, r4, #0  
  34.     bl  _printf  
  35.     pop {r4, pc}  
  36. .L4:  
  37.     .word   .LC0  
  38.     .word   _func  
  39.   
  40.     .section .rdata  
  41. .LC0:  
  42.     .ascii  "result: %d\n\000"  

此时, call() 能够被不支持代码交织的 ARM 代码所调用。 当程序被汇编时, 汇编器检测到 main() 函数正在以 Thumb 模式调用 call() 函数,因此自动调整 BL 指令以指向 call() 的真实入口。


  • .text 段的反汇编如下:

[cpp]  view plain  copy
 print ?
  1. 00000028 <_main>:  
  2.   28:   b530        b530    push    {r4, r5, lr}  
  3.   2a:   fffef7ff    f7ff    bl  2a <_main+0x2>  
  4.   2e:   4d06        4d06    ldr r5, [pc, #24]   (48 <.L7>)  
  5.   30:   ffe8f7ff    f7ff    bl  4 <_doit>  
  6.   34:   1c04        1c04    add r4, r0, #0  
  7.   36:   4805        4805    ldr r0, [pc, #20]   (4c <.L7+0x4>)  
  8.   38:   fff0f7ff    f7ff    bl  1c <.real_start_of_call>  
  9.   3c:   1824        1824    add r4, r4, r0  
  10.   3e:   1c28        1c28    add r0, r5, #0  
  11.   40:   1c21        1c21    add r1, r4, #0  
  12.   42:   fffef7ff    f7ff    bl  42 <_main+0x1a>  

你可能感兴趣的:(ARM / Thumb 指令混合编程之代码交织 ( interworking ))