当前位置: 首页 > article >正文

深入理解x86汇编:GNU格式的全面指南

在计算机科学的浩瀚海洋中,汇编语言如同一座不可忽视的灯塔,引导我们深入理解计算机的本质和运作机制。x86 汇编语言作为现代计算机架构中的核心之一,其复杂性和强大能力使其成为每位程序员都值得掌握的技能。而 GNU 格式提供了一种灵活且广泛使用的方法,使得我们能够更高效地与底层硬件进行交互。

在这篇指南中,我们将带您一步步走入 x86 汇编语言的世界,探索 GNU 格式的基本语法、常用指令及其背后的原理。无论您是初学者还是有一定经验的开发者,这里都将为您提供清晰易懂的示例和实用技巧,帮助您更好地理解汇编语言如何与高级语言协同工作,提升程序性能。通过本指南,您将不仅仅学习到如何写出功能完备的汇编代码,还能培养对计算机系统深刻的理解。这是一段充满挑战但同时也充满乐趣的旅程,让我们一起开启这段探索之旅吧!

一、x86 汇编语言简介

x86 汇编语言主要包括总线、寄存器结构,数据类型,基本的操作指令以及函数的调用规则。其中,x86 汇编语言的总线包括地址总线、数据总线和控制总线,分别决定了 CPU 的寻址能力、数据传送量和对系统中其他器件的控制能力。

在寄存器结构方面,x86 汇编语言拥有丰富的寄存器体系。通用寄存器包括 EAX、EBX、ECX、EDX、ESP、EBP、EBX、EDX、ESI、EDI。EAX 通常用于计算,是很多加法乘法指令的缺省寄存器,还存放函数返回值;ECX 通常用于循环变量计数,是重复前缀指令和 LOOP 指令的内定计数器;ESP 指示栈指针,用于指示栈顶位置;EBP 指示基址指针,用于指示子程序或函数调用的基址指针;EBX 是基地址寄存器;EDX 总是被用来放整数除法产生的余数;ESI/EDI 是 “源 / 目标索引寄存器”,在很多字符串操作指令中,DS:ESI 指向源串,而 ES:EDI 指向目标串。此外,EAX、EBX、ECX 和 EDX 的前两个高位字节和后两个低位字节可以独立使用,其中两位低字节又被独立分为 H 和 L 部分,以兼容 16 位的程序。

段寄存器用于定位内存节,包括 CS、SS、DS、ES、FS、GS。状态标志寄存器置位值为 1 或者清除值为 0,这些值由 CPU 控制,如 ZF、CF、SF、TF 等标志。在 x86 中,EIP 寄存器又称指令指针或程序计数器,保存了程序将要执行的下一条指令在内存中的地址。

数据类型方面,在 x86/x64 体系中,指令处理的数据分为 fundamental(基础)和 numeric(数值)两大类。基础类型有 byte(8 位)、word(16 位)、doubleword(32 位)和 quadword(64 位),代表指令能一次性处理的数据宽度。数值类型包括 integer(整型数)、floating-point(浮点数)、BCD(binary-code decmial integer)和 SIMD(single instruction, multiple data)等。

基本的操作指令涵盖数据传送指令、逻辑计算指令和算数运算指令等。例如,MOV 指令是数据传送指令中的一种,用于在不同的存储单元之间复制数据,其基本形式包括将数据从寄存器、内存或立即数移动到另一个寄存器或内存位置。PUSH 入栈指令用于将数据存储到堆栈中,POP 出栈指令用于从堆栈中取出数据并存储到指定的目标位置。XCHG 指令用于交换两个操作数的内容。此外,还有算术运算指令如 ADD、SUB、MUL、DIV 等,逻辑运算指令如 AND、OR、XOR、NOT 等。

在函数的调用规则方面,涉及到 CALL 和 RET 等指令。CALL 指令用于调用子程序,它将 IP 寄存器(PC)的 IP 旧值压栈保存,并设置 IP 新值,无条件转移到被调用函数的第一条指令。RET 指令用于子程序返回,从函数的栈帧顶部找到 IP 旧值,将其出栈并恢复 IP 寄存器。

