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

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

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

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

$ @( S% d& `# F! L2 L6 l$ S

两种实现方式

8 [* U& M5 S  Q

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

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

& J& u; D6 l6 c8 h: j  x

例子1:

% X4 s! @4 V6 L0 d: K

单句格式的:


  1. + ~7 I6 D' H6 n9 d( o, v
  2. main()1 \. V( x* Y+ x0 l- H$ `0 m8 W6 d
  3. {
    . D! e( y3 ?# n: m2 d4 {* I
  4. asm     mov ah,2;
    * ?  i7 K( |8 N, E' j( C- y
  5. asm     mov bh,0;
    * u/ A. Q7 q% I7 U
  6. asm     mov dl, 20;( F( _" \- x9 f1 K* `! y- |
  7. asm     mov dh,10;/ ^8 w% A2 S& @( A3 V( {) T4 Y1 G
  8. asm     int 10h; /*调用BIOS中断设置光标位置*/
    ! x7 ]" w8 k( @% t6 k2 ?
  9. }+ j$ g/ X/ d; v" j
  10. 模块格式的:/ U( Y1 q2 f. h2 R/ H; D3 I
  11. main()
    6 c4 `( C- _+ q6 x/ u
  12. {$ J8 Q+ O" W6 h; d- Q
  13. asm{0 J9 l! P: V( E3 u$ S) W
  14. mov ah,21 P/ E8 c$ O' F) y/ q. R3 L
  15. mov bh,0
    " t# D6 {; F2 ~& c$ J* K; C: m. N/ y1 A) W
  16. mov dl, 200 J4 D0 b& t" U* h& Q$ O3 T
  17. mov dh,102 \  |/ X! G' `) H: K4 p1 Z! f
  18. int 10h, ]; E& C, @( s! y/ l$ J3 a# }$ f$ l' {
  19.    }
    7 ~$ C# `1 W) u8 y
  20. }
复制代码

, W6 c% N! p. p; [) Y- x
- }% X; y% K/ z9 R7 n& D4 U

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

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


% A- X, V; w* K3 s3 R

例子2:

  1. 0 P5 T( N9 d9 G: r
  2. main()
    ' G; P! l0 P4 E' W1 R$ y  r
  3. {! V) V8 c  U  T
  4. char const *MESSAGE=”OutPut from asm..\n$”;5 D. k+ ^- L1 H  z
  5. asm{
    , q: J( j6 N- y/ ]5 t/ a- I
  6.   mov ah, 91 d& ]5 v* t! L5 ?, a; y
  7. mov dx, MESSAGE
    3 h1 P% G" r0 \8 s
  8. int 21h" u9 t* h  U- ^
  9. }! a, y- e3 A. [4 X' E
  10. }
复制代码


3 @9 O4 B' O  n3 j: |2 F


+ p1 I- _: h" X9 o

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

  1. 9 F2 b+ w8 V) z. {9 M) I
  2. #include <stdio.h>/ y: K' p! d" d( |) a
  3. main()
    3 }: t: Z2 N- [8 f
  4. {7 b. v8 @: ]0 D# C" C. A
  5.     printf(“OutPut from asm..\n$”);/ k+ f" C7 Y6 i" h
  6. }
复制代码

6 A/ H$ O; {% m) g. |& n- G

2 c( h, a% g  W

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


) o5 k( e  `* s9 \

C语言部分如下:
3 i* w' e5 K/ w: }

  1. 4 t8 i% P1 X: J) @4 q
  2. extern cursor (int,int),- L- h6 l2 t5 ?+ B
  3. main()
    5 K+ f0 U7 R4 i
  4. {1 b# O2 K) p, R
  5.         cursor(15,12);$ M) I) M' K- d
  6. }
复制代码

4 l, ~0 q3 R7 \3 U! v

3 w5 @$ I; E; P1 T, z

汇编语言部分如下:
: ]+ g. ]' X. h* y


  1. / P# b& d8 G# T; [+ T+ b& o
  2. .MODEL  SMALL* ^( d" j$ |& M  H6 `* M. Z
  3. .CODE
    ) C+ b1 G4 p* r; k
  4. PUBLIC
    + T3 p- v$ B9 U& ]( U0 v

  5. # H; x  m7 k9 {4 h  {- z+ ~
  6. _CURSOR PROC6 X1 n. ?. e, z) a+ A
  7. 2 O. \: h( i2 t9 F1 G1 p4 N
  8. PUSH  BP/ G0 c) g5 ]( J& X- |/ i
  9. MOV  BP,SP1 p/ {6 R# r. p% L5 e3 K
  10. MOV  DH,[BP+4]6 W$ @9 m, F- h
  11. MOV  DL,[BP+6]% h' T4 h+ f) j$ {
  12. MOV  AH,02
    7 d" p" T, f4 \' I# {5 E
  13. MOV  BH,00
    - i* O' f$ C0 M$ L
  14. INT  10H' ]1 L3 L* `( [0 \7 y1 Y5 Z
  15. POP  BP
    - l2 B2 @! S+ O$ P, j1 K+ w
  16. RET
    & h2 c9 Q, }& r: d+ ~* Y6 f) G
  17. " q6 g$ P) [$ P+ _. K* c
  18. _CURSOR ENDP
