2024年12月10日 04:51

公告:

欢迎来到havte!站点已升级完成,域名已迁移至dev.havte.com,大家收藏访问dev.havte.com站点。


Linux内核中的内联汇编

作者 admin, 2021年12月08日 17:21

« 上一篇 - 下一篇 »

admin

1. 内联汇编做了什么
在Linux内核中,我们时常见到内嵌汇编语言的函数,向这种在C语言函数内部嵌入汇编代码的做法我们称之为内联汇编。尽管现在的编译器对程序的优化都很优秀了,但在某些对效率要求较高的场合仍需要手动优化,我们暂时先不关心具体的内联汇编的规则,先来看看内联汇编是如何提高效率的。

1.1. 优化对比测试
测试环境:imx6ull

编译器:arm-linux-gnueabihf-gcc

反汇编:arm-linux-gnueabihf-objdump

Makefile:

test: main.c
        arm-linux-gnueabihf-gcc -o $@ $^

        arm-linux-gnueabihf-objdump -D test > test.dis
clean:
        rm test *.dis
1.1.1. 非内联汇编
写测试函数如下: main.c:

程序代码 [选择]
#include <stdio.h>
#include <stdlib.h>

int add(int a, int b)
{
return a + b;
}

int main(int argc, char **argv)
{
int a;
int b;
char *endptr;

if (argc != 3)
{
printf("Usage: %s <val1> <val2>\n", argv[0]);
return -1;
}

a = (int)strtol(argv[1], NULL, 0);
b = (int)strtol(argv[2], NULL, 0);

printf("%d + %d = %d\n", a, b, add(a, b));
}

查看反汇编:

这是我们写的add函数:
程序代码 [选择]
00010404 <add>:
   10404:       b480            push    {r7}
   10406:       b083            sub     sp, #12
   10408:       af00            add     r7, sp, #0
   1040a:       6078            str     r0, [r7, #4]
   1040c:       6039            str     r1, [r7, #0]
   1040e:       687a            ldr     r2, [r7, #4]
   10410:       683b            ldr     r3, [r7, #0]
   10412:       4413            add     r3, r2
   10414:       4618            mov     r0, r3
   10416:       370c            adds    r7, #12
   10418:       46bd            mov     sp, r7
   1041a:       f85d 7b04       ldr.w   r7, [sp], #4
   1041e:       4770            bx      lr

然后在main函数中被调用:

00010420 <main>:
#...省略
10470:       f7ff ffc8       bl      10404 <add>
#...省略
1.1.2. 内联汇编
写测试函数如下: main.c:

程序代码 [选择]
#include <stdio.h>
#include <stdlib.h>

int add(int a, int b)
{
int sum;
__asm__ volatile (
"add %0, %1, %2"
:"=r"(sum)
:"r"(a), "r"(b)
:"cc"
);
return sum;
}

int main(int argc, char **argv)
{
int a;
int b;

if (argc != 3)
{
printf("Usage: %s <val1> <val2>\n", argv[0]);
return -1;
}

a = (int)strtol(argv[1], NULL, 0);
b = (int)strtol(argv[2], NULL, 0);

printf("%d + %d = %d\n", a, b, add(a, b));
return 0;
}

00010474 <add>:
   10474:       1840            adds    r0, r0, r1
   10476:       4770            bx      lr
然后在main函数中被调用:

00010404 <main>:
#...省略
 10454:       f000 f80e       bl      10474 <add>
#...省略
1.2. 结论
可见,实现相同的功能,内联汇编add函数的指令相比纯C语言写的add函数大大简化,效率自然大大提高。

2. 语法规则
2.1. 内联汇编规则
asm [Qualifiers] (
                  ``AssemblerTemplate``
                  : ``OutputOperands``
                  : ``InputOperands``
                  : ``Clobbers`` 
                )

asm [Qualifiers] goto (
                      ``AssemblerTemplate``
                      : /* No outputs. */
                      : ``InputOperands``
                      : ``Clobbers``
                      : ``GotoLabels``
                    )
asm

也可以写作 __asm__ ,如果关键词 asm 和我们程序的一些标识符冲突了,我们可以使用 __asm__ ,表示这是一段内联汇编。

Qualifiers: 包含 volatile 和 inline 这两个限定符

volatile

volatile这一可选的限定符号无效,所有基本的asm块都是隐式的volatile

inline 1

如果使用inline限定符,那么出于内联的目的,asm语句的大小要尽可能的小。

AssemblerTemplate

汇编模板,用双引号包含起来,每条指令都需要使用系统汇编代码中的分隔符进行分隔,如分号" ; "或" \n\t "等, 在大多数地方都可以使用的组合是换行符和制表符(写为" \n\t ")。 有些汇编程序允许分号(;)作为行分隔符。但是,请注意,一些汇编语言使用分号来表示注释的开始。

示例:

"add %0, %1, %2"
:"=r"(sum)
:"r"(a), "r"(b)
%n代表输出操作数和输入操作数中按顺序出现第n个的变量 本示例中:

%0 代表 sum

%1 代表 a

%0 代表 b

OutputOperands

输出操作数,内联汇编执行时,输出的结果保存在哪里。 格式如下,当有多个变量时,用逗号隔开:

[ [asmSymbolicName] ] constraint (cvariablename)

asmSymbolicName 符号名,随便取,也可以不写。 constraint表示约束,有如下常用取值:

约束 constraint   描述
m   memory operand,表示要传入有效的地址,只要CPU能支持该地址,就可以传入
r   register operand,寄存器操作数,使用寄存器来保存这些操作数
i   immediate integer operand,表示可以传入一个立即数
constraint前还可以加上一些修饰字符,比如"=r"、"+r"、"=&r",含义如下:

constraint Modifier Characters   描述
=   表示内联汇编会修改这个操作数,即:写
+   这个操作数既被读,也被写
&   它是一个earlyclobber操作数
cvariablename C语言的变量名。

有了以上规则,请回看之前的示例代码,就会豁然开朗了:D。

goto

允许汇编指令跳转到C语言定义的GotoLabels任一个,多个GotoLabels用逗号分割。

goto跳转的汇编块不允许包含输出。这是由于编译器的内部限制:控制传递指令不能具有输出。

asm goto语句是隐式volatile的。

goto语句引用标签:

在汇编模板中引用标签,格式为 %l(n+i) (l是小写的L), n为输入操作数的个数(下标从0开始),i为第几个GotoLabels(下标从1开始)。

比如,asm块具有三个输入操作数并引用了两个Goto标签,则n=2(下标从0开始),因此将第一个Goto标签(下标从1开始,所以i = 1)在汇编模板中写为%l3,将第二个Goto标签(下标从1开始,所以i = 2)在汇编模板中写为%l4)。

如下代码示例显示了内联跳转。

2.2. 示例
程序代码 [选择]
#include <stdio.h>
#include <stdlib.h>

int test(int a, int b)
{
__asm__ goto (

"cmp %1, %0\n\t"
    /* BLE指令:b比a小则跳转 */
"blt %l2"
: /* No outputs. */
: "r"(a), "r"(b)
: "cc"
: carry);

return 0;

carry:
return 1;
}

int main(int argc, char **argv)
{
int a;
int b;

if (argc != 3)
{
printf("Usage: %s <val1> <val2>\n", argv[0]);
return -1;
}

a = (int)strtol(argv[1], NULL, 0);
b = (int)strtol(argv[2], NULL, 0);

printf("test return is %d\n",test(a, b));
return 0;
}
2.3. imx6ull测试结果
BLE指令实现功能:b比a小则跳转

./test  1 2
test return is 0

./test  3 2
test return is 1

./test  2 2
test return is 0