1.1内存

一个程序内存主要分为栈、堆、代码、数据四个节,分别用于函数的局部变量和参数、动态内存、程序指令和全局值。

  • 栈:在 x86 架构中,栈用于函数的局部变量和参数,以及控制程序执行流。栈是一种后进先出的数据结构,遵循特定的规则进行操作。例如,ESP(栈指针)寄存器包含了指向栈顶的内存地址,当数据被压入栈时,如使用 “push eax” 指令,ESP 会随之减小 4。当数据被取出时,如 “pop ebx” 指令,ESP 会增加 4。EBP(栈基址寄存器)在一个函数中会保持不变,程序把它当成定位器,用来确定局部变量和参数的位置。许多函数包含一段 “序言” 和 “结语”,在函数开始处保存函数中要用的栈和寄存器,在函数结尾恢复栈和这些寄存器。在函数调用过程中,栈也起到重要作用。使用 push 指令将参数压入栈中,使用 call memory_location 来调用函数时,当前指令地址(指 EIP 寄存器中的内容)被压入栈中,这个地址会在函数结束之后,被用于返回到主代码。

  • 堆:堆是为程序执行期间需要的动态内存准备的,用于创建(分配)新的值,以及消除(释放)不再需要的值。将其称为动态内存,是因为其内容在程序运行期间经常被改变。

  • 代码:代码节包含了在执行程序任务时 CPU 所取得的指令。这些代码决定了程序是做什么的,以及程序中的任务如何协调工作。

  • 数据:数据节包含了一些值,这些值在程序初始加载时被放到这里,称为静态值,因为程序运行时它们可能并不发生变化,还可以称为全局值,因为程序的任何部分都可以使用它们。

1.2总线

地址总线:宽度决定 CPU 的寻址能力。一个 CPU 有 N 根地址总线,最多可以寻找 2N 内存单元(B)。例如,8086CPU 有 20 位的地址总线,理论上拥有 1MB(220Byte)的寻址能力。地址总线的宽度决定了 CPU 能够访问的内存大小范围,它指定了存储器单元,使 CPU 能够准确地找到所需的数据和指令在内存中的位置。

数据总线:宽度决定 CPU 与其他器件进行数据传送时的一次数据传送量。一个 CPU 有 N 根数据总线,一次可传送一个 N 位二进制数据。如数据总线的宽度为 8 根则可传送 1B 的数据。数据总线的宽度决定了数据传输的速度,它是 CPU 与内存或其他器件之间进行数据传送的通道。

控制总线:宽度决定 CPU 对系统中其他器件的控制能力。有多少根控制总线,意味着 CPU 提供了多少种对外部器件的控制。控制总线负责传输控制信息,如器件的选择、读或写的命令等,它决定了 CPU 对系统中其他器件的控制能力和方式。

二、寄存器详解

2.1通用寄存器

一个x86-64的中央处理单元(CPU)包含一组16个存储64位值的通用寄存器。这些寄存器用来存储整数数据和指针。下图显示了这16个寄存器。它们的名字都以%r开头,不过后面还跟着不同命名规则的名字,这是由于指令集历史演化造成的。最初的8086中有8个16位的寄存器,即图中的%ax到%bp。每个寄存器都有特殊的用途,它们的名字就反映了这些不同的用途。扩展到IA32架构时,这些寄存器也扩展成32位寄存器,标号从%eax到%ebp。扩招到x86-64后,原来的8个寄存器扩展成64位,标号从%rax到%rbp。除此之外,还增加了8个新的寄存器,它们的标号是按照新的命名规则制定的:从%r8到%r15。

通用寄存器:X86 处理器中有 8 个 32 位的通用寄存器,包括 EAX、ECX、ESP、EBP、EBX、EDX、ESI/EDI。

