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

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

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

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


  j7 n9 G; W* L* q& I8 M' L

两种实现方式


/ h! X' J/ F& k& D

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

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

6 U0 b' G: W! C

例子1:


1 z5 B8 r) n/ R0 A3 p) B

单句格式的:

  1. 9 J9 l; b% @' [* {
  2. main()
    5 _& @+ T( K5 L" C: ]8 z
  3. & G6 ]: c* T6 z$ ?3 T, C
  4. asm     mov ah,2;4 r; [( L% ~: A+ U$ V) Y
  5. asm     mov bh,0;
    ' ]: @9 q7 w7 n+ T$ m
  6. asm     mov dl, 20;5 O/ E/ K. b4 w4 S% J/ S  C
  7. asm     mov dh,10;
    + v& G& J% v" i
  8. asm     int 10h; /*调用BIOS中断设置光标位置*/. A- T% W- x/ A7 G
  9. 9 b! w8 M+ {( O9 b' n$ w, U
  10. 模块格式的:* `- W* R+ z0 D% f) s9 b: V
  11. main()
    6 B2 y9 G7 K" R- i

  12. ) [& J9 e3 Q  w$ y) _- s7 a
  13. asm{
    * d- ~+ @, X; H2 D/ m  H/ w
  14. mov ah,2+ p3 T. a& w, k( m( ~
  15. mov bh,08 k$ J# R% J+ p2 }1 @$ u
  16. mov dl, 20
    $ c" j" F  f7 L
  17. mov dh,10* g  A! a# p" D. t
  18. int 10h
    ! p8 h! L  P' X/ m  j. I
  19.    }9 s$ }9 \) |( K& g+ l! o
复制代码

1 @8 Z' ~; n) y% O* o
) ~" _0 P; b0 f: @: Y. }  ]" C4 E

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

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


% ^6 P# m. J; ^" b# H9 S0 v( H

例子2:

  1. ) ^* u% Y& t! j2 r
  2. main()0 R5 J1 `; s6 U. Y0 c
  3. {4 O0 [& u; Y- z( }% G! I. `
  4. char const *MESSAGE=”OutPut from asm..\n$”;
    " [' ?* U# M- B8 W, f7 A2 B# @2 P3 w  S3 R
  5. asm{
    " Y$ B7 k; j1 G1 H) x
  6.   mov ah, 9
    4 K' H7 D9 g* G3 o
  7. mov dx, MESSAGE
    ) g0 w/ H  Y) R$ e. H
  8. int 21h% R8 T' \5 Y5 H# g8 \
  9. }  ^* T& I( Q! U2 \" y8 J
  10. }
复制代码


% M* @7 J) S" M! G4 z% X3 j

+ |6 e, l' b: {2 b) \  H

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

  1. / P( Z3 Y, x% y
  2. #include <stdio.h>9 Y& U$ T9 b4 s# h- B/ {( m5 o
  3. main()
    ( P* p& o/ |% L; y3 h# e# U
  4. {5 @  w( S$ x# m3 v
  5.     printf(“OutPut from asm..\n$”);
    $ [8 t9 T) A  _! Q( \: B
  6. }
复制代码


; D. [0 S0 [' Z& r


; r# W, N5 U4 E; ]

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


/ e7 [+ `: S- U$ x

C语言部分如下:
" i) g& m0 Q0 H4 \6 l7 K/ }4 }


  1. : w! Q1 u$ ~& l. A
  2. extern cursor (int,int),
    - m* i$ W/ l' }
  3. main()! p$ b: p6 g3 D3 M2 j& G% j$ i. G% P
  4. {1 s1 {" M. n. [- _
  5.         cursor(15,12);
    3 Q, a; T8 O) T& a& ~+ o. X! g
  6. }
复制代码

9 Y/ _0 r) ~- v0 ]


+ h. {0 w1 H  {! r* f! X

汇编语言部分如下:
$ I/ p1 l3 P3 M( I& m


  1. " F8 y7 t3 @. Z
  2. .MODEL  SMALL4 l! a: _3 i- l# F2 Q
  3. .CODE! r4 O. \1 w4 u4 x( k: z/ M, P  P6 k
  4. PUBLIC* ?$ ^& p! v" b" E) E5 U  ?

  5. # G; Y3 K2 j8 P  S
  6. _CURSOR PROC
    + h( L' K2 l% R8 q

  7. / a! Q' C3 e0 Y! O% s7 O, ^
  8. PUSH  BP& I" M) ^5 Z2 z: _" L, o
  9. MOV  BP,SP
    3 H, C* f; `) V6 F# ?: U! @
  10. MOV  DH,[BP+4]2 r  u+ b2 y5 \) G+ _0 N
  11. MOV  DL,[BP+6]
    7 ^  `# S6 y# j% u7 C. O0 q
  12. MOV  AH,02
    , r2 D: I9 h( }4 y( S$ d/ S2 h
  13. MOV  BH,00% h7 E5 `) j: _' g! @# a+ Y
  14. INT  10H& Z' x% ?1 t0 u9 y5 p) A2 u
  15. POP  BP" t" s1 w* O  S; J, ?+ f$ S4 s
  16. RET. |  B' d' G; }) D0 n$ T

  17. / N) @% A. ?, Z1 \
  18. _CURSOR ENDP
复制代码


: |; r$ ^1 _; f: V/ O9 j, ~


4 ~( S2 D( _: Q- k6 c  w% R* P

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


' Z8 b/ ?% L9 y: r

调用规则

1 \! D3 E7 f' i+ [' d, a9 P4 I. M

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


. z) n- n/ j4 P* ]3 e

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

9 l0 E4 B, F6 ^

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


0 G+ T  O% m- @

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


  k7 E* X& F( X! M4 g' I4 `! l

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


4 E1 {2 @3 J, ]( p$ P+ E

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


1 T% t- N9 a! e1 _/ h9 H

5、任何于C程序共享的名称都必须在前面加下划线,而且C语言只识别前8个字
' |, b- I5 e" _: X

符。


8 O/ f6 G* {& z) p* A

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

  @% g- `# ?' ~2 K) @" L. l

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

) _- ?& N) j; Q

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

! L6 @9 D6 C4 h, H4 q$ Q5 O/ w
把汇编语言程序与C语言程序链接到一起


8 d2 g% M/ T6 X% \1 ^; O& b+ C

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

& W0 b: O! G  U  e; e1 d9 j

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


( L4 G9 u6 G" j' n

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

7 h2 L3 `% p' J- t3 G; s' o; e

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


5 c) b, E7 [% |3 {' ^+ Q0 c+ w+ o

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

link XXX.obj + YYY.obj


9 U5 Z, A* [' A* j* {; U1 Z, A: r

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

1 S, \- ^6 Z4 Y8 M' D; K- I* p
GCC使用的内嵌汇编语法格式小教程

. F# p) `  \8 z) h

" c6 K; {/ Q+ c8 j
1. 入门( m& R+ a/ J) l5 h  |8 I
0 E3 T# k$ B- X- `
在C中嵌入汇编的最大问题是如何将C语言变量与指令操作数相关联。当然,gcc都帮我们想好了。下面是是一个简单例子。+ k+ D4 e9 s5 \; y, R5 S

: Y+ ?$ v0 A7 n
asm(“fsinx %1, %0”:”=f”(result):”f”(angle));

' G7 Q' e% U! {2 M这里我们不需要关注fsinx指令是干啥的;只需要知道这条指令需要两个浮点寄存器作为操作数。作为专职处理C语言的gcc编译器,它是没办法知道fsinx这条汇编指令需要什么样的操作数的,这就要求程序猿告知gcc相关信息,方法就是指令后面的”=f”和”f”,表示这是两个浮点寄存器操作数。这被称为操作数规则(constraint)。规则前面加上”=”表示这是一个输出操作数,否则是输入操作数。constraint后面括号内的是与该寄存器关联的变量。这样gcc就知道如何将这条嵌入式汇编语句转成实际的汇编指令了:
2 R' Q- X/ t5 x
" l' A) p; A; g* X
  • fsinx:汇编指令名
  • %1, %0:汇编指令操作数
  • “=f”(result):操作数%0是一个浮点寄存器,与变量result关联(对输出操作数,“关联”的意思就是说gcc执行完这条汇编指令后会把寄存器%0的内容送到变量result中)
  • “f”(angle):操作数%1是一个浮点寄存器,与变量angle关联(对输入操作数,“关联”的意思是就是说gcc执行这条汇编指令前会先将变量angle的值读取到寄存器%1中)
      F1 I5 F! ?* j' E9 R

! Q7 A$ U4 x3 I1 p* j因此这条嵌入式汇编会转换为至少三条汇编指令(非优化):
  • 将angle变量的值加载到寄存器%1
  • fsinx汇编指令,源寄存器%1,目标寄存器%0
  • 将寄存器%0的值存储到变量result/ M% ]  J: p0 P6 w$ r& m
$ H! L1 w* U$ T2 r6 z0 }
当然,在高优化级别下上面的叙述可能不适用;比如源操作数可能本来就已经在某个浮点寄存器中了。
  J  h/ r& L& P4 t" n
这里我们也看到constraint前加”=”符号的意义:gcc需要知道这个操作数是在执行嵌入汇编前从变量加载到寄存器,还是在执行后从寄存器存储到变量中。
常用的constraints有以下几个(更多细节参见gcc手册):& t7 k( R3 K% t+ U
4 X1 h6 j; G) p  M6 D( E
  • m    内存操作数
  • r    寄存器操作数
  • i    立即数操作数(整数)
  • f    浮点寄存器操作数
  • F   立即数操作数(浮点)
    ! T( _$ v/ y$ ~, K) u+ T
6 b1 r& u/ Z4 p7 X( I; U
从这个栗子也可以看出嵌入式汇编的基本格式:
  }$ |7 \8 ]2 Z! u6 T$ a& |5 n
asm(“汇编指令”:”=输出操作数规则”(关联变量):”输入操作数规则”(关联变量));

% n$ @: l8 e! j; B$ L: }输出操作数必须为左值;这个显然。

( @( p1 d( \' j- R, L. b! ~8 O  B% b( z
; O8 r- ]+ n  z6 C( ?" _( x0 {! W
) [/ o; {) x, c# K
2. 多个操作数,或没有输出操作数% I8 c+ D4 i( ^7 v; ?- N
9 X8 f5 M$ B  T' H+ Z: [! }
如果某个指令有多个输入或输出操作数怎么办?例如arm有很多指令是三操作数指令。这个时候用逗号分隔多个规则:
  x* g7 f" b- @/ r
- y% o7 {( \4 F9 w
asm(“add %0, %1, %2”:”=r”(sum):”r”(a), “r”(b));

5 p- s* `$ k4 R  H1 u$ R每条操作数规则按顺序对应操作数%0, %1, %2。

/ K8 t4 q8 u  L, D% n2 X* ~
对于没有输出操作数的情况,在汇编指令后就没有输出规则,于是就出现两个连续冒号,后跟输入规则。
& q: t1 ~1 w; @3 |
1 Q+ E2 J! a7 k% l9 t+ c

( m* R# R7 P: `9 K6 u2 D3. 输入-输出(或读-写)操作数
/ h+ z* g5 A- R# n& c

8 G' C/ g! Y+ x0 U2 a! \/ }有时候一个操作数既是输入又是输出,比如x86下的这条指令:
" I' y4 z0 Q: q: u: _
: J1 `; _4 z3 O$ \! p
add %eax, %ebx
* k- u* l5 m! [/ \+ h/ I) n2 W$ [
注意指令使用AT&T格式而不是Intel格式。寄存器ebx同时作为输入操作数和输出操作数。对这样的操作数,在规则前使用”+”字符:
+ i$ B% M: ^% [# `1 V
0 l; F2 u% l1 D) [
asm("add %1, %0" : "+r"(a) : "r"(b));

/ r" `7 X. A( P9 {, ^& G' N对应C语言语句a=a+b。

' d( S! i/ P0 M- C) _) x2 |3 G注意这样的操作数不能使用”=”符号,因为gcc看到”=”符号会认为这是一个单输出操作数,于是在将嵌入汇编转换为真正汇编的时候就不会预先将变量a的值加载到寄存器%0中。

# P, l( s8 X1 g( M; i/ s另一个办法是将读-写操作数在逻辑上拆分为两个操作数:
* z0 _5 J1 H, v) F# H

) \  H, X! G5 _  |% i+ t0 k
asm(“add %2, %0” : “=r”(a) : “0”(a), “r”(b));

% v3 _5 Z0 `) C对“逻辑”输入操作数1指定数字规则”0”,表示这个逻辑操作数占用和操作数0一样的“位置”(占用同一个寄存器)。这种方法的特点是可以将两个“逻辑”操作数关联到两个不同的C语言变量上:
, c$ l  y/ M9 B' l* I3 C

7 F, Q8 Q, C% {! w8 r5 V; I) v
asm("add %2, %0" : "=r"(c) : "0"(a), "r"(b));

$ H! T+ p, A% G4 y; z8 `对应于C程序语句c=a+b。

% K# E- [8 t2 A1 `4 d' a9 z数字规则仅能用于输入操作数,且必须引用到输出操作数。拿上例来说,数字规则”0”位于输入规则段,且引用到输出操作数0,该数字规则自身占用操作数计数1。* w% ~* l1 _; V8 A! N
/ D  a. r2 ]6 G1 l& L: J, L
这里要注意,通过同名C语言变量是无法保证两个操作数占用同一“位置”的。比如下面这样的写法是不行的:5 N5 r; V0 F3 i: E! D5 S7 ?# c

: I$ n1 ~- ^! ]) z) |" J* D(错误写法)asm(“add %2, %0”:”=r”(a):”r”(a), “r”(b));
5 _4 X% v6 Q3 h$ A6 s  W) B6 v" \- t" q& t& p  S" n
. \9 N0 _' @% Z8 P* x
4. 指定寄存器
/ |% N5 L% \& H) }
- M% `# ^6 b4 s

; F# N9 z) y' t- R/ [5 m有时候我们需要在指令中使用指定的寄存器;典型的栗子是系统调用,必须将系统调用码和参数放在指定寄存器中。为了达到这个目的,我们要在声明变量时使用扩展语法:
4 d" ^! B: I( u$ W1 H6 z( `

6 B- x, y. f: u
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

1 v! p7 _9 H5 V注意只有在执行汇编指令时能确定a在eax中,b在ebx中,其他时候a和b的存放位置是不可知的。
* w7 v1 |0 ~8 |: M) f, z8 `
另外,在这么用的时候要注意,防止statement 2在执行时覆盖了eax。例如statement 2改成下面这句:
5 l" q; a; c( E" e* |$ D; p( [- T
( l# D/ I0 p1 ~: [9 u
register int b asm(“%ebx”) = func();

: x6 `$ R5 j0 x! G, b/ X( `函数调用约定会将func()的返回值放在eax里,于是破坏了statement 1对a的赋值。这个时候可以先用一条语句将func返回值放在临时变量里:
) t- G& v& \% Z

% F& P: @# z# z* f+ F& @2 U
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 G( a3 E% Q0 ], z% b
5. 隐式改变寄存器8 g$ a% g- O* o+ V! w0 m
9 z8 P2 X' N9 J
有的汇编指令会隐含修改一些不在指令操作数中的寄存器,为了让gcc知道这个情况,将隐式改变寄存器规则列在输入规则之后。下面是VAX机上的栗子:
9 z; ~/ ]1 c& p6 O

6 F: C; {" t9 B0 V
asm volatile(“movc3 %0,%1,%2”
                : /* no outputs */
                :”g”(from),”g”(to),”g”(count)
                :”r0”,”r1”,”r2”,”r3”,”r4”,”r5”);4 R7 V* w/ [. ~6 w

! G- n# L7 \; A2 M, W(movc3是一条字符块移动(Move characters)指令)

9 K9 Y# M% D+ h9 z* y& C这里要注意的是输入/输出规则中列出的寄存器不能和隐含改变规则中的寄存器有交叉。比如在上面的栗子里,规则“g”中就不能包含r0-r5。以指定寄存器语法声明的变量,所占用的寄存器也不能和隐含改变规则有交叉。这个应该好理解:隐含改变规则是告诉gcc有额外的寄存器需要照顾,自然不能和输入/输出寄存器有交集。
3 E/ I  D6 F( p! x3 ~' ]
# u0 y1 K1 M# ]! m4 Q* ]
另外,如果你在指令里显式指定某个寄存器,那么这个寄存器也必须列在隐式改变规则之中(有点绕了哈)。上面我们说过gcc自身是不了解汇编指令的,所以你在指令中显式指定的寄存器,对gcc来说是隐式的,因此必须包含在隐式规则之中。另外,指令中的显式寄存器前需要一个额外的%,比如%%eax。 # |, c8 w! k. o6 }2 P: `8 R% ]$ ]

& B+ Y" d$ \* z: @* u( h3 e; L

( [$ m. f7 e- j1 q! W* u6. volatile0 X. X: A7 r% X+ P, f( F7 e& y

' B. ~/ k9 f5 D& v3 Masm volatile通知gcc你的汇编指令有side effect,千万不要给优化没了,比如上面的栗子。
8 Z! l8 g. g+ ]  a, w
/ o1 v) X1 E2 T6 s/ X% v) e+ }3 m
如果你的指令只是做些计算,那么不需要volatile,让gcc可以优化它;除此以外,无脑给每个asm加上volatile或者是个好办法。8 I" M+ ~4 q0 _: T. @  g! l6 ^
收藏 1 评论0 发布时间:2020-5-11 18:58

举报

0个回答

所属标签

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