你的浏览器版本过低,可能导致网站不能正常访问!
为了你能正常使用网站功能,请使用这些浏览器。

C语言中如何嵌入汇编语言代码?

[复制链接]
gaosmile 发布时间:2020-5-11 18:58
有时候我们希望在C/C++代码中使用嵌入式汇编,因为C中没有对应的函数或语法可用。比如我最近在ARM上写FIR程序时,需要对最后的结果进行饱和处理,但gcc没有提供ssat这样的函数,于是不得不在C代码中嵌入汇编指令。

在C语言中如何使用汇编语言呢?这个问题在 不同的编译器中,具体实现方法是不同的。不过在实现大方上也不过就是有两种,而且各种编译器的实现方法也是大同小异。一种是在C语言中嵌入汇编语言代码, 另一种是让C语言从外部调用汇编。下面我们就以 Borland格式为例来说一说具体用法。但是,GCC与Microsoft的实现方法的与Borland只在格式上有点区别。当然,GCC的嵌入汇编是 AT&T格式的。还好,不管什么格式,只是表达形式的不同而已,其内在含义是一模一样的。还是那句话各种编译器的实现方法是大同小异的,并没有本质的区别。


两种实现方式


首先,我们看一看在C语言中如何嵌入汇编语言代码。在C语言中嵌入汇编语言代码,也有两种格式,一种是单句的,一种是模块的。

我们来看看一些简单的例子。


例子1:


单句格式的:


  1. main()

  2. asm     mov ah,2;
  3. asm     mov bh,0;
  4. asm     mov dl, 20;
  5. asm     mov dh,10;
  6. asm     int 10h; /*调用BIOS中断设置光标位置*/

  7. 模块格式的:
  8. main()

  9. asm{
  10. mov ah,2
  11. mov bh,0
  12. mov dl, 20
  13. mov dh,10
  14. int 10h
  15.    }
复制代码



在这个小程序里面并没有突出“嵌入”二字。不过从这个程序中可以看出其基本格式。嵌入的各行代码前面加上asm关键字或者把汇编语句放入asm代码块中,每行以分号或换行符结束,而注释必须是C语言格式的。

下面我们来看一个让C语言和汇编协作的例子:


例子2:


  1. main()
  2. {
  3. char const *MESSAGE=”OutPut from asm..\n$”;
  4. asm{
  5.   mov ah, 9
  6. mov dx, MESSAGE
  7. int 21h
  8. }
  9. }
复制代码



上面这个例子十分的简单,它的纯C语言版本是:


  1. #include <stdio.h>
  2. main()
  3. {
  4.     printf(“OutPut from asm..\n$”);
  5. }
复制代码



接下来我们看一看如何让C语言调用汇编例程。我们还是看一个简单的小程序:


C语言部分如下:


  1. extern cursor (int,int),
  2. main()
  3. {
  4.         cursor(15,12);
  5. }
复制代码



汇编语言部分如下:


  1. .MODEL  SMALL
  2. .CODE
  3. PUBLIC

  4. _CURSOR PROC

  5. PUSH  BP
  6. MOV  BP,SP
  7. MOV  DH,[BP+4]
  8. MOV  DL,[BP+6]
  9. MOV  AH,02
  10. MOV  BH,00
  11. INT  10H
  12. POP  BP
  13. RET

  14. _CURSOR ENDP
复制代码



通过上面这个程序,你会看到调用汇编语言的关键就是如何传递参数。事实上,是通过堆栈来传递的但是具体规则是什么呢?下面我就来看看。


调用规则


实际上,在C语言中使用汇编语言最困难的就是如何安全有效的传递参数。否则在调用汇编子程序时就会从堆栈中取出错误的参数。更可恶的是这种错误在编译的时候是不会发现错误提示的。


下面是C与MASM汇编语言混合是用的时候采用的规则:


1、参数传递的次序与它们出现的次序是相反的。例如上例中的cursor (x,y)中,首先传递的是y,然后才是x。这与我们的一般想法是不一样的,所以在这儿容易出现错误。


2、 传递完参数后,C程序还将保存(CS,IP)。如果C程序是SMALL或COMPACT存储模式下编译的(或者过程是NEAR型的),那么只保存IP,而 在MEDIUM、LARGE或HUGE模式下编译的(或者过程是FAR型的),那么CS和IP都会被压入堆栈,其顺序是CS在前,IP在后。不过这个过程 是C语言自动进行的而不需要我们干预。这也就是我们在例子2中为什么用MOV DH,[BP+4]而不是MOV  DH,[BP]。因为前面是CS和IP而不是参数,真正的参数从[BP+4]开始。


3、还有BP也必须保存在堆栈中,然后我们才可以通过BP和偏移地址来访问参数。