图片

    • EAX:通常用于计算,是很多加法乘法指令的缺省寄存器,还存放函数返回值。可以分为三个不同的寄存器 AX、AH 和 AL 寄存器。AX 寄存器是 EAX 寄存器的低 16 位部分,可用于存储 16 位的数据;AH 寄存器是 AX 寄存器的高 8 位部分,可用于存储 8 位的数据;AL 寄存器是 AX 寄存器的低 8 位部分,也可用于存储 8 位的数据。

    • EBX:基地址寄存器,在内存寻址时存放基地址。

    • ECX:计数器,是重复前缀指令和 LOOP 指令的内定计数器,也可作为数据寄存器使用。

    • EDX:总是被用来放整数除法产生的余数,也可作为数据寄存器使用。

    • ESP:堆栈指针,指向当前栈顶。在栈操作中,当数据被压入栈时,ESP 会随之减小 4;当数据被取出时,ESP 会增加 4。

    • EBP:基址指针,指向当前栈底。在一个函数中,EBP 通常保持不变,程序把它当成定位器,用来确定局部变量和参数的位置。

    • ESI/EDI:分别叫做 “源 / 目标索引寄存器”。在很多字符串操作指令中,DS:ESI 指向源串,而 ES:EDI 指向目标串。大多数情况下可视为通用寄存器。

指令可以对这16个寄存器的低位字节中存放的不同大小的数据进行操作。字节级操作可以访问最低的字节,16位操作可以访问最低的2个字节,32位操作可以访问最低的4个字节,而64位操作可以访问整个寄存器。

Tips:当指令以寄存器作为目标时,对于生成小于8字节结果的指令,寄存器中剩下的字节如何处理,有两条规则:
  • 生成1字节和2字节数字的指令会保持剩下的字节不变

  • 生成4字节的指令会把高位4字节置为0。

后面这条规则是作为从IA32到x86-64的扩展的一部分而采用的。

2.2标志寄存器EFLAFS

EFLAGS标志寄存器包含有状态标志位、控制标志位以及系统标志位,处理器在初始化时将EFLAGS标志寄存器赋值为00000002H。

下图描绘了EFLAGS标志寄存器各位的功能,其中的第1、3、5、15以及22~31位保留未使用。由于64位模式不再支持VM和NT标志位,所以处理器不应该再置位这两个标志位。

图片

TIPs:在64位模式中,EFLAGS标志寄存器已从32位扩展为64位,被称作RFLAGS寄存器。其中高32位保留未使用,低32位与EFLAGS相同。

接下来,我们会根据标志位功能将EFLAGS划分位状态标志、方向标志、系统标志和IOPL区域等几部分,并对各部分的标志位功能进行逐一讲解。

⑴状态标志

EFLAGS标志寄存器的状态标志(位0、2、4、6、7和11)可以反映出汇编指令计算结果的状态,像add、sub、mul、div等汇编指令计算结果的奇偶性、溢出状态、正负值皆可从上述状态找那个反映出来。

