1 / 6
文档名称:

嵌入C代码中的386汇编语言程序段.pdf

格式:pdf   大小:358KB   页数:6页
下载后只包含 1 个 PDF 格式的文档,没有任何的图纸或源代码,查看文件列表

如果您已付费下载过本站文档,您可以点这里二次下载

分享

预览

嵌入C代码中的386汇编语言程序段.pdf

上传人:小屁孩 2022/12/2 文件大小:358 KB

下载得到文件列表

嵌入C代码中的386汇编语言程序段.pdf

相关文档

文档介绍

文档介绍:该【嵌入C代码中的386汇编语言程序段 】是由【小屁孩】上传分享,文档一共【6】页,该文档可以免费在线阅读,需要了解更多关于【嵌入C代码中的386汇编语言程序段 】的内容,可以使用淘豆网的站内搜索功能,选择自己适合的文档,以下文字是截取该文章内的部分文字,如需要获得完整电子版,请下载此文档到您的设备,方便您编辑和打印。嵌入C代码中的386汇编语言程序段
当需要在C语言的程序中嵌入一段汇编语言程序段时,可以使用gcc提供的“asm”语
句功能。例如,下面这么一行代码:
#define__SLOW_DOWN_IO__asm____volatile__(“outb%al,$0x80”)
这里,暂时忽略在asm和volatile前后的两个“__”字符,这也是gcc对C语言的一种
扩充,后面我们还要讲到。先来看括号里面加上了引号的汇编指令。这是一条8位输出指
令,如前所述在操作符上加了后缀“b”以表示这是8位的,而0x80因为是常数,即所谓
“直接操作数”,所以要加上前缀“$”,而寄存器名al也加了前缀“%”。知道了前面所讲
AT&T格式与Intel格式的不同,这就是一条很普通的汇编指令,很容易理解。
在同一个asm语句中也可以插入多行汇编程序。就在同一个文件中,在不同的条件下,
__SLOW_DOWN_IO又有不同的定义:
#define__SLOW_DOWN_IO__asm____volatile__(“jmp\nl:\tjmp1f1f\n1:”)
这就不怎么直观了。这里,一共插入了三行汇编语句,“\n”就是换行符,而“\t”则
表示TAB符。所以gcc翻译成下面的格式而交给gas去汇编:
jmp1f
1:jmp1f
1:
这里转移指令的目标1f表示往前(f表示forward)找到第一个标号为1的那一行。相
应地,如果是1b就表示往后找。这也是从早期Unix汇编代码中继承下来的,读过Unix第
6版的读者大概都还能记得。所以,这一小段汇编代码的用意就在于使CPU空做两条跳转
指令和消耗掉一些时间。既然是要消耗掉一些时间,而不是要节省一些时间,那么为什么
要用汇编语句来实现,而不是在C里面来实现呢?原因在于想要对此有比较确切的控制。
如果用C语言来消耗一些时间的话,你常常不能确切地知道经过编译以后,特别是经过优
化的话,最后产生的汇编代码究竟怎样。
如果读者觉得这毕竟还是容易理解的话,那么下面这一段(取自include/asm/)
就困难多了:
static__inline__voidatomic_add(inti,atomic_t*v)
{
__asm____volatile__(
LOCK"addl%1,%0"
:"=m"(v->counter)
:"ir"(i),"m"(v->counter));
}
一般而言,往C代码中插入汇编语言的代码片段要比“纯粹”的汇编语言代码复杂得
多,因为这里有个怎样分配使用寄存器,怎样与C语言代码中的变量结合的问题。为了这
个目的,必须对所用的汇编语言作更多的扩充,增加对汇编工具的指导作用。其结果是其
语法实际上变成了既不同于汇编语言,也不同于C语言的某种中间语言。
欢迎您阅读并下载本文档,本文档来源于互联网,如有侵权请联系删除!我们将竭诚为您提供优质的文档!
下面,先介绍一下插入C代码中的汇编成分的一般格式,并加以解释。以后,在我们
走过各种情景时碰到具体的代码时还会加以提示。
插入C代码中的一个汇编语言代码片段可以分成四部分,以“:”号加以分隔,其一般
形式为:
指令部:输出部:输入部:损坏部
注意不要把这些“:”号跟程序标号中所用的(如前面的1:)混淆。
第一部分就是汇编语句本身,其格式与在汇编语言程序中使用的基本相同,但也有区
别,不同之处下面会讲到。这一部分可以称为“指令部”,是必须有的,而其他各部分则可
视具体情况而省略,所以在最简单的情况下就与常规的汇编语句基本相同,如前面的两个
例子那样。
当将汇编语言的代码片段嵌入到C代码中时,操作数与C代码中的变量如何结合显然
是个问题。在本节开头的两个例子中,汇编指令都没有产生与C程序中的变量结合的问题,
所以比较简单。当汇编指令中的操作数需要与C程序中的某些变量结合时,情况就复杂多
了。这是因为:程序员在编写嵌入的汇编代码时,按照程序逻辑的要求很清楚应该选用什
么指令,但是却无法确切地知道gcc在嵌入点的前后会把哪一个寄存器分配用于哪一个变
量,以及哪一个或哪几个寄存器是空闲着的。而且,光是被动地知道gcc对寄存器的分配
情况也还是不够,还得有个手段把使用寄存器的要求告知gcc,反过来影响它对寄存器的分
配。当然,如果gcc的功能非常强,那么通过分析嵌入的汇编代码也应该能够归纳出这些
要求,再通过优化,最后也能达到目的。但是,即使那样,所引入的不确定性也还是个问
题,更何况要做到这样还不容易。针对这个问题,gcc采取了一种折中的办法:程序员只提
供具体的指令而对寄存器的使用则一般只提供一个“样板”和一些约束条件,而把到底如
何与变量结合的问题留给gcc和gas去处理。
在指令部中,数字加上前缀%,如%0、%1等等,表示需要使用寄存器的样板操作数。
可以使用的此类操作数的总数取决于具体CPU中通用寄存器的数量。这样,指令部中用到
了几个不同的这种操作数,就说明有几个变量需要与寄存器结合,由gcc和gas在编译和汇
编时根据后面的约束条件自行变通处理。由于这些样板操作数也使用“%”前缀,在涉及
到具体的寄存器时就要在寄存器名前面加上两个“%”符,以免混淆。
那么,怎样表达对变量结合的约束条件呢?这就是其余几个部分的作用。紧接在指令
部后面的是“输出部”,用以规定对输出变量,即目标操作数如何结合的约束条件。每个这
样的条件称为一个“约束”(constraint)。必要时输出部中可以有多个约束,互相以逗号分
隔。每个输出约束以“=”号开头,然后是一个字母表示对操作数类型的说明,然后是关于
变量结合的约束。例如,在上面的例子中,输出部为
:"=m"(v->counter)
这里只有一个约束条件,“=m”表示相应的目标操作数(指令部中的%0)是一个内存
单元v->counter。凡是与输出部中说明的操作数相结合的寄存器或操作数本身,在执行嵌入
的汇编代码以后均不保留执行之前的内容,这就给gcc提供了调度使用这些寄存器的依据。
输出部后面是“输入部”。输入约束的格式与输出约束条件相似,但不带“=”号。在
前面的例子中的输入部有两个约束。第一个为”ir”(i),表示指令中的%1可以是一个寄存
欢迎您阅读并下载本文档,本文档来源于互联网,如有侵权请联系删除!我们将竭诚为您提供优质的文档!
器中的“直接操作数”(i表示immediate),并且该操作数来自于C代码中的变量名(这里
是调用参数)i。第二个约束为"m"(v->counter),意义与输出约束中相同。如果一个输入约
束要求使用寄存器,则在预处理时gcc会为之分配一个寄存器,并自动插入必要的指令将
操作数即变量的值装入该寄存器。在输入部中说明的操作数结合的寄存器或操作数本身,
在执行嵌入的汇编代码后也不保留执行之前的内容。例如,这里的%1要求使用寄存器,所
以gcc会为其分配一个寄存器,并自动插入一条movl指令把参数i的数值装入该寄存器,
可是这个寄存器原来的内容就不复存在了。如果这个寄存器本来就是空闲的,那倒无所谓,
可是如果所有的寄存器都在使用,而只好暂时借用一个,那就得保证在使用以后恢复其原
有的内容。此时,gcc会自动在开头处插入一条pushl指令,将该寄存器原来的内容保存在
堆栈中,而在结束以后插入一条popl指令,恢复寄存器的内容。
在有些操作中,除用于输入操作数和输出操作数的寄存器以外,还要将若干个寄存器
用于计算或操作的中间结果。这样,这些寄存器原有的内容就损坏了,所以要在损坏部对
操作的副作用加以说明,让gcc采取相应的措施。不过,有时候就直接把这些说明放在输
出部了,那也并无不可。
操作数的编号从输出部的第一个约束(序号为0)开始,顺序数下来,每个约束计数
一次,在指令部中引用这些操作数或分配用于这些操作数的寄存器时,就在序号前面加上
一个“%”号。在指令部中引用一个操作数时总是把它当成一个32位的“长字”,但是对
其实施的操作,则根据需要也可以是字节操作或字(16位)操作。对操作数进行的字节操
作默认为对其最低字节的操作,字操作也是一样。不过,在一些特殊的操作中,对操作数
进行字节操作时也允许明确指出是对哪一个字节操作,此时在%与序号之间插入一个“b”
表示最低字节,插入一个“h”表示次低字节。
表示约束条件的字母有很多。主要有:
“m”,“v”和“o”——表示内存单元;
“r”——表示任何寄存器;
“q”——表示寄存器eax,ebx,ecx,edx之一;
“i”和“h”——表示直接操作数;
“E”和“F”——表示浮点数;
“g”——表示“任意”;
“a”,“b”,“c”,“d”——分别表示要求使用寄存器eax,ebx,ecx或edx;
“S”,“D”——分别表示要求使用寄存器esi或edi;
“I”——表示常数(0至31)。
此外,如果一个操作数要求与前面某个约束中所要求的是同一个寄存器,那就把与那
个约束对应的操作数编号放在约束条件中。在损坏部常常会以“memory”为约束条件,表
示操作完成后内存中的内容已有改变,如果原来某个寄存器(也许在本次操作中并未用到)
的内容来自内存,则现在可能已经不一致。
还要注意,当输出部为空,即没有输出约束时,如果有输入约束存在,则须保留分隔
标记“:”号。
欢迎您阅读并下载本文档,本文档来源于互联网,如有侵权请联系删除!我们将竭诚为您提供优质的文档!
回到上面的例子,读者现在应该可以理解这段代码的作用是将参数i的值加到
v->counter上。代码中的关键字LOCK表示在执行addl指令时要把系统的总线锁住,不让别
的CPU(如果系统中有不只一个CPU)打扰。读者也许要问,将两个数相加是很简单的操
作,C语言中明明有相应的语言成分,例如“v->counter+=i;”,为什么要用汇编呢?原因就
在于,这里要求整个操作只由一条指令完成,并且要将总线锁住,以保证操作的“原子性
(atomic)”。相比之下,上述的C语句在编译之后到底有几条指令是没有保证的,也无法
要求在计算过程中对总线加锁。
再看一段嵌入汇编代码:
#ifdefCONFIG_SMP
#defineLOCK_PREFIX"lock;"
#else
#defineLOCK_PREFIX""
#endif
#defineADDR(*(volatilelong*)addr)
staticinlinevoidset_bit(intnr,volatileunsignedlong*addr)
{
__asm____volatile__(LOCK_PREFIX
"btsl%1,%0"
:"=m"(ADDR)
:"Ir"(nr));
}
这里的指令btsl将一个32位操作数中的某一位设置成1。参数nr表示该位的位置。现
在读者应该不感到困难,也明白为什么要用汇编语言的原因了。
再来看一个复杂一点的例子:
staticinlinevoid*__memcpy(void*to,constvoid*from,size_tn)
{
intd0,d1,d2;
__asm____volatile__(
203:"rep;movsl"
"testb$2,%b4"
"je1f"
"movsw"
"1:testb$1,%b4"
"je2f"
"movsb"
"2:"
:"=&c"(d0),"=&D"(d1),"=&S"(d2)
:"0"(n/4),"q"(n),"1"((long)to),"2"((long)from)
欢迎您阅读并下载本文档,本文档来源于互联网,如有侵权请联系删除!我们将竭诚为您提供优质的文档!
:"memory");
return(to);
}
读者也许知道memcpy()。这里的__memcpy就是内核中对memcpy()的底层实现,用来
复制一块内存空间的内容,而忽略其数据结构。这是使用非常频繁的一个函数,所以其运
行效率十分重要。
先看约束条件和变量与寄存器的结合。输出部有三个约束,对应操作数%0至%2。其
中变量d0为操作数%0,必须放在寄存器ecx中,原因等一下就会明白。同样,d1即%1必
须放在寄存器edi中;d2即%2必须放在寄存器esi中。再看输入部,这里有四个约束,对
应于操作数%3至%6。其中操作数%3与操作数%0使用同一个寄存器,所以也必须是寄存器
ecx;并且要求由gcc自动插入必要的指令,事先将其设置成n/4,实际上是将复制长度从
字节个数n换算成长字个数n/4。至于n本身,则要求gcc任意分配一个寄存器存放。操作
数%5与%6,即参数to与from,分与别%1和%2使用相同的寄存器,所以也必须是寄存器
edi和esi。
再看指令部,读者马上就能看到这里似乎只用了%4。为什么那么多的操作数似乎都没
有用到呢?读完这些指令就明白了。
第一条指令是“rep”,表示下一条指令movl要重复执行,每重复一遍就把寄存器ecx
中的内容减1,直到变成0为止。所以,在这段代码中一共执行n/4次。那么,movl又干
些什么呢?它从esi所指向的地方复制一个长字到edi所指的地方,并使esi和edi分别加4。
这样,当代码中的203行执行完毕,到达204行时,所有的长字都已复制好,最多只剩下
了三个字节了。在这个过程中,实际上使用了ecx、esi以及edi三个寄存器,即%0(同时
也是%3)、%2(同时也是%6)以及%1(同时也是%5)三个操作数,这些都隐含在指令中,
从字面上看不出来。同时,这也说明了为什么这些操作数必须存放在指定的寄存器中。
接着就是处理剩下的三个字节了,先通过testb测试操作数%4,即复制长度n的最低
字节中的bit2,如果这一位为1就说明还有至少两个字节,所以通过指令movsw复制一个
短字(esi和edi则分别加2),否则就把它跳过。再通过testb(注意它前面时\t,表示在预
处理后的汇编代码中插入一个TAB字符)测试操作数%4的bit1,如果这一位为1就说明还
剩下一个字节,所以通过movsb再复制一个字节,否则就把它跳过。到达标号2的时候,
执行就结束了。读者不妨自己写一段C代码来实现这个函数,编译以后用objdump看它的
实现,并与此作一比较,相信就能体会到为什么这里要采用汇编语言。
作为读者的复****材料,下面时strcmp()的代码,不熟悉i386指令的读者可以找一本Intel
的指令手册对照着阅读。
staticinlineintstrncmp(constchar*cs,constchar*ct,size_tcount)
{
registerint__res;
intd0,d1,d2;
__asm____volatile__(
"1:decl%3"//
欢迎您阅读并下载本文档,本文档来源于互联网,如有侵权请联系删除!我们将竭诚为您提供优质的文档!
"js2f"
"lodsb"
"scasb"
"jne3f"
"testb%%al,%%al"
"jne1b"
"2:xorl%%eax,%%eax"
"jmp4f"
"3:sbbl%%eax,%%eax"
"orb$1,%%al"
"4:"
:"=a"(__res),"=&S"(d0),"=&D"(d1),"=&c"(d2)
:"1"(cs),"2"(ct),"3"(count));
return__res;
}
,为从%0到%3,%0存放__res,必须
放在寄存器eax里面。%1存放d0,必须放在edi寄存器里面。%2存放d1,必须放在edi。%3
存放d2,必须放在ecx里面。输入约束条件有三个,分别对应上面的%1到%
入参数cx,ct,count。
下面来看指令部,“decl%3”对count减1,“js2f”,为负数就向前跳转到2处,“lodsb”
将地址DS:(E)SI的值装入AL,并且esi++。scasb,用AL中的字节值与ES:(E)DI处的字符相比
较,然后设置标志位,并且edi++。“jne3f”,不相等就向前跳转到3处。“testb%%al,%%al”
测试al中的值是不是NULL.“jne1b”不是NULL就向后跳转到1处。“xorl%%eax,%%eax”,
是NULL,则将eax寄存器清0.“jmp4f”直接跳转到4处结束。"3:sbbl%%eax,%%eax"。
将eax清0,"orb$1,%%al"al与1异或。
上面函数的总体功能就是将字符串cs与字符串ct的前count个字符进行比较。
参数:cs-字符串,ct-字符串,count-比较的字符数。
%0-eax(__res)返回值,%1-edi(cs)串1指针,%2-esi(ct)串2指针,%3-ecx(count)。
返回:如果串1!=串2,则返回1;串1=串2,则返回0;。
欢迎您阅读并下载本文档,本文档来源于互联网,如有侵权请联系删除!我们将竭诚为您提供优质的文档!