嵌入式程序优化(1)——内嵌arm汇编

1. 内嵌汇编介绍

内嵌汇编是代码优化时的常见手段,它是指在 C代码 中嵌入汇编代码,从而使得代码更加紧凑,避免一些无效操作,有时能够满足一些特殊的代码需求,这也是为后面的neon优化做基础准备。笔者觉得掌握内嵌汇编是一名嵌入式工程师应该必备的技能,进行优化代码,退能看汇编调bug,实属码农居家必备良技

PS:本文需要有一定的裸机汇编基础才能阅读

2. 内嵌汇编语法

不同的C编译器内联汇编代码时,它们的写法是各不相同的,下面为gcc写法

内嵌汇编语法如下,其中汇编语句模板必不可少,其他三部分可选,如果使用了后面的部分,而前面部分为空,也需要用: (冒号)格开,相应部分内容为空

语法如下:

asm(
    (asm code)
    :(output)
    :(input) 
    :(clobber)
)
  • asm: __asm__或asm用来声明一个内联汇编表达式,所以任何一个内联汇编表达式都以它开头,是必不可少的。

  • asm code: 由汇编语句序列组成,语句之间使用“;”分开,指令中的操作数可以使用占位符引用C语言变量,操作数占位符最多10个,名称如下:%0,%1…,%9

  • output: 输出部分描述输出操作数,不同的操作数描述符之间用逗号格开,每个操作数描述符由限定字符串和C语言变量组成。每个输出操作数的限定字符串必须包含“=”表示他是一个输出操作数。

  • input: 输入部分描述输入操作数,不同的操作数描述符之间使用逗号格开,每个操作数描述符由限定字符串和C语言表达式或者C语言变量组成。

  • clobber: 这部分常常以“memory”为约束条件,以表示操作完成后内存中的内容已有改变,如果原来某个寄存器的内容来自内存,那么现在内存中这个单元的内容已经改变(参考3.3章节)

需要注意的是,使用 asm 关键字声明一段汇编代码后, 可以使用 volatile 向GCC声明不允许对该内联汇编优化,否则,当使用优化选项(-o)进行编译时GCC会根据字自己的判断决定是否将内联汇编表达式的指令优化掉。

下面会通过简单的例子来解释

3. 内嵌汇编要素

3.1 操作符

操作符通常用于声明内嵌汇编中变量的属性

  • r: 通用寄存器r0-r15,表示需要将变量与某个通用寄存器相关联,先将变量的值读入寄存器,然后在指令中使用相应寄存器

  • m: 一个有效的内存地址,表示操作数是在内存中,在编译时将直接使用内存中的变量,而不会像"r"操作符一样将其读入寄存器,一般是使用对内存进行操作的指令时会才使用

  • I: 数据处理指令中的立即数

  • X: 被修饰的操作符只能用作输出

3.2 修饰符

修饰符通常用于声明操作符支持的属性

  • 无修饰符:此类操作符是只读的,输入部分必须为read-only,C编译器是没有能力做这个检查

  • =: 被修饰的操作符只写, 输出部分必须为write-only,相应C表达式的值必须是左值,C编译器是没有能力做这个检查

  • +: 被修饰的操作符具有可读可写的属性

  • &: 被修饰的操作符能被作为输出

3.3 clobber

clobber 简单来说就是声明了哪些变量或者寄存器被修改了,从而告知编译器的编译代码的时候不要使用该寄存器或者变量, 避免发生不可预知的错误

比如我不想寄存器使用r0,则在clobber中声明r0,那么编译器则会选择其他寄存器

常用的 clobber 如下:

  • memory: 表示修改了内存中的数据,这将强迫编译器在执行汇编代码前存储所有缓存的值,然后在执行完汇编代码后重新加载该值

  • rn: 表示修改了寄存器rn(n寄存器标号)

  • cc: 表示修改了cpsr等标志类寄存器,它用来向编译器指明,内嵌汇编指令改变了内存中的值。这将强迫编译器在执行汇编代码前存储所有缓存的值,然后在执行完汇编代码后重新加载该值

4. 内嵌汇编例子讲解

4.1 mem_to_mem

通过一个简单的例子来理解上面的语法, 该例子的作用是使用汇编将变量值在内存中移动

void asm_mem_to_mem(void) 
{ 
    unsigned int var_1 = 0;
    unsigned int var_2 = 999;
    unsigned int var_3 = 0;

    __asm__ __volatile__
    (
        
        "mov %0, #20;" //asm code
        "mov %2, %1;" 
        : "=r" (var_1), "=r"(var_3)
        : "r" (var_2)
        : "memory"
    ); 
}
  • asm code : 其中%0、%1等均表示变量
  • output: 声明输出变量,操作符 r 表示使用任何寄存器,修饰符 = 号 表示只写,如果不添加 = 号,编译会提示 output operand constraint lacks ‘=’
  • input : 声明输出变量,操作符 r 表示使用任何寄存器来传递变量
  • clobber:memory 表示修改了内存