状态标志:标志寄存器,置位值为 1 或者清除值为 0,由 CPU 控制。

  • ZF:零标志,当一个操作的结果为 0 时,设置为 1;否则被清除。

  • CF:进位标志,当一个操作的结果相对于目标操作数太大或太小时,设置为 1;否则被清除。用于无符号数的向最高位进位(或者借位)。

  • SF:负数标志,当一个操作的结果为负数,设置为 1;若结果为正数,SF 被清除。对算术运算,当运算结果的最高位值为 1 时,SF 也会被置位。

  • DF:方向标志位,位于 EFLAGS 标志寄存器的第 10 位,控制着字符串指令的操作方向。置位 DF 标志位可使字符串指令按从高到低的地址方向(自减)操作数据,复位 DF 标志位可使字符串指令按从低到高的地址方向(自增)操作数据。汇编指令 std 和 cld 可用于置位和复位 DF 方向标志。

  • TF:陷阱标志位,用于调试。当它被置位时,x86 处理器每次只执行一条指令。

  • IF:中断标志位,若 IF 位为 1,表示中断开启,CPU 可响应外部可屏蔽中断;若为 0,表示中断关闭,CPU 不再响应来自 CPU 外部的可屏蔽中断,但 CPU 内部的异常还是要响应的。

  • IOPL:输入输出特权级,占用 2 位来表示 4 个任务特权级,即特权级 0~3。

  • NT:任务嵌套标志位,当一个任务中又嵌套调用了另一个任务时,此 NT 位为 1,否则为 0。

  • RF:恢复标志位,用于程序调试,指示是否接受调试故障,它需要与调试寄存器一起使用。当 RF 为 1 时忽略调试故障,为 0 时接受。

  • VM:虚拟 8086 模式标志位。

  • AC:对齐检查标志位,若 AC 位为 1 时,则进行地址对齐检查,位 0 时不检查。

  • VIF:虚拟中断标志位,虚拟模式下的中断标志。

  • VIP:虚拟中断挂起标志位,在多任务情况下,为操作系统提供的虚拟中断挂起信息,需要与 VIF 位配合。

  • ID:识别标志位,系统经常要判断 CPU 型号,若 ID 位为 1,表示当前 CPU 支持 CPUID 指令,这样便能获取 CPU 的型号、厂商信息等;若 ID 位为 0,则表示当前 CPU 不支持 CPUID 指令。

EIP 指令指针:保存程序将要执行的下一条指令在内存中的地址。在 x86 中,EIP 寄存器又称指令指针或程序计数器,控制了 EIP 即控制了 CPU 要执行什么。

这些标志位可反映出三种数据类型的计算结果:无符号整数、有符号整数和BCD整数(Binary-coded decimal integers)。其中CF标志位可反映出无符号整数运算结果的溢出状态;OF标志位可反映出有符号整数(补码表示)运算结果的溢出状态;AF标志位表示BCD整数运算结果的溢出状态;SF标志位反应出有符号整数运算结果的正负值;ZF标志位反映出有符号或无符号整数运算的结果是否为0。

以上这些标志位,只有CF标志位可通过stc、clc和cmc(Complement Carry Flag,计算原CF位的补码)汇编指令更改位值。它也可借助位操作指令(bt、bts、btr和btc指令)将指定位值复制到CF标志位。而且,CF标志位还可在多倍精度整数计算时,结合adc指令(含进位的加法计算)或sbb指令(含借位的减减法)将进位计算或借位计算扩展到下次计算中。

至于状态跳转指令Jcc、状态字节置位指令SETcc、状态循环指令LOOPcc以及状态移动指令CMOVcc,它们可将一个或多个状态标志位作为判断条件,进程分支跳转、字节置位以及循环计数。

⑵方向标志

DF方向标志位(Direction Flag)位于EFLAGS标志寄存器的第 10 位,它控制着字符串指令(诸如movs、cmps、scas、lods、stos等)的操作方向。置位DF标志位可使字符串指令按从高到低的地址方向(自减)操作数据,复位DF标志位可使字符串指令按从低到高的地址方向(自增)操作数据。汇编指令std和cld可用于置位和复位DF方向标志。

