美文网首页
嵌入式程序优化(1)——内嵌arm汇编

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

作者: wipping的技术小栈 | 来源:发表于2019-11-10 17:32 被阅读0次

    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内联汇编asmhttps://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汇编

          本文链接:https://www.haomeiwen.com/subject/osuabctx.html