汇编指令入门级整理 | 原力计划
作者 | AlbertS
出品 | CSDN 博客
前言
我们大都是被高级语言惯坏了的一代,源源不断的新特性正在逐步添加到各类高级语言之中,汇编作为最接近机器指令的低级语言,已经很少被直接拿来写程序了,不过我还真的遇到了一个,那是之前的一个同事,因为在写代码时遇到了成员函数权限及可见性的问题,导致他无法正确调用想执行的函数,结果他就开始在 C++ 代码里嵌入汇编了,绕过了种种限制终于如愿以偿,但是读代码的我们傻眼了…
因为项目是跨平台的,代码推送的 Linux 上编译的时候他才发现,汇编代码的语法在 Linux 和 Windows 上居然是不一样的,结果他又用一个判断平台的宏定义“完美”的解决了,最终这些代码肯定是重写了啊,因为可读性太差了,最近在学习左值、右值、左引用和右引用的时候,总是有人用程序编译生成的中间汇编代码来解释问题,看得我迷迷糊糊,所以决定熟悉一下简单的汇编指令,边学习边记录,方便今后忘记了可以直接拿来复习。
什么是汇编语言
汇编语言是最接近机器语言的编程语言,引用百科中的一段话解释为:
汇编语言(assembly language)是一种用于电子计算机、微处理器、微控制器或其他可编程器件的低级语言,亦称为符号语言。在汇编语言中,用助记符代替机器指令的操作码,用地址符号或标号代替指令或操作数的地址。汇编语言又被称为第二代计算机语言。
汇编语言产生的原因
对于绝大多数人来说,二进制程序是不可读的,当然有能人可以读,比如第一代程序员,但这类人快灭绝了,直接看二进制不容易看出来究竟做了什么事情,比如最简单的加法指令二进制表示为 00000011,如果它混在一大串01字符串中就很难把它找出来,所以汇编语言主要就是为了解决二进制编码的可读性问题。
汇编与二进制的关系
换句话来说,汇编语言就是把给机器看的二进制编码翻译成人话,汇编指令是机器指令的助记符,与机器指令是一一对应的关系,是一种便于阅读和记忆的书写格式。有效地解决了机器指令编写程序难度大的问题,并且使用编译器,可以很方便的把汇编程序转译成机器指令程序,比如之前提到的 00000011 加法指令,对应的汇编指令是 ADD,在调用汇编器时就会把 ADD 翻译成 00000011。
寄存器
说到汇编指令不得不提到寄存器,寄存器本身是用来存数据的,因为 CPU 本身只负责逻辑运算,数据需要单独储存在其他的地方,但是对于不熟悉寄存器的人来说会有疑惑,数据不是存在硬盘上吗?或者说数据不是存在内存中吗?这些想法都没错,那么寄存器是用来做什么的呢?
寄存器作用
其实硬盘、内存都是用来存储数据的,但是 CPU 的运算速度远高于内存的读写速度,更不用说从硬盘上取数据了,所以为了避免被拖慢速度影响效率,CPU 都自带一级缓存和二级缓存,一些 CPU 甚至增加了三级缓存,从这些缓存中读写数据要比内存快很多,但是还是无法使用飞速运转的 CPU,所以才会有寄存器的存在。
寄存器不是后来增加的,在最初的计算中就已经设计出来,相比而言,多级缓存出现的更晚一些,通常那些最频繁读写的数据都会被放在寄存器里面,CPU 优先读写寄存器,再通过寄存器、缓存跟内存来交换数据,达到缓冲的目的,因为可以通过名称访问寄存器,这样访问速度是最快的,因此也被称为零级缓存。
存取速度比较
通过上面的叙述我们可以知道存取速度从高到低分别是: 寄存器 > 1级缓存 > 2级缓存 > 3级缓存 > 内存 > 硬盘,关于它们的存取速度,举个例子很容易就能明白了,比如我们做菜(CPU工作)时,取手中(寄存器)正拿着的肉和蔬菜肯定是最快的,如果没有就需要把案板上(1级缓存)处理好的菜拿过来,如果案板上没有就在更远一点的洗菜池(2级缓存)中找一找,还没找到的话就要到冰箱(3级缓存)中看一看了,这时发现家里真没有,那去楼下的菜店(内存)去买点吧,转了一圈发现没有想要的,最后还是开车去农贸市场(硬盘)买吧。
通过上面这个例子应该能明白它们的速度关系了,既然缓存这么快,为什么不用缓存代替内存,或者将2、3级缓存都换成1级缓存呢?这里边有一个成本问题,速度越快对应着价格越高,如果你买过机械硬盘和固态硬盘应该很容易就理解了。
寄存器分类
常用的 x86 CPU 寄存器有8个:EAX 、EBX、ECX、EDX、EDI、ESI、EBP、ESP,据说现在寄存器总数已经超过100个了,等我找到相关资料再来补充,上面这几个寄存器是最常用的,这些名字也常常出现在汇编的代码中。
我们常说的32位、64位 CPU 是指数据总线的宽度或根数,而寄存器是暂存数据和中间结果的单元,因此寄存器的位数也就是处理数据的长度与数据总线的根数是相同的,所以32位 CPU 对应的寄存器也应该是32位的。
常用寄存器用途
上面提到大8个寄存器都有其特定的用途,我们以32位 CPU 为例简单说明下这些寄存器的作用,整理如下表:
寄存器EAX、AX、AH、AL的关系
在上面的图标中每个常用寄存器后面还有其他的名字,它们是同一个寄存器不同用法下的不同名字,比如在32位 CPU 上,EAX是32位的寄存器,而AX是EAX的低16位,AH是AX的高8位,而AL是AX的低8位,它们的对照关系如下:
汇编语言指令
终于说到汇编常用指令了,因为 linux 和 windows 下的汇编语法是有些不同的,所以下面我们先通过 windows 下的汇编指令来简单学习一下,后续再来比较两者的不同。
数据传送指令
算术运算指令
逻辑运算指令
循环控制指令
转移指令
Linux 和 windows 下汇编的区别
前面说到 linux 和 windows 下的汇编语法是不同的,其实两种语法的不同和系统不同没有绝对的关系,一般在 linux 上会使用 gcc/g++ 编译器,而在 windows 上会使用微软的 cl 也就是 MSBUILD,所以产生不同的代码是因为编译器不同,gcc 下采用的是AT&T的汇编语法格式,MSBUILD 采用的是Intel汇编语法格式。
总结
-
汇编指令是机器指令的助记符,与机器指令是一一对应的
-
AT&T的汇编语法格式和Intel汇编语法格式的是不同的
-
常用寄存器:EAX 、EBX、ECX、EDX、EDI、ESI、EBP、ESP
-
存取速度从高到低分别是: 寄存器 > 1级缓存 > 2级缓存 > 3级缓存 > 内存 > 硬盘
-
常用的汇编指令:mov、je、jmp、call、add、sub、inc、dec、and、or
版权声明:本文为CSDN博主「AlbertS」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/albertsh/article/details/106041560
☞华为全球分析师大会:HMS Core全球开发者应用集成的数量加速增长,打造全场景智慧体验
8086汇编(16位汇编)学习笔记09.宏汇编
宏汇编在文件中是当做关键字的,但是在bug中运行时并没有这些指令,这些关键词被称为伪指令,cpu并不认识他们,需要经过编译器转化成 cpu认识的代码,但是他多我们写代码帮助又很大
表达式
表达式中的求值是在程序链接时完成的,所以表达式中的各值必须是在汇编或链接期就能确定,也就是 说不能将寄存器或者变量运⽤于表达式。
算术表达式
逻辑运算符
逻辑运算即位运算,逻辑运算符与对应的指令助记符单词是相同的,当它们出现在操作码部分时是指 令,出现在操作数时是逻辑运算符
关系运算符
关系运算符的结果,如果结果为真,则所有位都置为1,即FFFF;否则所有位都置为0,即 .
- @@是匿名标号
- @b 向上查找最近的@@, b是back
- @f 向下查找最近的@@ ,f是front
一般我们用 jmp 跳转你需要有标号,而且标号名必须唯一 ,但是如果 标号名是 @@ ,那么就可以重复,跳转用 @b 或 @f , @@ 的 跳转距离在段内是 没有限制的
格式 ORG 偏移值
此指令后的下⼀个变量或指令从 偏移值 开始存放,即把一个变量或者指令放在指定的偏移内,可以提前,也可以延后
- $伪指令代表当前指令或变量的地址
- 常⽤于计算缓冲区⻓度和获取当前ip值
- 可与 ORG 配合使⽤
结构体名 struc
结构体名 ends
结构体使⽤<>来初始化
结构体可以通过变量名和寄存器来访问成员
结构体可以在定义时候初始化,也可以在后面初始化
结构作为局部变量无法赋初值
作用:用来定义宏
不可以重命名
可⽤于常量和表达式
可⽤于字符串
可⽤于指令名,给指令取别名
可⽤于类型,给类型取别名
可⽤于操作数
可以被修改
只能⽤于常数
宏名 macro [参数1][,参数2]…
endm
宏会在使⽤的地⽅展开
宏可以带参数
字符串拼接使⽤&
- 源文件后缀名为asm
- 每个源文件末尾需要有end
- 汇编头文件后缀名为inc
- 头文件包含 include xxx.inc
- 头文件防重复包含
- ifndef SECOND_1 SECOND_1 equ 1 Func1 proto far stdcall arg1 : word, arg2 : word extern g_dw : word endif
函数在源文件定义,在头文件中声明即可。
全局变量在定义文件中必须使用public指明此变量为全局public变量名
全局变量在使用文件中必须使用extern 指明此变量来自外部文件 extern 变量:类型
格式
格式1:
.IF condition ;以英文“句号”开头 ;条件\”condition\”成立时所执行的指令序列
指令序列
.ENDIF
格式2:
.IF condition ;条件\”condition\”不成立时所执行的指令序列
指令序列1
.ELSE
指令序列2
.ENDIF
格式3:
.IF condition1
指令序列1
.ELSEIF condition2 ;条件\”condition2\”成立时所执行的指令序列
指令序列2
.ENDIF
其中:条件表达式“condition”的书写方式与C语言中条件表达式的书写方式相似,也可用括号来组成复杂的条件表达式。
条件表达式中可用的操作符有:==(等于)、!=(不等)、>(大于)、>=(大于等于)、<(小于)、<=(小于等于)、&(位操作与)、!(逻辑非)、&&(逻辑与)、||(逻辑或)等。
1、WHILE型循环伪指令
.WHILE condition ;条件\”condition”成立时所执行的指令序列 循环体的指令序列 .ENDW
其中:.ENDW与前面的.WHILE相匹配,它标志着其循环体到此结束。
如果条件表达式“condition”在循环开始时,就为“假”(false),那么,该循环体一次也不会被执行。
2、REPEAT型循环伪指令
.REPEAT 循环体的指令序列.UNTIL condition .REPEAT 循环体的指令序列.UNTILCXZ [condition]
REPEAT型循环在执行完循环体后,才判定逻辑表达式condition的值。若该表达式的值为真,则终止该循环,并将执行伪指令.UNTIL[CXZ]后面的指令,否则,将向上跳转到伪指令.REPEAT之后的指令,为继续执行其循环体作准备
循环终止 break 和继续 continue
(1)、终止循环伪指令.BREAK
.BREAK .IF condition
该伪指令用来终止包含它的最内层循环。前者是无条件终止循环,后者是仅当逻辑表达式condition为真时,才终止循环。.WHILE 1 .REPEAT….BREAK .IF condition…….BREAK .IF condition…
ENDW .UNTIL 0
对于以上二个循环,如果没有指令来终止循环的话,它们都将进入死循环状态,但如果在该层循环体内,存在伪指令“.BREAK .IF condition”的话,那么,当逻辑表达式condition为真时,该循环就会被终止了。
(2)、循环继续伪指令
.CONTINUE.CONTINUE .IF condition
我们一起学RISC-V——10-C和汇编语言混合编程
本期内容如下:
- GCC内联汇编简述
- GCC内联汇编“输出操作数”和“输入操作数”部分
- GCC内联汇编“可能影响的寄存器或存储器”部分
- GCC内联汇编参考实例一
- GCC内联汇编参考实例二
- 在汇编中调用C/C++函数
一、GCC内联汇编简述
由于本文介绍的是GCC的RISC-V工具链,因此在C/C++程序中嵌入汇编程序遵循GCC内联汇编(inline asm )语法规则,其格式由如下部分组成:
各组成部分简述如下:
注意:也可以使用前后各带两个下划线的asm__,_asm_是GCC 关键字asm 的宏定义。
注意:也可以使用_volatile_,__volatile是GCC 关键字volatile 的宏定义。
注意:“汇编指令列表”中的编写语法和普通的汇编程序编写一样,可以在其中定义标签(Label)、定义对齐(.align n )、定义段(.section name )等。
有关“输出操作数”部分的详细介绍,本文后续内容详细介绍。
有关“输入操作数”部分的详细介绍,本文后续内容详细介绍。
有关“可能影响的寄存器或存储器”部分的详细介绍,本文后续内容详细介绍。
综上,一个典型的完整内联RISC-V汇编程序格式如下:
二、 GCC内联汇编“输出操作数”和“输入操作数”部分
由于C/C++中使用的是抽象层次较高的变量或者表达式,如下所示:
而汇编指令中直接操作的是寄存器,以RISC-V指令集为例,一个加法指令的汇编指令如下:
那么,当在C/C++程序中添加了汇编程序之时,程序员如何将其所需要操作的C/C++变量与汇编指令的操作数对应起来呢?那就需要使用到GCC内联汇编的“输出操作数”和“输入操作数”部分来指定。
GCC内联汇编语法的“输入操作数”和“输出操作数”部分用来指定当前内联汇编程序的输入和输出操作符列表。其遵循如下语法格式:
每一个输入或者输出操作符都由3部分组成,分别为:
:[ 汇编操作数字符名 ]\”=rm\”(C变量或表达式),[ 汇编操作数字符名 ]\”=rm\”(C变量或表达式)
(1)方括号[]中的符号名,用于将内联汇编指令列表中使用的操作数(由%[字符]指定)和此操作符(由[字符]指定)通过同名“字符”绑定起来。
在汇编指令列表中除了使用“%[字符]”中明确的符号命名指定之外,还可以使用“%数字”的方式进行隐含指定。“数字”从0开始,依次表示输出操作数和输入操作数。譬如:假设包含“输出操作数”列表中有2个操作数,“输入操作数”列表中有2个操作数,则汇编程序中%0表示第一个输出操作数,%1表示第二个输出操作数,%2表示第一个输入操作数,%3表示第二个输入操作数。
(2)引号中的限制字符串,用于约束此操作数变量的属性,常用的约束如:
字母“r”代表使用编译器自动分配的寄存器来存储该操作数变量;字母“m”代表使用内存地址来存储该操作数变量。如果同时指明“rm”则编译器自动选择最优方案。
对于“输出操作数”而言,等号“=”代表输出变量用作输出,原来的值会被新值替换;加号“+”代表输出变量不仅作为输出,还作为输入。
注意:此约束对不适用于“输入操作数”。
(3)圆括号()中的内容为C/C++变量或者表达式。
输出操作符之间需要使用逗号分割。
三、GCC内联汇编“可能影响的寄存器或存储器”部分
如果内联汇编中的某个指令会更新某些寄存器的值,则必须在asm中第三个冒号后的“可能影响的寄存器或存储器”中显示的指定出这些寄存器,从而通知GCC编译器让其不再假定之前存入这些寄存器中的值依然合法。指定出这些寄存器由逗号分隔开,每个寄存器由引号包含住,如下所示:
注意:对于那些已经由“输入操作数”和“输出操作数”部分约束指定了的变量,由于编译器自动分配寄存器,因此编译器知道哪些寄存器会被更新,所有程序员无需担心这部分寄存器,不用在“可能影响的寄存器或存储器”进行显示的指定。
如果内联汇编中的某个指令会以无法预料的形式修改了存储器中的值,则必须在asm中第三个冒号后的“可能影响的寄存器或存储器”中显示的加上“memory”,从而通知GCC编译器不要将存储器中的值暂存在处理器的通用寄存器中。
四、 GCC内联汇编参考实例一
以下描述的“add”汇编和c程序给出了一个完整的实例,代码如下:
从上述示例可以看出,通过使用“输出操作数”和“输入操作数”部分的指定,可以将C/C++中的变量或者表达式映射到汇编指令中充当操作数进行操作。在此过程中,程序员无需关心真正执行的汇编指令具体使用的寄存器索引是什么(譬如到底是t1,还是t2等等),编译器会根据引号中指定的操作数约束按照编译优化的原则来分配合理的寄存器索引号。因此,程序员仅仅需要关心操作数和变量的映射,无需关心操作数会映射到处理器具体的哪个通用寄存器,使得软件程序员能够从底层硬件的细节中被解放出来。
五、GCC内联汇编参考实例二
RISC-V架构中定义的CSR寄存器由于需要使用特殊的CSR指令进行访问,如果在C/C++程序中需要使用CSR寄存器,只能够采用内嵌汇编(CSR指令)的方式才能够对CSR寄存器进行操作。以下是在C语言中调用RISC-V的CSR读或者写汇编指令访问CSR寄存器的一个实例,代码如下:
GCC内联汇编语法的规则比较复杂,信息量很大。本文由于限于篇幅,仅对其最基本的语法和示例进行介绍,以帮助读者能够看懂并且编写简单的C/C++内联汇编程序。感兴趣的读者可以自行查阅完整的GNU C/C++内联汇编语法手册了解更多详情。
六、在汇编中调用C/C++函数
除了在C/C++程序中内嵌汇编程序之外,还可以在汇编程序中调用C/C++函数。这种情形在实际的工程中使用也很常见,由于C/C++语言构造的函数非常普遍,在某些以汇编程序为主体的程序中也会调用C/C++的函数。
在介绍C/C++函数调用之前,需要先介绍应用程序二进制接口(Abstract Binary Interface,ABI),ABI描述了应用程序和操作系统之间,应用和它的库之间,或者应用的组成部分之间的接口。ABI涵盖了各种细节,如:
其中,函数调用约定决定了函数调用时参数传递和函数返回结果的规则,有关RISC-V架构ABI的函数调用约定。
对于RISC-V汇编程序而言,在汇编程序中调用C/C++语言函数,必须遵照ABI所定义的函数调用规则,即,函数参数由寄存器a0-a7所传递,函数返回由寄存器a0-a1所指定,一个具体的示例代码如下:
汇编语言由于是一种低级语言,因此抽象层次较低,程序编写难度较大,在实际的工作中,更多的情形是能够阅读理解某些现有的汇编代码,或者编写比较简单的汇编程序。
由于本文介绍的RISC-V工具链基于的是GCC工具链,因此RISC-V汇编程序也遵循GNU汇编语法规则,完整的GNU汇编语法手册长达数百页,介绍了大量的伪操作和语法,但是大多数的语法并不常用。本文由于限于篇幅,仅对RISC-V汇编常用的语法进行简要介绍,以帮助读者初步认识RISC-V汇编语言程序、能够看懂并且编写简单而基本的汇编程序。如果对于RISC-V汇编编程进阶感兴趣的读者可以自行查阅RISC-V汇编语言的完整的GNU汇编语法手册了解更多详情。
本文作者及来源:Renderbus瑞云渲染农场https://www.renderbus.com
文章为作者独立观点不代本网立场,未经允许不得转载。