⑶系统标志和IOPL区域

  • 第 8 位为TF位,即Trap Flag,意为陷阱标志位。此位若为1,用于让CPU进入单步运行方式,若为0,则为连续工作的方式。平时我们用的debug程序,在单步调试时,原理上就是让TF位为1。

  • 第 9 位为IF位,即Interrupt Flag,意为中断标志位。若IF位为1,表示中断开启,CPU可响应外部可屏蔽中断。若为0,表示中断关闭,CPU不再响应来自CPU外部的可屏蔽中断,但CPU内部的异常还是要响应的。

  • 第 12~13 位为IOPL,即 Input Output Privilege Level,这用在有特权级概念的CPU中。有4个任务特权级,即特权级0~3,故IOPL要占用2位来表示这4种特权级。

  • 第 14 位为NT,即 Nest Task,意为任务嵌套标志位。8088支持多任务,一个任务就是一个进程。当一个任务中又嵌套调用了另一个任务时,此NT位为1,否则为0。

  • 第 16 位为RF位,即 Resume Flag,意为恢复标志位。该标志位用于程序调试,指示是否接受调试故障,它需要与调试寄存器一起使用。当RF为1时忽略调试故障,为0时接受。

  • 第 17 位为VM位,即 Virtual 8086 Model,意为虚拟8086模式。

  • 第 18 位为AC位,即 Alignment Check / Access Control,意为对齐检查。若AC位为1时,则进行地址对齐检查,位0时不检查。

  • 第 19 位为VIF位,即 Virtual Interrupt Flag,意为虚拟终端标志位,虚拟模式下的中断标志。

  • 第 20 位为VIP位,即 Virtual Interrupt Pending Flag,意为虚拟中断挂起标志位。在多任务情况下,为操作系统提供的虚拟中断挂起信息,需要与 VIF 位配合。

  • 第 21 位为ID位,即 Identification Flag,意为识别标志位。系统经常要判断CPU型号,若ID位为1,表示当前CPU支持CPUID指令,这样便能获取CPU的型号、厂商信息等;若ID位为0,则表示当前CPU不支持CPUID指令。

2.3段寄存器

x86-64架构,拥有6个16位段寄存器(CS、DS、SS、ES、FS和GS),用于保存16位段选择子。段选择子是一种特殊的指针,用于标识内存中的段。要访问内存中的特定段,该段的段选择子必须存在于相应的段寄存器中。

在平坦内存模型中,段选择子指向线性地址空间的地址0;在分段内存模型中,每个分段寄存器通常加载有不同的段选择子,以便每个分段寄存器指向线性地址空间内的不同分段。

每一个段寄存器表示三种存储类型之一:代码,数据,栈。

CS寄存器保存了代码段(code segment)选择子。代码段存储的是需要执行的指令,处理器使用CS寄存器内的代码段选择子和RIP/EIP寄存器的内容生成的线性地址来从代码段查询指令。RIP/EIP寄存器存储的是下一条要执行的指令在代码段上的偏移。

DS、ES、FS和GS寄存器指向了四个数据段(data segment)。SS寄存器包含栈段(stack segment)的段选择子,其中存储当前正在执行的程序、任务或处理程序的过程堆栈。

2.4 控制寄存器

目前,Intel处理器共拥有6个控制寄存器(CR0、CR1、CR2、CR3、CR4、CR8),它们有若干个标志位组成,通过这些标志位可以控制处理器的运行模式、开启扩展特性以及记录异常状态等功能。

2.5 指令指针寄存器

RIP/EIP寄存器,即指令指针寄存器,有时称为程序计数器。指令指针(RIP/EIP)寄存器包含当前代码段中要执行的下一条指令的偏移量。

2.6 MSR寄存器组

MSR(Model-Specific Register)寄存器组可提供性能监测、运行轨迹跟踪与调试以及其它处理器功能。在使用MSR寄存器组之前,我们应该通过CPUID.01h:EAX[5]来检测处理器是否支持MSR寄存器组。处理器可以使用RDMSR和WRMSR对MSR寄存器组进行访问,整个访问过程借助ECX寄存器索引寄存器地址,再由EDX:EAX组成的64位寄存器保持访问值。(在处理器支持64位模式下,RCX、RAX和RDX寄存器的高32位将会被忽略)。

三、数据表示

在 x86/x64 体系中,指令处理的数据分为 fundamental(基础)和 numeric(数值)两大类。

基础类型有 byte(8 位)、word(16 位)、doubleword(32 位)和 quadword(64 位),代表指令能一次性处理的数据宽度。例如,在处理一些简单的数据类型时,byte 可以用来存储单个字符或者小的整数,word 则可以存储稍大一些的整数。这些不同的数据宽度在不同的场景下有着不同的应用。

