更多内容可以加入Linux系统知识库套餐(教程+视频+答疑):文章目录
一、C语言实现加法
二、使用汇编函数实现加法
三、内联汇编语法
四、使用案例
沉淀、分享、成长,让自己和他人都能有所收获!😄
📢现代编译器已经足够优秀,大部分的C代码转成汇编码后,效率都很高。但是有些特殊的算法需要我们手工优化,这时就需要手写汇编代码;或是有时需要调用特殊的汇编指令(比如使用ldrex/strex实现互斥访问),这都涉及内联汇编。
一、C语言实现加法
01 #include <stdio.h>02 #include <stdlib.h>0304intadd(int a,int b)05{06return a+b;07}0809intmain(int argc,char**argv)10{11int a;12int b;1314if(argc !=3)15{16printf("Usage: %s <val1> <val2>\n", argv[0]);17return-1;18}1920 a =(int)strtol(argv[1],NULL,0);21 b =(int)strtol(argv[2],NULL,0);2223printf("%d + %d = %d\n", a, b,add(a, b));24return0;25}26
二、使用汇编函数实现加法
对应的汇编也挺复杂:需要入栈、出栈等操作,效率不算高。
26600010404<add>:26710404: b480 push {r7}26810406: b083 sub sp, #1226910408: af00 add r7, sp, #02701040a:6078 str r0,[r7, #4]2711040c:6039 str r1,[r7, #0]2721040e:687a ldr r2,[r7, #4]27310410:683b ldr r3,[r7, #0]27410412:4413 add r3, r2 // 真正实现加法的只有这条指令27510414:4618 mov r0, r327610416:370c adds r7, #1227710418:46bd mov sp, r72781041a: f85d 7b04 ldr.w r7,[sp], #42791041e:4770 bx lr280
使用汇编函数实现加法
01.text // 放在代码段02.global add // 实现全局函数add03.thumb // 使用thumb指令, main.c默认使用thumb指令, 所以这里也使用thumb指令0405 add:06 add r0, r0, r107 bx lr08
根据ATPCS规则,main函数调用add(a, b)时,会把第一个参数存入r0寄存器,把第二个参数存入r1寄存器。在上面第06行里,把r0、r1累加后,结果存入r0:根据ATPCS规则,r0用来保存返回值。可以看到,这个add函数连栈都没有使用,非常高效。这只是一个很简单的例子,我们工作中并不使用汇编来进行“加法优化”,在计算量非常大的地方可以考虑单独编写汇编函数实现优化。
三、内联汇编语法
从上面例子可以看到,我们完全可以新建一个汇编文件,在ATPCS规则之下编写代码,这样C函数就可以直接调用汇编函数。但是,需要新建汇编文件,有点麻烦。使用内联汇编,可以在C代码中内嵌汇编代码。先看看内联汇编的语法。
内联汇编语法:① asm也可以写作“asm”,表示这是一段内联汇编。② asm-qualifiers有3个取值:volatile、inline、goto。volatile的意思是易变的、不稳定的,用来告诉编译器不要随便优化这段代码,否则可能出问题。比如汇编指令“mov r0, r0”,它把r0的值复制到r0,并没有实际做什么事情,你的本意可能是用这条指令来延时。编译器看到这指令后,可能就把它去掉了。加上volatile的话,编译器就不会擅自优化。其他2个取值我们不关心,也比较难以理解,不讲。③ AssemblerTemplate汇编指令,用双引号包含起来,每条指令用“\n”分开,比如:
“mov %0,%1\n”“add %0,%1,%2\n”
④ OutputOperands输出操作数,内联汇编执行时,输出的结果保存在哪里。格式如下,当有多个变量时,用逗号隔开:
[[asmSymbolicName]]constraint(cvariablename)
smSymbolicName是符号名,随便取,也可以不写。constraint表示约束,有如下常用取值:
constraint前还可以加上一些修饰字符,比如“=r”、“+r”、“=&r”,含义如下:
cvariablename:C语言的变量名。示例1如下:
[result]"=r"(sum)
它的意思是汇编代码中会通过某个寄存器把结果写入sum变量。在汇编代码中可以使用“%[result]”来引用它。示例2如下:
"=r"(sum)
在汇编代码中可以使用“%0”、“%1”等来引用它,这些数值怎么确定后面再说。
⑤ InputOperands输入操作数,内联汇编执行前,输入的数据保存在哪里。格式如下,当有多个变量时,用逗号隔开:
[[asmSymbolicName]]constraint(cexpression)
- asmSymbolicName是符号名,随便取,也可以不写。
- constraint表示约束,参考上一小节,跟OutputOperands类似。
示例1如下:
[a_val]"r"(a),[b_val]"r"(b)
它的意思变量a、b的值会放入某些寄存器。在汇编代码中可以使用%[a_val]、%[b_val]使用它们。
示例2如下:
"r"(a),"r"(b)
它的意思变量a、b的值会放入某些寄存器。在汇编代码中可以使用%0、%1等使用它们,这些数值后面再说。⑥ Clobbers在汇编代码中,对于“OutputOperands”所涉及的寄存器、内存,肯定是做了修改。但是汇编代码中,也许要修改的寄存器、内存会更多。比如在计算过程中可能要用到r3保存临时结果,我们必须在“Clobbers”中声明r3会被修改。下面是一个例子:
:"r0","r1","r2","r3","r4","r5","memory"
我们常用的是有“cc”、“memory”,意义如下:
四、使用案例
staticinlinevoidarch_spin_lock(arch_spinlock_t*lock){unsignedlong tmp; u32 newval;arch_spinlock_t lockval;prefetchw(&lock->slock); __asm__ __volatile__("1: ldrex %0, [%3]\n"" add %1, %0, %4\n"" strex %2, %1, [%3]\n"" teq %2, #0\n"" bne 1b":"=&r"(lockval),"=&r"(newval),"=&r"(tmp):"r"(&lock->slock),"I"(1<< TICKET_SHIFT):"cc");while(lockval.tickets.next != lockval.tickets.owner){wfe(); lockval.tickets.owner =READ_ONCE(lock->tickets.owner);}smp_mb();}
这段代码是一个使用内联汇编嵌入在 C 代码中的片段,主要用于实现自旋锁的逻辑。下面逐行介绍:
- “=&r” (lockval), “=&r” (newval), “=&r” (tmp): 这部分是输出操作数的声明,指定了寄存器约束 (=&r) 和对应的 C 变量名。这些变量用于存储锁的当前值 lockval、新值 newval 和临时变量 tmp。
- “r” (&lock->slock), “I” (1 << TICKET_SHIFT): 这部分是输入操作数的声明,指定了寄存器约束 (“r”) 和对应的 C 变量名。&lock->slock 表示锁变量的地址,1 << TICKET_SHIFT 是一个常量,用于表示锁的步长。
- “cc”: 这是一个修饰符,表示代码可能会影响处理器的条件码寄存器,需要告诉编译器以便进行适当的处理。
- 1: ldrex %0, [%3]: 这是一个标签 1,标识一个循环的起始点。ldrex 是 ARM 处理器的原子加载指令,用于加载内存中的值到寄存器 %0,[%3] 表示从地址 %3(即锁变量的地址)处加载值。
- add %1, %0, %4: 将加载的值 %0 和常量 %4 相加,结果存储到 %1 中。
- strex %2, %1, [%3]: 使用 strex 指令将 %1 中的值存储回锁变量地址 %3 处,并将操作结果存储到 %2 中。
- teq %2, #0: 使用 teq 指令比较 %2 和常量 #0,检查 strex 操作是否成功,如果不成功则跳转到标签 1。
- bne 1b: 如果上一条指令结果不为零,则跳转到标签 1,即重新尝试获取锁的操作。这段内联汇编代码实现了自旋锁的关键逻辑,其中利用了 ARM 处理器的原子指令来实现对锁变量的操作,并通过循环进行自旋等待直到成功获取到锁。