复制代码

) n5 s) k7 m) t% S% f


) p7 P; J3 `9 o' r

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


- I" U: k7 g+ ^% t% x

调用规则

9 i2 q' H- _+ a6 R; n2 b2 ?% {

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

) x% c' `- q- u: P' d: \

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


, R2 w3 A* i6 S2 S3 t8 x

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


0 e/ l( r* c9 ~

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]开始。


' |7 q! f( v4 U& p8 T( w! r

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

0 V" c' R+ g7 T

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


. a/ H, M; l7 N6 j" N

5、任何于C程序共享的名称都必须在前面加下划线,而且C语言只识别前8个字
' Q) j3 h9 L: M; p' l3 L4 J* [

符。


% n6 l9 M  j9 G4 k3 h& U

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

# b# T; J" B# h2 \- O

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


8 ]2 d4 c) g9 b' |/ V

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


; G; C& @  N& @: O( [3 j
把汇编语言程序与C语言程序链接到一起

& j. {4 f7 g9 D: X

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


) {( h; v" p8 P; m* Q

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


- T' t8 B/ m4 O2 \) P# d( ?

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


! u& c5 ]* X9 Z9 e4 n% P; n! }& l

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

$ c6 o- W4 z( V' G

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

link XXX.obj + YYY.obj

7 O% |$ t3 ?) a: F

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

+ ~! T# k& p/ v& i/ ~
GCC使用的内嵌汇编语法格式小教程

8 i* @; X! ?0 C2 t; L. v! i
& B. y' A7 g- m% x) `
1. 入门. Q0 s0 P" d( h0 h% m/ }- D

/ ]3 D) f( O% u& G+ E2 a( y6 O0 _; m
在C中嵌入汇编的最大问题是如何将C语言变量与指令操作数相关联。当然,gcc都帮我们想好了。下面是是一个简单例子。
5 p* n. o0 s' t9 R: M" x: A6 P

8 b9 b/ t6 N& d: q6 G+ [" A
asm(“fsinx %1, %0”:”=f”(result):”f”(angle));
, {1 Z& q7 N2 {3 I5 b! ?. M1 S
这里我们不需要关注fsinx指令是干啥的;只需要知道这条指令需要两个浮点寄存器作为操作数。作为专职处理C语言的gcc编译器,它是没办法知道fsinx这条汇编指令需要什么样的操作数的,这就要求程序猿告知gcc相关信息,方法就是指令后面的”=f”和”f”,表示这是两个浮点寄存器操作数。这被称为操作数规则(constraint)。规则前面加上”=”表示这是一个输出操作数,否则是输入操作数。constraint后面括号内的是与该寄存器关联的变量。这样gcc就知道如何将这条嵌入式汇编语句转成实际的汇编指令了:
2 ]. |: i! s) O4 O/ F) G, F
: g4 `- A# A, E' E1 A8 ?* T( T/ Y
  • fsinx:汇编指令名
  • %1, %0:汇编指令操作数
  • “=f”(result):操作数%0是一个浮点寄存器,与变量result关联(对输出操作数,“关联”的意思就是说gcc执行完这条汇编指令后会把寄存器%0的内容送到变量result中)
  • “f”(angle):操作数%1是一个浮点寄存器,与变量angle关联(对输入操作数,“关联”的意思是就是说gcc执行这条汇编指令前会先将变量angle的值读取到寄存器%1中)
    & i6 n  X) p  W  p
6 }/ s  x$ p5 [6 C7 V1 f
因此这条嵌入式汇编会转换为至少三条汇编指令(非优化):
  • 将angle变量的值加载到寄存器%1
  • fsinx汇编指令,源寄存器%1,目标寄存器%0
  • 将寄存器%0的值存储到变量result) B2 o. r# a8 W, [- O$ y
' B" n1 J" g% A8 c2 a% Z
当然,在高优化级别下上面的叙述可能不适用;比如源操作数可能本来就已经在某个浮点寄存器中了。
3 j) n! Z( G. p: h5 ~& R- h( z9 q
这里我们也看到constraint前加”=”符号的意义:gcc需要知道这个操作数是在执行嵌入汇编前从变量加载到寄存器,还是在执行后从寄存器存储到变量中。
常用的constraints有以下几个(更多细节参见gcc手册):
1 j, {0 S; {1 l0 F& g

0 }! N& j6 r' A, J& D0 m. u& j( \
  • m    内存操作数
  • r    寄存器操作数
  • i    立即数操作数(整数)
  • f    浮点寄存器操作数
  • F   立即数操作数(浮点)7 p. F% {, A& a6 e' W* ?
; i1 Q  m9 \5 Y: O, z
从这个栗子也可以看出嵌入式汇编的基本格式:

# D6 @4 `6 G8 V0 Kasm(“汇编指令”:”=输出操作数规则”(关联变量):”输入操作数规则”(关联变量));

/ p& h" z. ?( _' v" d7 A& @输出操作数必须为左值;这个显然。
% M# r2 s6 @) W

5 P  C# D; J6 Z, v6 ^# F( ]
5 n, D# h# ?6 ]: y( H
2. 多个操作数,或没有输出操作数
. `* T2 A7 |5 X  N

7 @/ ]8 M# V0 e# Q& ?. S% O如果某个指令有多个输入或输出操作数怎么办?例如arm有很多指令是三操作数指令。这个时候用逗号分隔多个规则:
. y2 C+ T0 r! a  s3 w9 J

, |3 @9 v' H/ z7 |( ]
asm(“add %0, %1, %2”:”=r”(sum):”r”(a), “r”(b));

' o9 W3 b' S% W% \; A每条操作数规则按顺序对应操作数%0, %1, %2。

/ a9 G* r* h4 E+ i1 X
对于没有输出操作数的情况,在汇编指令后就没有输出规则,于是就出现两个连续冒号,后跟输入规则。
) W  D- J( ?5 o! n1 U( F9 k! n
( ]1 a9 E2 h3 F$ Z7 C
# s; y( f5 @; J# ~
3. 输入-输出(或读-写)操作数+ h& \. S  A# H0 y% m! f% a

2 ?% V+ C/ F+ }! e, v有时候一个操作数既是输入又是输出,比如x86下的这条指令:
: C! U* T' G+ Q/ h
7 L: \  w' a8 E1 z
add %eax, %ebx

! g( E2 Y6 p! A2 `' ~注意指令使用AT&T格式而不是Intel格式。寄存器ebx同时作为输入操作数和输出操作数。对这样的操作数,在规则前使用”+”字符:
9 e/ s6 Y" X& Q! C, @

% f1 r- d" _+ I0 V
asm("add %1, %0" : "+r"(a) : "r"(b));
* l2 _) ^0 _0 G) w$ b+ W- S( v
对应C语言语句a=a+b。
0 g- \; S! A8 M' ^8 Q0 ^$ S- n
注意这样的操作数不能使用”=”符号,因为gcc看到”=”符号会认为这是一个单输出操作数,于是在将嵌入汇编转换为真正汇编的时候就不会预先将变量a的值加载到寄存器%0中。

- @5 r8 K& _9 l9 l  k- n  B另一个办法是将读-写操作数在逻辑上拆分为两个操作数:$ p6 q' f6 v1 u' q$ v

8 p, \1 Q5 }) @. e$ |2 Q
asm(“add %2, %0” : “=r”(a) : “0”(a), “r”(b));
5 L! q) Y$ W! o3 H
对“逻辑”输入操作数1指定数字规则”0”,表示这个逻辑操作数占用和操作数0一样的“位置”(占用同一个寄存器)。这种方法的特点是可以将两个“逻辑”操作数关联到两个不同的C语言变量上:
$ F9 _3 e3 M2 r& ]6 R; ]8 M
" o1 L2 O. y4 M0 |6 S/ }
asm("add %2, %0" : "=r"(c) : "0"(a), "r"(b));