numeric(数值)类型包括 integer(整型数)、floating-point(浮点数)、BCD(binary-code decmial integer)和 SIMD(single instruction, multiple data)等。

integer(整型数)包括 unsigned 类型和 signed 类型。unsigned 类型的整数没有符号位,可以表示更大的非负整数范围;signed 类型的整数有符号位,可以表示正负整数。在实际编程中,根据需要选择合适的整数类型可以提高程序的效率和准确性。

floating-point(浮点数)包括 single-precision floating-point(单精度浮点数)、double-precision floating-point(双精度浮点数)、double extended-precision floating-point(拓展双精度浮点数)。这些浮点数类型遵循 IEEE754 标准,在不同的计算需求中,选择合适的浮点数类型可以保证计算的精度和性能。

BCD(binary-code decmial integer)包括 non-packed BCD 码和 packed BCD 码。在 BCD 码中,一个十进制的每一位,使用 8 位的二进制进行编码。非压缩的 BCD 码浪费了一半的空间,而压缩的 BCD 码每个 BCD 数字用 4 位表示,可以更有效地利用存储空间。

SIMD(single instruction, multiple data)属于 packed 类型的数据。SIMD 数据是在一个 operand(操作数)里集成了多个 integer、floating-point 或者 BCD 数据。SIMD 指令可以一次性同时处理这些数据,大大提高了数据处理的吞吐量,特别适用于一些需要大量数据并行处理的场景,如图形处理和科学计算等。

四、基础操作指令

指令格式:包括指令前缀、ModR/M、SIB、位移、立即数等部分。

x86 的指令格式为:[label:] mnemonic [operands][ ;comment ]。指令包含的操作数个数可以是:0 个,1 个,2 个或 3 个。操作数有 3 种基本类型:立即数、寄存器操作数和内存操作数。指令前缀在指令序列中可能起到调整内存操作数的段选择子、调整操作数的缺省大小等作用。例如,“26 66 c7 84 c8 44 33 22 11 78 56” 这条指令中,“26” 和 “66” 就是前缀部分,分别起到调整内存操作数的段选择子和调整操作数大小的作用。ModR/M 辅助说明操作码的操作数,定义操作数的属性。SIB 辅助说明 ModR/M,在操作码的操作数为内存地址且需要与 ModR/M 一起使用时辅助寻址。位移部分在操作码的操作数为内存地址(小端序排列)时,用来表示位移操作。立即数则是当操作码的操作数为常量时的该常量。

字节序:网络数据使用大端字节序,x86 使用小端字节序。如网络上 IP 地址为 127.0.0.1 是 0x7f000001,在 x86 小端字节序则是 0x0100007f。

操作数:分为内存地址、立即数、寄存器三个类型。

立即数是用数字文本表达式,如在 ATT 格式的汇编代码中,立即数的书写方式是 -577 或 $Ox1F。不同的指令允许的立即数值范围不同,汇编器会自动选择最紧凑的方式进行数值编码。

寄存器表示某个寄存器的内容,16 个寄存器的低位 1 字节、2 字节、4 字节或 8 字节中的一个作为操作数,这些字节数分别对应于 8 位、16 位、32 位或 64 位。使用 % R 的格式来表示寄存器的值,R 是寄存器的名称。例如,EAX、EBX、ECX、EDX、ESP、EBP、ESI/EDI 等通用寄存器,以及段寄存器 CS、SS、DS、ES、FS、GS 等都可以作为操作数。

内存引用其实表示的就是内存地址,不过这个计算的地址是虚拟地址,虚拟地址到物理地址的实际转换是操作系统做的事情。内存引用的情况很多,但是都是根据公式 Imm (Rb,Ri,s) 来计算的,其中 Imm:一个常数,表示虚拟地址的偏移量;Rb:基址寄存器;Ri:变址寄存器;s:比例因子,s 的值只能是 1,2,4,8,并且默认值是 1。最终的虚拟地址为:add = Imm + Rb 的值 + Ri 的值 ×s。例如,“4 (% rax)” 是一个内存引用,表示的是 (4 + 寄存器 rax 的值) 所指向的内存地址;“9 (% rax,% rdx)” 表示的是 (9 + 寄存器 rax 的值 + 寄存器 rdx 的值 * 1) 所指向的内存地址;“0XFC (,% rdx,4)” 表示的是 (0XFC + 寄存器 rdx 的值 * 4) 所指向的内存地址;“(% rax,% rdx,4)” 表示的是 (寄存器 rax 的值 + 寄存器 rdx 的值 * 4) 所指向的内存地址。

