在嵌入式汇编中,您可以将C语言表达式指定为汇编指令的操作数,而不必担心如何将C语言表达式的值读取到寄存器中以及如何将C语言表达式写入寄存器中. 计算结果返回到C变量. 您只需要告诉C语言表达式和程序中汇编指令操作数之间的对应关系,GCC就会自动插入代码以完成必要的操作.
1. 简单的内联汇编
示例:
__ asm__ __volatile __(“ hlt”); “ __asm__”表示以下代码是内联汇编,“ asm”是“ __asm__”的别名. “ __volatile__”表示编译器不会优化代码,以下说明保持不变,并且“ volatile”为其别名. 组装说明在括号中.
2. 内联汇编的例子
要使用内联汇编,必须首先编写汇编指令模板,然后将C语言表达式与指令的操作数相关联,并告诉GCC这些操作受到哪些限制. 例如,在以下汇编语句中:
__ asm__ __violate __(“ movl%1,%0”: “ = r”(结果): “ m”(输入));
“电影%1,%0”是指令模板; “%0”和“%1”表示指令的操作数,称为占位符,并且内联汇编程序依靠它们来转换C语言表达式和相应的指令操作数. 指令模板在C语言表达式后的括号内. 在此示例中,只有两个: “结果”和“输入”,它们按照出现顺序对应于指令操作数“%0”和“%1”. 注意相应的顺序: 第一个C表达式对应于“%0”;第二个表达式对应于“%1”,依此类推,最多10个操作数,分别使用“%0”,“%1”. ...“%9”表示. 每个操作数前面都有一个用引号引起来的字符串,字符串的内容是操作数的限制或要求. “结果”前面的受限字符串是“ = r”,其中“ =”表示“结果”是输出操作数,“ r”表示“结果”需要与通用寄存器关联,首先是值读取寄存器中的操作数,然后在指令中使用相应的寄存器,而不是“结果”本身. 当然,在执行指令之后,寄存器中的值需要存储在变量“结果”中. 从表面上看,该指令似乎直接执行“结果”运算,实际上GCC已经执行了隐式处理,因此我们可以编写更少的指令. “输入”前面的“ r”表示需要先将表达式放入寄存器中,然后在指令中使用该寄存器参与操作.
C表达式或变量与寄存器之间的关系由GCC自动处理. 我们只需要使用限制字符串来指示GCC如何处理它. 限制字符必须符合指令的操作数要求,否则生成的汇编代码将是错误的,读者可以将上面示例中的两个“ r”更改为“ m”(m表示操作数位于内存中,而不是寄存器中),编译后的结果是:
移动输入,结果
很明显,这是一条非法指令,因此限制字符串必须符合指令的操作数要求. 例如,指令movl允许寄存器到寄存器,立即数到寄存器等,但不允许内存到内存操作,因此两个操作数不能同时使用“ m”作为限定符.
内联汇编语法如下:
__ asm __(汇编语句模板: 输出部分: 输入部分: 破坏描述部分)
总共四个部分: 汇编语句模板,输出部分,输入部分,销毁描述部分,每个部分用“: ”分隔,汇编语句模板是必不可少的,其他三个部分是可选的,如果后者使用,并且前面部分为空,还需要使用“: ”打开,对应的部分为空. 例如:
__ asm__ __volatile __(“ cli” ::: “内存”)
1. 汇编语句模板
汇编语句模板由汇编语句序列组成. 使用“;”,“ \ n”或“ \ n \ t”来分隔语句. 指令中的操作数可以使用占位符来引用C语言变量. 操作数最多包含10个占位符,其名称如下: %0,%1,...,%9. 指令中由占位符表示的操作数始终被视为长字符(4个字节),但是取决于指令,应用于它们的操作可以是字或字节. 当使用操作数作为单词或字节时c语言对应汇编语句,默认值为低位字或低位字节. 字节操作可以显式指示是低字节还是子字节. 方法是在%和序列号之间插入一个字母,“ b”代表低字节,“ h”代表高字节,例如: %h1.
2. 输出部分
输出部分描述输出操作数. 不同的操作数描述符用逗号分隔. 每个操作数描述符由合格的字符串和C语言变量组成. 每个输出操作数的限定字符串必须包含“ =”,以表明它是一个输出操作数.
示例:
__ asm__ __volatile __(“ pushfl; popl%0; cli”: “ = g”(x))
描述符字符串指示对变量的限制,以便GCC可以根据这些条件决定如何分配寄存器,以及如何生成必要的代码来处理指令操作数与C表达式或C变量之间的连接.
3. 输入部分
输入部分描述输入操作数. 不同的操作数描述符用逗号分隔. 每个操作数描述符都由限定的字符串和C语言表达式或C语言变量组成.
示例1:
__ asm__ __volatile__(“ lidt%0” ::“ m”(real_mode_idt));
示例二(bitops.h):
静态__inline__ void __set_bit(int nr,volatile void * addr)
{
__ asm __(
“ btsl%1,%0”
: “ = m”(ADDR)
: “ Ir”(nr));
}
示例功能是将(* addr)的nr位设置为1. 第一个占位符%0对应于C语言变量ADDR,第二个占位符%1对应于C语言变量nr. 因此,以上汇编语句代码等效于以下伪代码: btsl nr,ADDR,此指令的两个操作数不能全部是存储器变量,因此将nr的合格字符串指定为“ Ir”,并将nr和立即数指定为或寄存器是关联的,因此两个操作数中只有ADDR是存储变量.
4. 限制字符
4.1. 受限字符列表
有多种限制字符,其中一些与特定体系结构有关. 这只是i386中可能使用的常用限定符和一些常用限定符. 它们的作用是指导编译器如何处理后续C语言变量和指令操作数之间的关系.
类别预选赛说明
通用寄存器“ a”将输入变量放入eax
这里有一个问题: 如果已经使用了eax,该怎么办?
实际上,这非常简单: 因为GCC知道已经使用了eax,所以它在此汇编代码中
在开头插入一句pushl%eax,将eax的内容保存到堆栈中,然后
在此代码的末尾添加一条语句popl%eax以恢复eax的内容
“ b”将输入变量放入ebx
“ c”将输入变量放入ecx
“ d”将输入变量放入edx
“ s”将输入变量放入esi
“ d”将输入变量放入edi
“ q”将输入变量放入eax,ebx,ecx和edx之一
“ r”将输入变量放入通用寄存器,即eax,ebx,ecx,
edx,esi,edi之一
“ A”将eax和edx组合到一个64位寄存器中(使用长整型)
内存“ m”个内存变量
“ o”操作数是一个内存变量,但其寻址方法是偏移类型,
即基址或基址加索引地址
“ V”操作数是一个内存变量,但寻址模式不是偏移类型
“”“操作数是内存变量,但寻址模式是自动递增
“ p”操作数是一个合法的内存地址(指针)
寄存器或存储器“ g”将输入变量放入eax,ebx,ecx和edx之一
或作为存储变量
“ X”操作数可以是任何类型
立即
“ I”之间的立即数0-31(用于32位移位指令)
“ J” 0-63之间的立即数(用于64位移位指令)
“ N”立即值介于0-255之间(用于out命令)
“我”立即
“ n”个即时数据,某些系统不支持单词以外的即时数据,
这些系统应使用“ n”代替“ i”
匹配“ 0”表示受其限制的操作数与指定的操作数匹配,
“ 1” ...,即操作数是指定的操作数,例如“ 0”
使用“ 9”来描述“%1”操作数,然后实际上是“%1”引用它
是“%0”操作数,请注意0-9和
作为限定词
指令中“%0”和“%9”之间的区别,前者描述了操作数,
后者代表操作数.
&输出操作数不能使用与输入操作数相同的寄存器
操作符类型“ =”操作数仅写在指令(输出操作数)中
“ +”操作数在指令中是读写类型(输入和输出操作数)
浮点数“ f”浮点寄存器
“ t”第一个浮点寄存器
“ u”个第二个浮点寄存器
“ G”标准的80387浮点常数
%该操作数可以与下一个操作数交换位置
例如,addl的两个操作数可以交换
(当然,两个操作数都不能是立即数)
#一些注释c语言对应汇编语句,从该字符到逗号后的所有字母都将被忽略
*表示如果选择了寄存器,则会忽略其后的字母
5. 破坏描述部分
破坏描述符用于通知编译器我们使用了哪些寄存器或内存. 它由逗号分隔的字符串组成. 每个字符串描述一个情况,通常是一个寄存器名称. 除了寄存器外,还有“内存”. 例如: “%eax”,“%ebx”,“内存”等.
“内存”很特殊,可能是内联汇编中最困难的部分. 为了清楚地解释它,首先介绍编译器的优化知识,然后查看C关键字volatile. 最后,查看描述符.
1. 编译器优化简介
内存访问速度远远小于CPU处理速度. 为了提高计算机的整体性能,将硬件高速缓存引入硬件中以加快对内存的访问. 另外,在现代CPU中,不必严格按顺序执行指令的执行,并且可以不按顺序执行没有相关性的指令,从而充分利用CPU的指令流水线并提高执行速度. 以上是硬件级别的优化. 从软件级别看优化: 一种是程序员在编写代码时的优化,另一种是编译器的优化. 编译器优化的常用方法是: 将内存变量缓存到寄存器中;调整指令顺序以充分利用CPU指令流水线,通常对读取和写入指令进行重新排序. 优化常规内存时,这些优化是透明且高效的. 解决由编译器优化或硬件重新排序引起的问题的方法是,在必须从硬件(或其他处理器)角度按特定顺序执行的操作之间设置内存屏障. Linux提供了宏解决方案编译器的执行顺序.
void屏障(void)
此函数通知编译器插入一个内存屏障,但对硬件无效. 编译后的代码会将当前CPU寄存器中的所有修改后的值存储到内存中. 当需要这些数据时,它们将再次从存储器中读出.
2. C语言关键字volatile
C语言关键字volatile(请注意,它用于修改变量,而不是上面介绍的__volatile__)表示变量的值可以在外部更改,因此每次都无法将对这些变量的访问缓存在寄存器中-访问时使用. 此关键字通常在多线程环境中使用,因为在编写多线程程序时,同一变量可能会被多个线程修改,并且程序会通过该变量同步每个线程,例如:
DWORD __stdcall threadFunc(LPVOID信号)
{
int * intSignal = reinterpret_cast (信号);
* intSignal = 2;
while(* intSignal!= 1)
sleep(1000);
返回0;
}
程启动时将intSignal设置为2,然后循环运行直到intSignal为1退出. 显然,必须从外部更改intSignal的值,否则该线程将不会退出. 但是,即使在外部将其值更改为1,该线程在实际运行时也不会退出,通过查看相应的伪汇编代码可以清楚地看到该线程:
移动斧头,信号
标签:
if(ax!= 1)
转到标签
对于C编译器,它不知道此值将被其他线程修改. 自然地将其缓存在寄存器中. 记住,C编译器没有线程的概念!这时,您需要使用volatile. volatile的原始含义是: 可以在当前线程之外更改此值. 换句话说,我们需要在threadFunc的intSignal前面添加volatile关键字. 此时,编译器知道该变量的值将在外部更改,因此每次访问该变量时都会再次读取该变量,并且循环变为如下伪代码所示:
标签:
移动斧头,信号
if(ax!= 1)
转到标签
3,内存
基于上述知识,不难理解内存修改描述符. 内存描述符告诉GCC:
1)不要重新排列内联汇编说明和先前的说明;也就是说,在执行内联汇编代码之前,必须先执行之前的所有指令
2)不要在寄存器中缓存变量,因为此代码可能使用内存变量,并且这些内存变量将以不可预测的方式更改,因此GCC插入必要的代码以首先将变量值缓存在寄存器中到内存,如果以后再访问这些变量,则需要再次访问内存.
本文来自电脑杂谈,转载请注明本文网址:
http://www.pc-fly.com/a/jisuanjixue/article-249642-1.html