ARM汇编1:如何在C语言中使用汇编

如何在C语言中使用汇编语言

我最近对ARM的NEON编程有兴趣,主要是为了想学习一些矩阵计算加速相关的知识。但是我又不想写纯粹的汇编语言,我想在C语言中嵌入汇编来使用。

经过检索学习,我找到两种可行的方式。我在阅读ncnn代码的时候,发现下面这两种方式都有用到。为了后续能愉快的阅读ncnn代码,将相关知识做个简单的整理。

方式1: Neon Intrinsics(Neon内联)

NEON intrinsics可以视作在NEON指令上面封装了一层c语言接口。
Neon的汇编指令和寄存器不熟的话,可以借助arm_neon.h这个头文件,以近似C语言编程的方式调用Neon的功能。
下面是一个简单的例子:

#include "arm_neon.h"

void rgb_deinterleave_neon(uint8_t *r, uint8_t *g, uint8_t *b, uint8_t *rgb, int len_color) {
    /*
     * Take the elements of "rgb" and store the individual colors "r", "g", and "b"
     */
    int num8x16 = len_color / 16;
    uint8x16x3_t intlv_rgb;
    for (int i=0; i < num8x16; i++) {
        intlv_rgb = vld3q_u8(rgb+3*16*i);
        vst1q_u8(r+16*i, intlv_rgb.val[0]);
        vst1q_u8(g+16*i, intlv_rgb.val[1]);
        vst1q_u8(b+16*i, intlv_rgb.val[2]);
    }
}

arm_neon.h定义了一些特定的数据类型,这些数据类型可以被映射到Neon的专用寄存器,arm_neon.h里定义的一些C语言函数可以对这些特殊类型进行操作。基本上所有的Neon指令都有对应的函数。
这种方式仅限于使用Neon的功能,那些普通的汇编指令是没办法使用的。
使用GCC编译Neon Intrinsics代的时候,需要加上一些特殊选项。本文着重介绍第二种方法,这里就不详细介绍了。

方式2: 内联汇编

这种方式就是用GCC的内联汇编的机制来在C语言中嵌入一段汇编代码。这个机制不是针对ARM的,GCC支持的所有硬件平台(如X86)都可以使用。
下面是我从libyuv中复制过来的一个完整函数:

void ScaleARGBRowDownEven_NEON(const uint8_t* src_argb,
                               ptrdiff_t src_stride,
                               int src_stepx,
                               uint8_t* dst_argb,
                               int dst_width) {
  (void)src_stride;
  asm volatile(
      "1:                                        \n"
      "ld1         {v0.s}[0], [%0], %3           \n"
      "ld1         {v0.s}[1], [%0], %3           \n"
      "ld1         {v0.s}[2], [%0], %3           \n"
      "ld1         {v0.s}[3], [%0], %3           \n"
      "subs        %w2, %w2, #4                  \n"  // 4 pixels per loop.
      "prfm        pldl1keep, [%0, 448]          \n"  // prefetch 7 lines ahead
      "st1         {v0.16b}, [%1], #16           \n"
      "b.gt        1b                            \n"
      : "+r"(src_argb),                // %0
        "+r"(dst_argb),                // %1
        "+r"(dst_width)                // %2
      : "r"((int64_t)(src_stepx * 4))  // %3
      : "memory", "cc", "v0");
}

可以看到这种方式可以将c语言的变量传入汇编代码,并用汇编语言修改C语言变量。基本上这段代码可以认为是一个内联函数,有入参,有出参。
至于汇编代码部分,可以是普通的汇编,也可以是neon这类SIMD汇编。无论是ARM、x86还是RISC-V,都可以使用这样的方式来嵌入汇编。
本文后面主要介绍下第二种方式的主要相关知识。

学习资料

上面说到,第二种在C语言嵌入汇编的方式不是某一硬件平台的,而是GCC的一种机制,无论是ARM、x86还是RISC-V,都可以使用这样的方式来嵌入汇编。

所以最权威的资料在GCC的手册中,《gcc10.4.0手册在线版》的6.47小节“How to Use Inline Assembly Language in C Code”就专门介绍了相关知识,但是主要是用x86的例子来举例的。

另外,国外有个博文也是很好的学习材料:GCC-Inline-Assembly-HOWTO
国内有人对它进行了翻译,翻译质量还可以:最牛X的GCC 内联汇编

内联汇编的详细介绍

基本汇编内联

如果汇编代码无需使用C的变量作为输出和输入操作数,则可以使用这种方式。

asm volatile("movl %ecx %eax"); /* 这个好像是x86的汇编,将 ecx 寄存器的内容移至 eax  */

这里汇编可以是一行,也可以是多行。

扩展内联汇编

如果汇编代码需要使用C语言的变量和内存,则需要使用扩展内联汇编的方式。

这种方式的格式为:

asm asm-qualifiers ( AssemblerTemplate
	: OutputOperands
	[ : InputOperands
	[ : Clobbers ] ])