五、案例示例

5.1定义数据

⑴内部数据类型

  • Byte:8 位无符号整数,代表指令能一次性处理 8 位的数据宽度。例如,可以用来存储单个字符或者小的整数。

  • SByte:8 位有符号整数,可用于存储有符号的小数值。

  • Word:16 位无符号整数,在处理稍大一些的整数时可以使用。

  • SWord:16 位有符号整数,适用于有符号的中等大小的整数。

  • DWord:32 位无符号整数,可用于存储较大的数值或者作为地址等。

  • SDWord:32 位有符号整数,用于有符号的较大数值的处理。

  • FWord:48 位整数,在保护模式中的远指针等特定场景下使用。

  • QWord:80 位(10 字节)整数,可用于高精度的数值计算。

  • TByte:80 位(10 字节)整数,类似于 QWord,在需要较大数据宽度的场景下发挥作用。

  • Real4:32 位(4 字节)IEEE 短实数,用于存储单精度浮点数。

  • Real8:64 位(8 字节)IEEE 长实数,用于存储双精度浮点数。

  • Real10:80 位(10 字节)IEEE 拓展实数,适用于更高精度的浮点数计算。

⑵伪指令

  • DB:8 位整数,用于定义字节类型的数据,并将这些数据存储到内存中。例如,可以定义字符串、字符常量和数据表等。如 “msg DB 'Hello, world!', 0” 定义了一个名为 msg 的标号,后面跟着一个字符串常量和一个 0 字节。这个字符串会被存储在程序的数据段中,可以通过 msg 标号来引用这个字符串。

  • DW:16 位整数,用于定义字类型的数据。可以定义一系列 16 位的数值、字符常量等。

  • DD:32 位整数或实数,用于定义双字类型的数据,可以存储 32 位的整数或者实数。

  • DQ:64 位整数或实数,用于定义四字类型的数据,可以存储 64 位的整数或者实数。

  • DT:80 位(10 字节)整数,用于定义特定长度的整数数据。

  • EQU:把某变量当成一个常数,例如 “pi equ a; 表达式”“pi equ b; 用或 equ 定义过的符号”“pi equ <3.14>; 可以是任意数值和文本”。

  • TEXTEQU:类似于 EQU,创建了文本宏。它有三种格式:第一种为名称分配的是文本;第二种分配的是已有文本宏的内容;第三种分配的是整数常量表达式。例如,“continueMsg TEXTEQU <"Do you wish to continue (Y/N)?">”,“.data prompt1 BYTE continueMsg” 表示变量 prompt1 使用了文本宏 continueMsg。“rowSize =5 count TEXTEQU %(rowSize * 2) move TEXTEQU <mov> setupAL TEXTEQU <move al,count>”,语句 setupAL 就会被汇编为 “mov al,10”。用 TEXTEQU 定义的符号随时可以被重新定义。

5.2汇编编译器

NASM:INTEL 语法,跨 Linux 和 Windows 平台。

NASM(Netwide Assembler)是一个用于 x86 和 x86-64 架构的汇编语言编译器。它支持多种汇编语法,包括 Intel 语法,具有跨平台的特性,可以在 Linux 和 Windows 等操作系统上运行。其语法设计简洁易懂,与 Intel 语法相似但更简单。NASM 可输出多种目标文件格式,包括可执行文件、目标文件或库文件等。它还拥有强大的宏处理器,能方便地定义和使用宏,简化复杂的汇编代码编写过程。同时,NASM 具有广泛的文档和活跃的社区支持,开发者可以在文档和社区中找到大量的教程、示例代码和技术支持。

