美文网首页
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