使用操作符r来传递变量的意思指内存中变量的值如何进入汇编中进行运算,因为 arm 汇编的所有计算都是在寄存器中进行的,不能够直接从内存中进行计算,所以需要先将 变量 从内存中读取到寄存器中,而传递变量的意思是使用 ldr 指令时,需要指定寄存器来装载变量,那么使用 操作符r 来声明该变量可以使用任何寄存器来装载

4.2 register_to_mem

读取寄存器中的值到内存

void asm_register_to_mem(int param_0, int param_1, int param_2)

{ 

    int var_0 = 0; 
    int var_1 = 0; 
    int var_2 = 0; 

    /* 1. 错误例子,没有声明寄存器被修改 */

    __asm__ __volatile__
    ( 
      "str r0, [%0];" //r0 -> var_0
      "str r1, [%1];" //r1 -> var_1
      "str r2, [%2];" //r2 -> var_2
      :
      :"r"(&var_0), "r"(&var_1), "r"(&var_2)
      :"memory"
    ); 

    /*
        0x000103c8 <+4>:  add r7, sp, #0

        0x000103ca <+6>:  str r0, [r7, #12]

        0x000103cc <+8>:  str r1, [r7, #8]

        0x000103ce <+10>:  str r2, [r7, #4]

        0x000103d0 <+12>:  movs  r3, #0

        0x000103d2 <+14>:  str r3, [r7, #28]

        0x000103d4 <+16>:  movs  r3, #0

        0x000103d6 <+18>:  str r3, [r7, #24]

        0x000103d8 <+20>:  movs  r3, #0

        0x000103da <+22>:  str r3, [r7, #20]

        0x000103dc <+24>:  add.w  r3, r7, #28

        0x000103e0 <+28>:  add.w  r2, r7, #24//r2被修改

        0x000103e4 <+32>:  add.w  r1, r7, #20//r1被修改

        0x000103e8 <+36>:  str r0, [r3, #0]

        0x000103ea <+38>:  str r1, [r2, #0]

        0x000103ec <+40>:  str r2, [r1, #0]

        从汇编代码可知, 因为没有在 clobber 中声明寄存器被修改
        所以编译器在汇编代码时使用了装有 变量 的寄存器 r1、r2,导致 r1、r2 中的值被修改,从而得不到正确值
    */


    /* 2. 正确例子 */

    __asm__ __volatile__
    ( 
      "str r0, [%0];" //r0 -> var_0
      "str r1, [%1];" //r1 -> var_1
      "str r2, [%2];" //r2 -> var_2
      :
      :"r"(&var_0), "r"(&var_1), "r"(&var_2)
      :"memory", "r0", "r1", "r2"
    ); 

    /*
        0x0001046c <+4>:  add r7, sp, #8

        0x0001046e <+6>:  str r0, [r7, #12]

        0x00010470 <+8>:  str r1, [r7, #8]

        0x00010472 <+10>:  str r2, [r7, #4]

        0x00010474 <+12>:  movs  r3, #0

        0x00010476 <+14>:  str r3, [r7, #28]

        0x00010478 <+16>:  movs  r3, #0

        0x0001047a <+18>:  str r3, [r7, #24]

        0x0001047c <+20>:  movs  r3, #0

        0x0001047e <+22>:  str r3, [r7, #20]

        /* r0没有被修改,编译器转而使用r3来进行操作 */
        0x00010480 <+24>:  add.w  r3, r7, #28 

        /* r1没有被修改,编译器转而使用r4来进行操作 */
        0x00010484 <+28>:  add.w  r4, r7, #24 

        /* r2没有被修改,编译器转而使用r5来进行操作 */
        0x00010488 <+32>:  add.w  r5, r7, #20 

        /* 将r0的值装入内存中的变量 */
        0x0001048c <+36>:  str r0, [r3, #0] 

        /* 将r1的值装入内存中的变量 */
        0x0001048e <+38>:  str r1, [r4, #0] 

        /* 将r2的值装入内存中的变量 */
        0x00010490 <+40>:  str r2, [r5, #0] 
    */



  

    /* 3. 错误例子,没有声明寄存器被修改*/

    __asm__ __volatile__
    ( 
      "mov %0, r0;" //r0 -> var_0
      "mov %1, r1;" //r1 -> var_1
      "mov %2, r2;" //r2 -> var_2
      :"=r"(var_0), "=r"(var_1), "=r"(var_2) //输入部分,表示变量var输入到r0-r15中的一个寄存器 
      :
      :"memory"
    ); 

    /*
        0x00010470 <+8>:  str r1, [r7, #8]

        0x00010472 <+10>:  str r2, [r7, #4]

        0x00010474 <+12>:  movs  r3, #0

        0x00010476 <+14>:  str r3, [r7, #28]

        0x00010478 <+16>:  movs  r3, #0

        0x0001047a <+18>:  str r3, [r7, #24]

        0x0001047c <+20>:  movs  r3, #0

        0x0001047e <+22>:  str r3, [r7, #20]

        0x00010480 <+24>:  mov r1, r0 //r0被修改

        0x00010482 <+26>:  mov r2, r1 //r1被修改

        0x00010484 <+28>:  mov r3, r2 //r2被修改

        0x00010486 <+30>:  str r1, [r7, #28]

        0x00010488 <+32>:  str r2, [r7, #24]

        0x0001048a <+34>:  str r3, [r7, #20]

        0x0001048c <+36>:  ldr r3, [r7, #20]

        0x0001048e <+38>:  str r3, [sp, #4]

        0x00010490 <+40>:  ldr r3, [r7, #24]

        0x00010492 <+42>:  str r3, [sp, #0]

        0x00010494 <+44>:  ldr r3, [r7, #28]
    */



    /* 4. 正确例子*/

    __asm__ __volatile__
    ( 
       "mov %0, r0;" //r0 -> var_0
       "mov %1, r1;" //r1 -> var_1
       "mov %2, r2;" //r2 -> var_2
       :"=r"(var_0), "=r"(var_1), "=r"(var_2) //输入部分,表示变量var输入到r0-r15中的一个寄存器 
       :
       /* 告诉编译器,寄存器r0, r1, r2已经被修改了。不要将r0, r1, r2用于操作 */
       :"memory", "r0", "r1", "r2"
    ); 

    /*
        0x000103c6 <+2>:  sub sp, #36 ; 0x24

        0x000103c8 <+4>:  add r7, sp, #0

        0x000103ca <+6>:  str r0, [r7, #12]

        0x000103cc <+8>:  str r1, [r7, #8]

        0x000103ce <+10>:  str r2, [r7, #4]

        0x000103d0 <+12>:  movs  r3, #0

        0x000103d2 <+14>:  str r3, [r7, #28]

        0x000103d4 <+16>:  movs  r3, #0

        0x000103d6 <+18>:  str r3, [r7, #24]

        0x000103d8 <+20>:  movs  r3, #0

        0x000103da <+22>:  str r3, [r7, #20]  

        0x000103dc <+24>:  mov r5, r0    

        0x000103de <+26>:  mov r4, r1 //r1没有被修改,编译器转而使用r4来进行操作

        0x000103e0 <+28>:  mov r3, r2 //r2没有被修改,编译器转而使用r3来进行操作

        0x000103e2 <+30>:  str r5, [r7, #28]

        0x000103e4 <+32>:  str r4, [r7, #24]

        0x000103e6 <+34>:  str r3, [r7, #20]
    */
}

4.2 mem_to_cpsr

修改 cpsr寄存器 中的值

void asm_mem_to_cpsr(void)

{

  int cpsr_status = 0x80;

  __asm__ __volatile__
  (
    "msr cpsr,%0"
    : 
    : "r" (cpsr_status)//声明输入
    : "cc"
  ); 

/*  汇编代码

    0x000103d6 <+2>:  sub sp, #12

    0x000103d8 <+4>:  add r7, sp, #0

    0x000103da <+6>:  movs  r3, #128  ; 0x80

    0x000103dc <+8>:  str r3, [r7, #4]

    0x000103de <+10>:  ldr r3, [r7, #4]

    0x000103e0 <+12>:  msr CPSR_fc, r3

    0x000103e4 <+16>:  nop

*/

}

4.3 cpsr_to_mem

读取 cpsr寄存器 中的值

void asm_cpsr_to_mem(void)

{

  int cpsr_status = 0;

  asm 
  (
    "mrs %0, cpsr;"
    :"=r" (cpsr_status)//声明输出
    :
    : "memory"//告诉编译器内存已经被修改了

  );

}

4.4 内存屏障

# define barrier() _asm__volatile_("": : :"memory")

内存屏障向GCC声明,内存做了改动,GCC在编译的时候,会将此因素考虑进去。在访问IO端口和IO内存时,会用到内存屏障。它就是防止编译器对读写IO端口和IO内存指令的优化而实际的错误。

其原理是保留程序的执行顺序,因为在使用了带有 memory clobberasm 声明后,所有变量的内容都是不可预测的。编译器将按顺序编译语句

5. 调试流程

笔者使用的是经典的调试器 gdb 进行调试, 其步骤如下

  1. 设置断点在main函数 b main

  2. 将程序运行到 asm_test_input 处 run

  3. 汇编级执行 stepi n(n是运行的步数,如果不写则默认1步)

  4. 打印内存 x /nxw addr (笔者使用命令来打印堆栈中的数值,其中n是打印个数,具体请参靠gdb的命令说明)

  5. 打印寄存器 info register

  6. 打印汇编 disassemble

6. 参考资料

ARM嵌入式开发中的GCC内联汇编asm:https://www.cnblogs.com/fengliu-/p/7667892.html
asm volatile内嵌汇编用法简述:https://blog.csdn.net/geekcome/article/details/6216436
ARM汇编-从内嵌汇编开始:https://blog.csdn.net/u011298001/article/details/83864516
ARM GCC 内嵌(inline)汇编手册:http://blog.chinaunix.net/uid-20543672-id-3194385.html

你可能感兴趣的:(嵌入式程序优化(1)——内嵌arm汇编)