4、最后一条指令应当是后面不带数字的RET,因为把堆栈到原始位置的工作将由C程序重新获得控制权以后才会执行。


5、任何于C程序共享的名称都必须在前面加下划线,而且C语言只识别前8个字

符。


6、对于普通的参数C语言传递的是参数值,而对于数组,传递的是指针(也就是数据的地址)。


7、如果C程序是在MEDIUM、LARGE或HUGE模式下编译的,那么汇编语言过程应该设为FAR型,C程序是SMALL或COMPACT存储模式下编译的,那么汇编语言过程应该设为NEAR型。


不过在MASM5.1或TASM1.0以及更高的版本的时候就不必担心偏移地址、在共享名称前加下划线以及保存BP这些琐事了,因为它们可以由编译器自动完成了。很显然例子2是旧格式的。


把汇编语言程序与C语言程序链接到一起


1、确保汇编语言中的过程被定义为PUBLIC,过程名以下划线开始。例如,在C语言中叫做“sum”到汇编语言中就应该是“_sum”.


2、在C语言程序中过程定义为外部类型,例如在例子2中的extern cursor (int,int)。


3、用汇编器对汇编语言程序汇编,得到XXX.obj文件。


4、用C语言编译器编译C语言程序,得到YYY.obj文件。


5、用链接器将它们链接到一起生成可执行文件:

link XXX.obj + YYY.obj


以上就是混合使用C语言和汇编语言应该注意的几点问题。关于在GCC中使用汇编语言大体上是和上面一样的,只是实现细节上有一点区别而已。下面的这篇文章对于在GCC中使用内嵌汇编进行详细的解释。


GCC使用的内嵌汇编语法格式小教程


1. 入门

在C中嵌入汇编的最大问题是如何将C语言变量与指令操作数相关联。当然,gcc都帮我们想好了。下面是是一个简单例子。

asm(“fsinx %1, %0”:”=f”(result):”f”(angle));

这里我们不需要关注fsinx指令是干啥的;只需要知道这条指令需要两个浮点寄存器作为操作数。作为专职处理C语言的gcc编译器,它是没办法知道fsinx这条汇编指令需要什么样的操作数的,这就要求程序猿告知gcc相关信息,方法就是指令后面的”=f”和”f”,表示这是两个浮点寄存器操作数。这被称为操作数规则(constraint)。规则前面加上”=”表示这是一个输出操作数,否则是输入操作数。constraint后面括号内的是与该寄存器关联的变量。这样gcc就知道如何将这条嵌入式汇编语句转成实际的汇编指令了:

  • fsinx:汇编指令名
  • %1, %0:汇编指令操作数
  • “=f”(result):操作数%0是一个浮点寄存器,与变量result关联(对输出操作数,“关联”的意思就是说gcc执行完这条汇编指令后会把寄存器%0的内容送到变量result中)
  • “f”(angle):操作数%1是一个浮点寄存器,与变量angle关联(对输入操作数,“关联”的意思是就是说gcc执行这条汇编指令前会先将变量angle的值读取到寄存器%1中)

因此这条嵌入式汇编会转换为至少三条汇编指令(非优化):
  • 将angle变量的值加载到寄存器%1
  • fsinx汇编指令,源寄存器%1,目标寄存器%0
  • 将寄存器%0的值存储到变量result

当然,在高优化级别下上面的叙述可能不适用;比如源操作数可能本来就已经在某个浮点寄存器中了。

这里我们也看到constraint前加”=”符号的意义:gcc需要知道这个操作数是在执行嵌入汇编前从变量加载到寄存器,还是在执行后从寄存器存储到变量中。
常用的constraints有以下几个(更多细节参见gcc手册):

  • m    内存操作数
  • r    寄存器操作数
  • i    立即数操作数(整数)
  • f    浮点寄存器操作数
  • F   立即数操作数(浮点)

从这个栗子也可以看出嵌入式汇编的基本格式:

asm(“汇编指令”:”=输出操作数规则”(关联变量):”输入操作数规则”(关联变量));

输出操作数必须为左值;这个显然。



2. 多个操作数,或没有输出操作数

如果某个指令有多个输入或输出操作数怎么办?例如arm有很多指令是三操作数指令。这个时候用逗号分隔多个规则:

asm(“add %0, %1, %2”:”=r”(sum):”r”(a), “r”(b));

每条操作数规则按顺序对应操作数%0, %1, %2。

对于没有输出操作数的情况,在汇编指令后就没有输出规则,于是就出现两个连续冒号,后跟输入规则。


3. 输入-输出(或读-写)操作数

有时候一个操作数既是输入又是输出,比如x86下的这条指令:

add %eax, %ebx

注意指令使用AT&T格式而不是Intel格式。寄存器ebx同时作为输入操作数和输出操作数。对这样的操作数,在规则前使用”+”字符:

asm("add %1, %0" : "+r"(a) : "r"(b));

对应C语言语句a=a+b。

注意这样的操作数不能使用”=”符号,因为gcc看到”=”符号会认为这是一个单输出操作数,于是在将嵌入汇编转换为真正汇编的时候就不会预先将变量a的值加载到寄存器%0中。

另一个办法是将读-写操作数在逻辑上拆分为两个操作数:

asm(“add %2, %0” : “=r”(a) : “0”(a), “r”(b));

对“逻辑”输入操作数1指定数字规则”0”,表示这个逻辑操作数占用和操作数0一样的“位置”(占用同一个寄存器)。这种方法的特点是可以将两个“逻辑”操作数关联到两个不同的C语言变量上:

asm("add %2, %0" : "=r"(c) : "0"(a), "r"(b));

对应于C程序语句c=a+b。

数字规则仅能用于输入操作数,且必须引用到输出操作数。拿上例来说,数字规则”0”位于输入规则段,且引用到输出操作数0,该数字规则自身占用操作数计数1。

这里要注意,通过同名C语言变量是无法保证两个操作数占用同一“位置”的。比如下面这样的写法是不行的:

(错误写法)asm(“add %2, %0”:”=r”(a):”r”(a), “r”(b));


4. 指定寄存器


有时候我们需要在指令中使用指定的寄存器;典型的栗子是系统调用,必须将系统调用码和参数放在指定寄存器中。为了达到这个目的,我们要在声明变量时使用扩展语法:

register int a asm(“%eax”) = 1;              // statement 1
register int b asm(“%ebx”) = 2;              // statement 2
asm("add %1, %0" : "+r"(a) : "r"(b));         // statement 3

注意只有在执行汇编指令时能确定a在eax中,b在ebx中,其他时候a和b的存放位置是不可知的。

另外,在这么用的时候要注意,防止statement 2在执行时覆盖了eax。例如statement 2改成下面这句:

register int b asm(“%ebx”) = func();

函数调用约定会将func()的返回值放在eax里,于是破坏了statement 1对a的赋值。这个时候可以先用一条语句将func返回值放在临时变量里:

int t = func();
register int a asm(“%eax”) = 1;              // statement 1
register int b asm(“%ebx”) = t;              // statement 2
asm("add %1, %0" : "+r"(a) : "r"(b));         // statement 3

5. 隐式改变寄存器

有的汇编指令会隐含修改一些不在指令操作数中的寄存器,为了让gcc知道这个情况,将隐式改变寄存器规则列在输入规则之后。下面是VAX机上的栗子:

asm volatile(“movc3 %0,%1,%2”
                : /* no outputs */
                :”g”(from),”g”(to),”g”(count)
                :”r0”,”r1”,”r2”,”r3”,”r4”,”r5”);

(movc3是一条字符块移动(Move characters)指令)

这里要注意的是输入/输出规则中列出的寄存器不能和隐含改变规则中的寄存器有交叉。比如在上面的栗子里,规则“g”中就不能包含r0-r5。以指定寄存器语法声明的变量,所占用的寄存器也不能和隐含改变规则有交叉。这个应该好理解:隐含改变规则是告诉gcc有额外的寄存器需要照顾,自然不能和输入/输出寄存器有交集。

另外,如果你在指令里显式指定某个寄存器,那么这个寄存器也必须列在隐式改变规则之中(有点绕了哈)。上面我们说过gcc自身是不了解汇编指令的,所以你在指令中显式指定的寄存器,对gcc来说是隐式的,因此必须包含在隐式规则之中。另外,指令中的显式寄存器前需要一个额外的%,比如%%eax。


6. volatile

asm volatile通知gcc你的汇编指令有side effect,千万不要给优化没了,比如上面的栗子。

如果你的指令只是做些计算,那么不需要volatile,让gcc可以优化它;除此以外,无脑给每个asm加上volatile或者是个好办法。
收藏 1 评论0 发布时间:2020-5-11 18:58

举报

0个回答

所属标签

STM32团队

意法半导体微控制器和微处理器拥有广泛的产品线,包含低成本的8位单片机和基于ARM® Cortex®-M0、M0+、M3、M4、M33、M7及A7内核并具备丰富外设选择的32位微控制器及微处理器


最新内容

关于
我们是谁
投资者关系
意法半导体可持续发展举措
创新与技术
意法半导体官网
联系我们
联系ST分支机构
寻找销售人员和分销渠道
社区
媒体中心
活动与培训
隐私策略
隐私策略
Cookies管理
行使您的权利
官方最新发布
STM32N6 AI生态系统
STM32MCU,MPU高性能GUI
ST ACEPACK电源模块
意法半导体生物传感器
STM32Cube扩展软件包
关注我们
微信公众号二维码 微信公众号
手机版二维码 手机版