- q0 z6 j$ _, U2 [( \对应于C程序语句c=a+b。
; Z* G. @2 m, v1 |
数字规则仅能用于输入操作数,且必须引用到输出操作数。拿上例来说,数字规则”0”位于输入规则段,且引用到输出操作数0,该数字规则自身占用操作数计数1。
! T# f7 K/ w0 E! k5 K8 ^

$ x6 r6 W( |. w4 k这里要注意,通过同名C语言变量是无法保证两个操作数占用同一“位置”的。比如下面这样的写法是不行的:/ ~$ X. R6 c9 u% ]

# @5 d' A2 ~/ w, P1 n+ H(错误写法)asm(“add %2, %0”:”=r”(a):”r”(a), “r”(b));
: g% X( D8 k; x& q: j$ f0 a6 x1 O# |2 h2 g

5 S2 `9 z) d8 ~. Z1 w# q5 e6 j( g4. 指定寄存器
1 r) @& i$ V0 A
7 [7 d  [% B! D% ^" w' }

/ t9 l9 t. x, l有时候我们需要在指令中使用指定的寄存器;典型的栗子是系统调用,必须将系统调用码和参数放在指定寄存器中。为了达到这个目的,我们要在声明变量时使用扩展语法:/ X1 }, r1 g4 S- e( F
9 A$ a! `' O1 H9 H9 C, Q  t
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

: I& ]! O4 r4 X注意只有在执行汇编指令时能确定a在eax中,b在ebx中,其他时候a和b的存放位置是不可知的。
- T, x0 i4 j3 v: k
另外,在这么用的时候要注意,防止statement 2在执行时覆盖了eax。例如statement 2改成下面这句:
2 v0 a4 ]0 F! u( }
. l2 o$ D, \% j, W
register int b asm(“%ebx”) = func();

; q' ]. ]) J/ Q函数调用约定会将func()的返回值放在eax里,于是破坏了statement 1对a的赋值。这个时候可以先用一条语句将func返回值放在临时变量里:
- G. `- D0 W" J
7 \+ `4 K# t, B* P8 R( H8 R' R
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

) g6 d1 _1 X) t* j, A
5. 隐式改变寄存器
) K4 V0 b4 y# p+ a& N% a