GNU as:默认 ATT 语法,可以通过伪指令声明使用 INTEL 语法。

GNU as 汇编器默认使用 AT&T 语法,这种语法与 Intel 汇编语法有一些不同之处。例如,在 AT&T 语法中,立即操作数前面要加 '$',寄存器操作数名前要加上百分号 '%',绝对跳转操作数前要加上 '*' 等。不过,GNU as 可以通过伪指令声明使用 INTEL 语法,例如使用.intel_syntax 和.intel_syntax noprefix 伪指令可以切换到 Intel 语法,注意寄存器前面的处理方式会有所不同。GNU as 是 Unix 系列操作系统默认使用的汇编器,它可以直接通过命令行选项生成指定位数的代码,如 --32/--64/--x32 选项。

5.3 x86汇编入门示例

以 “Hello world!” 为例,展示 NASM 和 GNU as 编译器的使用方法。

NASM 的使用方法

①首先创建一个简单的汇编程序,以下是一个输出 “Hello world!” 的示例:

section.data
    hello db "Hello world!",10
    hello_len equ $-hello

section.text
    global _start

_start:
    mov eax,4
    mov ebx,1
    mov ecx,hello
    mov edx,hello_len
    int 0x80
    mov eax,1
    xor ebx,ebx
    int 0x80

②保存为hello_nasm.asm文件,然后使用 NASM 进行编译。在命令行中执行以下命令:

nasm -f elf64 hello_nasm.asm -o hello_nasm.o

③这将生成一个目标文件hello_nasm.o。接着使用链接器将目标文件链接成可执行文件:

ld hello_nasm.o -o hello_nasm
现在就可以运行这个程序了,它会在控制台输出 “Hello world!”。
GNU as 的使用方法
以下是使用 GNU as 输出 “Hello world!” 的示例:
.section.data
    hello_str:.string "Hello world!"

.section.text
   .globl _start
_start:
    mov edi, hello_str
    call puts
    mov eax, 60
    xor edi, edi
    syscall

④保存为hello_gnu.asm文件,默认情况下 GNU as 使用 AT&T 语法,可以通过伪指令声明使用 INTEL 语法。如果要使用 INTEL 语法,可以在文件开头添加.intel_syntax noprefix。


http://www.kler.cn/a/542383.html

相关文章:

  • React 初级教程
  • yum报错 Could not resolve host: mirrorlist.centos.org
  • Cursor 编辑器详细介绍与使用
  • http常用状态码
  • UE5.5 PCGFrameWork--GPU CustomHLSL
  • Golang 并发机制-7:sync.Once实战应用指南
  • 深入理解Java对接DeepSeek
  • 如何在WPF中实现软件内嵌效果
  • Baklib使数字化内容管理更加高效和便捷设计理念和实用案例解析
  • Linux 系统使用教程
  • 使用epoll与sqlite3进行注册登录
  • 基于ESP32的远程开关灯控制(ESP32+舵机+Android+物联网云平台)——下
  • linux部署nacos集群
  • 【Elasticsearch】derivative聚合
  • CSS 属性选择器详解与实战示例
  • 认识Electron 开启新的探索世界一
  • 香港服务器远程超出最大连接数的解决方案是什么?
  • android 指定跳转页面返回触发
  • 数据分析:蛋白质组的GO term的富集分析详解
  • MySQL 使用create table as 与like 复制表
  • redis底层数据结构——整数集合
  • ubuntu20.04+ROS+Gazebo+px4+QGC+MAVROS
  • MacOS 15 无法打开Docker问题(Malware Blocked)解决
  • 【分布式理论9】分布式协同:分布式系统进程互斥与互斥算法
  • 【前端】几种常见的跨域解决方案代理的概念
  • 前端如何判断浏览器 AdBlock/AdBlock Plus(最新版)广告屏蔽插件已开启拦截