翻译一些下就是:

asm 限定符( 汇编程序模板
	: 输出操作数 /* 可选 */
	: 输入操作数 /* 可选 */
	: 修饰寄存器列表 /* 可选 */
);

其中限定符有三个:volatile、inline、goto。
其中常用的就是volatile,我在libyuv中看到的都是volatile,所以就不考虑其它的了。
如果GCC的优化器确定不需要输出变量,有时会丢弃asm语句。此外,如果优化器认为代码总是返回相同的结果(即在调用之间没有任何输入值改变),则可能会将代码移出循环。使用volatile限定符将禁用这些优化。

汇编程序模板

汇编程序模板是一个包含汇编程序指令的字符串。字符串可以包含汇编程序识别的任何指令。GCC不解析汇编程序指令本身,不知道它们的含义,甚至不知道它们是否是有效的汇编程序输入。

每条指令应以分界符结尾。对于ARM汇编,一般用"\n"或者"\n\t"来分割各行代码。

输出和输入操作数

内联汇编语句可以有零个或多个输出操作数,指示被汇编代码修改的C变量的名称。
输入操作数也类似,可以为空。有多个操作数的时候,需要以逗号分割,为空的时候冒号也不能省。输出操作数表达式必须是左值。

下面的例子有三个输出操作数,没有输入操作数。

void CopyRow_NEON(const uint8_t* src, uint8_t* dst, int width) {
  asm volatile(
      "1:                                        \n"
      "ldp         q0, q1, [%0], #32             \n"
      "prfm        pldl1keep, [%0, 448]          \n"
      "subs        %w2, %w2, #32                 \n"  // 32 processed per loop
      "stp         q0, q1, [%1], #32             \n"
      "b.gt        1b                            \n"
      : "+r"(src),                  // %0
        "+r"(dst),                  // %1
        "+r"(width)                 // %2  // Output registers
      :                             // Input registers
      : "cc", "memory", "v0", "v1"  // Clobber List
  );
}

在汇编代码中使用操作数有两种方式,一种就是类似上面的代码,"%num"这种占位符的方式来使用。其中num是从0 开始的偏移量,第一个操作数就是%0, 第三个就是%2。
输出和输入操作数共用这个数字的范围。如果上面的代码增加一个输入操作数的话,则汇编代码可以用%3来指代它。

操作数前面的字符串里属于约束修饰符,常用的有"r"和"m"这两种来指定存储方式:

"r" : 寄存器操作数约束
"m" : 内存操作数约束,该操作数不会通过寄存器中转

其中"r"修饰的操作数会被gcc分配一个寄存器来存储,能加快访问速度。
另外常用的约束修饰符还有(指定读写操作):

"+":表示该操作数同时被指令读写
"=" : 意味着对于这条指令,操作数为只写的;旧值会被忽略并被输出数据所替换。
"&" : 在所有不能与输入重叠的输出操作数上使用' & '约束修饰符。否则,GCC可能会将输出操作数分配到与不相关的输入操作数相同的寄存器中,假设汇编代码在产生输出之前消耗其输入。

约束修饰符可以组合,比如: “+r”、“=m”、“+&r” 。
当您列出多个可能的位置(例如,“=rm”)时,编译器将根据当前上下文选择最有效的位置。

我个人理解:下为啥要写清楚读写操作,因为处理器的存储是分层的,寄存器最快,后面是cache、内存和磁盘。对于只写的,就不用在运行汇编程序前从内存同步到cache或寄存器了,如果是只读的,程序运行结束就不用同步到内存了。总之可以节省点拷贝和同步的时间,并确保内容的正确性。)

总共的操作数数量上限是30。如果使用’ + '约束修饰符的操作数作为两个操作数计数(即同时作为输入和输出)。

修饰寄存器列表 (Clobbers)

每个clobber列表项都是一个用双引号括起来并用逗号分隔的字符串常量。通常都是汇编代码用到的寄存器的名字。
当编译器选择使用哪个寄存器来表示输入和输出操作数时,它会避开clobered寄存器。

上面clobber列表项有两个比较奇怪的项:“memory"和"cc”。

"cc" :表示汇编程序代码修改了标志寄存器。

"memory" :告诉编译器,程序集代码对输入和输出操作数中列出的项以外的项执行内存读写(例如,访问输入参数之一所指向的内存)。为了确保内存包含正确的值,GCC可能需要在执行asm之前将特定的寄存器值刷新到内存中。

总之记住,这两个是最常用的,理解不了也没关系,先这么用。

参考资料

NEON Programmer’s Guide

Learn the architecture - Optimizing C code with Neon intrinsics

ARM底层汇编优化之NEON优化 - 概述(基础入门 )

GCC-Inline-Assembly-HOWTO

最牛X的GCC 内联汇编

ARM汇编语言入门

gcc10.4.0手册在线版v

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值