0 X' ^% M' M% _, ?7 f
有的汇编指令会隐含修改一些不在指令操作数中的寄存器,为了让gcc知道这个情况,将隐式改变寄存器规则列在输入规则之后。下面是VAX机上的栗子:5 `7 k0 S4 r+ b/ v

* B8 Q( x0 P- L: ]& u8 C. H' a) l
asm volatile(“movc3 %0,%1,%2”
                : /* no outputs */
                :”g”(from),”g”(to),”g”(count)
                :”r0”,”r1”,”r2”,”r3”,”r4”,”r5”);
# [; y" {5 x3 T8 L2 {7 M
* Y. k3 R) }2 D2 l
(movc3是一条字符块移动(Move characters)指令)
. |" G, K2 `& L/ @" q7 o9 s0 T
这里要注意的是输入/输出规则中列出的寄存器不能和隐含改变规则中的寄存器有交叉。比如在上面的栗子里,规则“g”中就不能包含r0-r5。以指定寄存器语法声明的变量,所占用的寄存器也不能和隐含改变规则有交叉。这个应该好理解:隐含改变规则是告诉gcc有额外的寄存器需要照顾,自然不能和输入/输出寄存器有交集。
7 X' F! {1 P9 p

9 R! K% I; e( ^# L6 c: P# S另外,如果你在指令里显式指定某个寄存器,那么这个寄存器也必须列在隐式改变规则之中(有点绕了哈)。上面我们说过gcc自身是不了解汇编指令的,所以你在指令中显式指定的寄存器,对gcc来说是隐式的,因此必须包含在隐式规则之中。另外,指令中的显式寄存器前需要一个额外的%,比如%%eax。
) c$ ~3 u$ \) x$ s0 X* V+ D, ]  B* V- O3 \: }. d4 P+ }

/ o# l/ p/ o5 T2 d$ s5 b6. volatile
; F3 W' M  K1 D9 d, w! K+ I6 U6 T
: H( s( E0 b+ K$ @& V' ?
asm volatile通知gcc你的汇编指令有side effect,千万不要给优化没了,比如上面的栗子。& `1 H8 C1 [7 a8 ?: s  L0 E
0 ^$ ]- g, ^0 X
如果你的指令只是做些计算,那么不需要volatile,让gcc可以优化它;除此以外,无脑给每个asm加上volatile或者是个好办法。
+ l, J, Y. x9 \! Z+ X, k  m
赞 收藏 1 评论0 发布时间:2020-5-11 18:58

举报

0个回答
关于
我们是谁
投资者关系
意法半导体可持续发展举措
创新与技术
意法半导体官网
联系我们
联系ST分支机构
寻找销售人员和分销渠道
社区
媒体中心
活动与培训
隐私策略
隐私策略
Cookies管理
行使您的权利
官方最新发布
人形机器人运动控制、感知与智能配电
半导体创新技术与应用方向
EE架构与软件定义汽车
12V/48V 汽车智能配电(SPD)
区域控制单元(ZCU)与分区架构
关注我们
st-img 微信公众号
st-img 手机版