美文网首页
GNU C内嵌汇编语言

GNU C内嵌汇编语言

作者: 半笔闪 | 来源:发表于2020-01-14 15:28 被阅读0次

    大部分的移动端深度学习框架都会使用到neon优化对深度算法的算子进行优化,有些使用了Neon Instruction C进行优化,而更多的可能是使用嵌入汇编进行优化,本文就介绍一下如何在C中嵌入汇编。当然C嵌入汇编还有很多场景,很多操作系统开发场景中,C是无法完全代替汇编语言的。例如操作某些特殊的CPU寄存器、操作主板上的某些IO端口或者对性能要求极其苛刻的场景等,我们都需要在C中嵌入汇编来满足要求。
    GUN C语言提供了关键词asm来声明代码是内嵌的汇编语句,如下(摘自NCNN前向推理框架中的arm优化代码):

     if (nn > 0)
            {
            asm volatile(
                "0:                             \n"
                "pld        [%1, #128]          \n"
                "vld1.f32   {d0-d1}, [%1: 128]  \n"
    
                "vmax.f32   q0, q0, %q4         \n"
                "vmin.f32   q0, q0, %q5         \n"
    
                "subs       %0, #1              \n"
                "vst1.f32   {d0-d1}, [%1: 128]! \n"
    
                "bne        0b                  \n"
    
                : "=r"(nn),     // %0
                  "=r"(ptr)     // %1
                : "0"(nn),
                  "1"(ptr),
                  "w"(_min),    // %q4
                  "w"(_max)     // %q5
                : "cc", "memory", "q0"
            );
            }
    
    1. asm关键字:用于声明这行代码是一个内嵌汇编表达式,它是关键词asm的宏定义(#define asm asm)。它是内嵌汇编语言必不可少的关键字,任何内嵌的 汇编表达式都以次关键字作为开头;如果希望编写符合ANSI C标准的程序(即与ANSI C标准兼容),那么建议使用关键字asm
    2. volatile关键字:其作用是告诉编译器此行代码不能被编译器优化,编译时保持代码原状。由此看来,它也是内嵌汇编语言不可或缺的关键字,否则经过编译器优化后,汇编语句很可能被修改以至于无法达到预期的执行效果。如果期望编写符合ANSI C标准的程序(即与ANSI C标准兼容),那么建议使用关键字volatile

    内嵌汇编表达式

    C语言中嵌入汇编需要显式的标明寄存器的分配情况、与C程序的融合情况等。主要靠内嵌汇编表达式说明。表达式主要由4部分构成,它们分别由“:”号分隔,如上ncnn代码所示,其完整的格式如下:
    指令部分:输出部分:输入部分:损坏部分
    如果将内嵌汇编表达式当作函数,指令部分时函数中的代码,输入部分用于向函数传入参数,而输出部分则可以理解为函数的返回值。损坏部分描述了在指令部分执行过程中,将被修改的寄存器、内存空间或标志寄存器,并且这些修改部分并未在输出部分和输入部分出现过,格式为:
    “损坏描述”,“损坏描述” ...
    如果需要声明多个寄存器,则必须使用逗号‘,’将它们分隔开,这点与输入/输出部分一致。

    操作约束和修饰符

    每个输入/输出表达式都必须指定自身的操作约束。操作约束的类型可以细分为寄存器约束、内存约束和立即数约束。在输出表达式中,还有限定寄存器操作的修饰符。

    内嵌汇编的语法及最常用的arm汇编指令

    1. 在c里内嵌汇编
             __asm__ __volatile__(
             "汇编代码 \n"              
             "汇编代码 \n"
        :"=r"(c变量名)    //第一个冒号表示从汇编里输出到c语言的变量, =号表示在汇编里只能改变C变量的值,而不能取它的值. +号表示可以取变量值,也可改变变量的值. r表示在汇编里用一个寄存器代替c变量
     
        :"r"(c变量名) //第二个冒号表示汇编里只能取c变量的值, 不能再有"=","+"号
            //输入的变量的寄存器只能使用一次, 如果多次使用此输入的值,则应放到一个固定的寄存器上面(R0-R12)
     
        :"r0", "r1" //第三个冒号表示告诉编译器不要把r0, r1寄存器分配给%0, %1等
            );  
        // __volatile__ 告诉编译器不要优化下面的汇编代码, 可用可不用
    2. (mov rd, #立即数或寄存器), 把立即数或寄存器的值给rd寄存器
    //立即数受限制: 4位表示移位, 8位表示数字:  0x56000000   == 0x56 << 24;
    //注意,只有MOV指令才可以把一个寄存器上的值搬移到另一个寄存器
     
    3. ldr  rd, =立即数, 立即数没限制, //把立即数放入rd寄存器 ,  ldr不能用于寄存器给寄存器赋值
     
    4. add  rd, r0, #立即数或寄存器  //r0+#立即数或另一寄存的值放入rd寄存器   
                      //add  r0, #4  --> add   r0, r0, #4
     
    5. sub  rd, 被减数, 减数 //被减数必须是寄存器, 减数可以是立即数或寄存器
     
    6. mul  rd, rm, rn //rd和rm不能同一个寄存器(旧版本编译器)
     
    //////////////////////////////////////////
    CPSR / SPSR  (架构手册P49)
    31   30    29     28    7   6     4--0  
    N    Z     C      V     I   F   M[4:0]
                        工作模式位(架构手册P52)
     
    I bit: Disables IRQ interrupts when it is set.
    F bit: Disables FIQ interrupts when it is set.
     
    N bit: 表示指令里目标寄存器的结果为负数时, N=1, 如为正数或零时N=0
    Z bit: 如指令运算的结果为0, 则Z=1, 如果结果不是0, 则Z=0
    C bit: 如果指令运算的结果产生进位或借位时, C=1, 如果没有, C=0
    V bit: 如果指令运算的结果溢出时, V=1, 如果没有V=0
     
     
    因cpsr是个特殊的寄存器, 需用特定的汇编指令来读取出来
        mrs r0, cpsr  //把cpsr的值读出放入通用寄存器r0里
     
    默认情况下, 一般的指令都不会根据目标寄存器里的结果来改变cpsr里的值, 需要特定的汇编指令
    才会改变cpsr的值
     
    cmp   // cmp  r0, #1 根据r0减去1的结果来改变cpsr的条件状态位, r0本身的值不变
    movs //根据目标寄存器的结果来改变cpsr的条件状态位
    adds
    subs
    ...
    ///////////////////////////////////////////
     
    7. condition flasg : N(负数)  Z(是否零)  C(进位/借位)  V(溢出)
        conditon: 架构手册/p112
        eq(0000)  :  当cpsr里的Z位的值为1时, 此条件成立, 否则条件不成立, 使用此条件的
               汇编指令不会执行   
        ne(0001)  :  当cpsr里的Z位的值为0时, 此条件成立, 使用此条件的汇编指令会执行
    //如果指令里没有指定条件, 则为条件码所占的位为e
     
    8. cmp Rn, #立即数/寄存器 //根据Rn减去第二个参数的值的结果更新cpsr的条件标志位
     
    9. movs Rd, #5 //先把5放入Rd, 再根据Rd里的结果更新cpsr的条件标志位
     
    10. 移位lsl, lsr 逻辑左移, 右移
        "lsl    r0, r0, #4 \n"
         asr 算术右移(C语言里>>是算术右移)
     
    11. and rd, rn, #立即数或者寄存器 //把两个数位与上后,把结果放入rd寄存器    add rd, #4 == add  rd,  rd, #4
        orr rd, rn, #立即数或者寄存器 //把两个数位或上后,把结果放入rd寄存器
     
        mvn   r0, #4 //把4的反码存入r0寄存器
     
    12. bic rd, rn, op(#立即数或寄存器) //清除rn对应op里的相应位是1的值(可指定多位), 把结果放入rd
     
    13. ldr rd, [rn] //把rn里的值作为地址,然后把地址上面的值放入rd
    14. str r0, [rn] //把rn里的值作为地址, 然后把r0上的值写到rn里的地址上
        注意: ldr/str的"[]"符号里不能写立即数的地址, 必须要把地址保存到寄存器后才可以操作
     
     
    15. ldr/str 操作4个字节   ldrb/strb操作1个字节  ldrh/strh操作2个字节
        在内嵌汇编里, 输入输出的%寄存器要用固定的寄存器来操作,尢其是输入的,一进去应用一个固定的寄存器代替它
     
    16, ldr rd, [rm, rn/#立即数] //把rm寄存器上值加上rn值的结果作为地址,再把地址上值放入rd     
     
    17.  mydata:
         .word 0x789, 0x123 //相当于申请8个字节空间, 前4个字节放的值为0x789, 后4个字节放的值为0x123. mydata就是首地址, mydata本身不会占指令空间
         .word func    //func是个函数,  分配4个字节空间, 装func函数的地址
     
    18  mystr:
        .string "hello"
        .align 2        //.string定义一字符串hello, .align 2按2的2次方字节来对齐, mystr为字符串的首地址
     
     
       r13寄存器就是sp寄存器, 用于记录当前栈顶所在的内存地址
     
        //stm/ldm是批量处理指令, fd为full descend, 意思为sp指向的地址是有数据的, 操作时先下降, 再保存
    19. stmfd  sp!, {r0-r3} //把寄存器r0-r3里的值保存到栈里去,   push {r0-r3}, 压栈顺序r3, r2, r1, r0
        ldmfd  sp!, {r0-r3} //把保存到栈的值恢复到r0-r3寄存器去,  pop {r0-r3}
    

    相关文章

      网友评论

          本文标题:GNU C内嵌汇编语言

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