一文带你入门汇编语言

oYto · 2023-12-23 20:14:33 · 395245 次点击 · 大约8小时之前 开始浏览    置顶
这是一个创建于 2023-12-23 20:14:33 的主题,其中的信息可能已经有所发展或是发生改变。

更好的阅读体验请点击 YinKai 's Blog。 由于篇幅限制,完整内容请到博客内浏览。

简介

我为什么要学汇编

​ 因为想在寒假手写一下操作系统玩玩,所以提前学一学汇编,到时候放假就可以直接上手写了。

什么是汇编语言

​ 由于处理器只能理解机器语言指令,即 0 和 1 组成的字符串。然而,机器语言对软件开发来说过于晦涩和复杂。因此,低级汇编语言是为特定的处理器系列而设计的,通过符号代码和更易于理解的形式表示各种指令。

汇编语言的优点

​ 学习使用汇编语言可以让人深入了解计算机体现结构和底层硬件工作原理,提供对计算机内部操作的更详细的了解。

​ 我们可以收获:

  • 更加深入了解计算机的体系结构、寄存器、指令集和内存管理
  • 了解程序是如何在计算机上执行的
  • 有助于我们编写更高效的代码,可以直接控制底层硬件资源,使你能够优化代码以提高程序的性能
  • 提高我们调试代码的能力

PC 硬件的基本特征

​ PC 的主要硬件由处理器、存储器和寄存器组成。寄存器是保存数据和地址的处理器组件。为了执行程序,系统将其从外部设备复制到内部存储器中。处理器负责执行程序指令。

​ 处理器支持以下数据大小:

  • 单词:2 字节数据项
  • 双字:4 字节(32 位)数据项
  • 四字:8 字节(64 位)数据项
  • 段落:16 字节(128 位)区域
  • 千字节:1024 字节
  • 兆字节:1,048,576 字节

寻址内存中的数据

​ 处理器控制指令执行的过程称为——执行周期,它通常包含以下几个阶段:

  1. 取指令:处理器从内存中读取下一条指令的地址,并将指令加载到指令寄存器中
  2. 译码:处理器对取得的指令进行译码,确定指令的操作类型和操作数
  3. 执行:处理器执行指令的操作,可能涉及算术运算、逻辑运算、数据传输等
  4. 写回:将执行结果写回到寄存器或内存中,更新存储器或寄存器内容

​ 在计算机中,处理器在访问内存时以字节为单位进行操作。考虑一个十六进制数 0725H,它需要两个字节的内存来存储。其中,高位字节或最高有效字节为 07,低位字节为 25.

:::warning

​ 需要注意的是,处理器以相反的字节顺序存储数据,即低位字节存储在低内存地址中,高位字节存储在高内存地址中。因此,当处理器将值 0725H 从寄存器传输到内存时,它首先将 25 传输到较低的内存地址,然后将 07 传输到下一个内存地址。

Register-Memory.png

​ 当处理器从内存获取数字数据到寄存器时,它会再次反转字节。这个过程中,有两种内存地址形式:

  1. 绝对地址:这是具体位置的直接引用,表示数据存储在内存的特定地址。

    例如:如果 x 表示内存地址,则数据 0725H 存储在 x 地址和 x + 1 地址上,分别对应低位字节 25 和高位字节 07.

  2. 段地址(或偏移量):这是具有偏移值的内存段的起始地址。段地址与偏移量相结合,给出了实际的内存地址。

    例如:如果有一个段地址 y,那么 x 表示内存中的偏移地址。在这种情况下,数据 0725H 存储在地址 y:x 和 y:x+1 上,分别对应低位字节 25 和 高位字节 07.

:::

安装和环境设置

​ 在学习过程中,我们需要使用 NASM 汇编器,因为它免费、有据可查,并且可以在 Linux 和 Windows 上使用

​ 首先要验证是否已安装 NASM,可以使用下面的方法验证:

  1. 打开 Linux 终端
  2. 输入 whereis nasm 并按 Enter
  3. 如果已安装会出现类似 nasm: /usr/bin/nasm 的行,否则只能看到 nasm:

​ 如果没有安装,就需要安装 NASM。

​ 我的机器是 centos7 的虚拟机,我是直接在命令行中进行安装的,安装步骤如下:

  1. 打开终端并以 root 用户身份登录

  2. 运行以下命令更新 yum 软件包列表:

    yum update
    
  3. 运行以下命令安装 NASM:

    yum install nasm
    
  4. 过程中的询问,输入 y 并按 Enter 键继续

  5. 等待安装完成后,使用以下命令验证是否成功安装:

    nasm -v
    

基本语法

汇编程序可以分为三个部分:

  • 数据部分(data section)
  • 未初始化数据部分(bss section)
  • 文本部分(text section)

data 部分

​ 数据部分通常用于存储程序中需要初始化的数据。这可以包括常量、变量和其他静态数据。这个部分的数据在程序运行之前被初始化,并且在整个程序的执行过程中保持不变。

​ 声明数据部分的语法如下:

section .data

bss 部分

​ 未初始化数据部分用于存储程序中未初始化的全局和静态变量。与数据部分不同,bss 部分的变量在程序加载时不会被初始化,而是在运行时由系统初始化为零或空值。这样可以节省可执行文件的大小,因为在文件中只需要记录这些变量的名称和大小,而不需要存储它们的实际值。

​ 声明 bss 部分的语法如下:

section .bss

text 部分

​ 文本部分包含程序的实际代码。这是程序的主要执行部分,包括机器指令和指令的地址。在这个部分,汇编程序将源代码翻译成机器可执行的指令,使得计算机能够按照特定的算法执行相应的操作。

​ 声明 文本部分的语法如下:

section .text

注释

​ 汇编语言中的注释以分号;开头。注释可以独立一行存在,也可以与指令在同一行。例如:

; This is a line of comments
add eax, ebx ; adds ebx to eax

汇编语言语句

​ 汇编语言程序由三种类型的语句组成:

  • 可执行指令:告诉处理器要执行的操作,每条指令包括操作码和操作数
  • 汇编器指令或伪操作:用于影响汇编过程的方面,它们不会生成机器语言指令
  • 宏:一种文本替换机制

汇编语言语句的语法

​ 汇编语言语句每行输入一个语句,每个语句都遵循以下格式:

[label]    mnemonic    [operands]    [;comment]

​ 方括号中的字段是可选的。

​ 基本指令由两部分组成,第一部分是哟啊执行的指令名词(或助记符),第二部分是命令的操作数或参数。

​ 以下是一些典型汇编语言语句的示例:

  1. MOV指令(数据传送):

    MOV AX, 42       ; 将值42存储到寄存器AX中
    MOV BX, AX       ; 将寄存器AX的值传送到寄存器BX中
    
  2. ADD和SUB指令(加法和减法):

    ADD AX, BX       ; 将寄存器AX和BX中的值相加,并将结果存储在AX中
    SUB CX, 10       ; 从寄存器CX中减去值10,并将结果存储在CX中
    
  3. CMP和JMP指令(比较和跳转):

    CMP AX, BX       ; 比较寄存器AX和BX的值
    JE  label        ; 如果相等,则跳转到标签label处
    JG  another_label ; 如果大于,则跳转到另一个标签another_label处
    JL  target_label         ; 如果小于,跳转到目标标签
    
  4. INC和DEC指令(递增和递减):

    INC SI           ; 将寄存器SI中的值递增1
    DEC CX           ; 将寄存器CX中的值递减1
    
  5. LOOP指令(循环):

    MOV CX, 5        ; 设置循环计数器CX的初始值为5
    loop_start:      ; 循环开始标签
       ; 循环体代码
       DEC CX        ; 循环计数器递减1 
       JNZ loop_start; 如果计数器不为零,则跳转到循环开始标签
    

汇编中的 Hello World 程序

section .data
    msg db 'Hello, world!', 0xa  ; 要打印的字符串,0xa 是换行符
    len equ $ - msg     ; 字符串的长度

section .text
    global _start     ; 必须为链接器(ld)声明的全局入口点

_start:             ; 告诉链接器入口点
    ; write message to stdout
    mov eax, 4       ; 系统调用号(sys_write)
    mov ebx, 1       ; 文件描述符(标准输出)
    mov ecx, msg     ; 要写入的消息
    mov edx, len     ; 消息的长度
    int 0x80         ; 调用内核

    ; exit the program
    mov eax, 1       ; 系统调用号(sys_exit)
    xor ebx, ebx     ; 返回码为0
    int 0x80         ; 调用内核

上面的代码被编译并执行后,会输出如下内容:

Hello, world!

在 NASM 中编译和链接汇编程序

​ 为了能让上面的程序运行起来,我们需要按下面的步骤编译和链接上述程序:

  1. 使用文本编译器输入上述代码并将其保存为 hello.asm,后续的操作都在该目录下进行
  2. 输入 nasm -f elf hello.asm 编译汇编程序
    • -f elf:这是 NASM 的一个选项,用于指定生成的目标文件的格式。在这里,elf 表示目标文件将采用 ELF(Executable and Linkable Format)格式。
    • ELF 是一种通用的二进制文件格式,用于可执行文件、目标文件和共享库。
  3. 如果程序没有问题,就会程序名为 hello.o 的程序目标文件
  4. 输入 ld -m elf_i386 -s -o hello hello.o 命令,链接目标文件并创建名为 hello 的可执行文件
    • ld: 这是链接器的命令。链接器的作用是将多个目标文件链接在一起,解析符号引用,生成最终的可执行文件。在执行该命令时,链接器会将系统库和其他必要运行时库链接到目标文件 hello.o 中。我们的代码中由于程序只是在标准输出上打印一条消息,因此系统库中的一些 I/O 相关的函数可能被链接进来,以便程序能够正确地执行。
    • -m elf_i386: 这个选项告诉链接器使用 ELF (Executable and Linkable Format) 文件格式,并且生成 32 位 x86 架构的可执行文件。elf_i386 表示生成的可执行文件是面向 32 位 x86 架构的 ELF 文件。
    • -s: 这个选项用于剥离(strip)可执行文件中的符号表信息。符号表包含了程序中定义的各种符号(如变量、函数名等)的信息。在生产环境中,剥离符号表可以减小可执行文件的大小,但同时也会使得可执行文件不易调试。
    • -o hello: 这个选项指定生成的可执行文件的输出名称为 hello-o 是指定输出文件的选项,后面跟着输出文件的名称。
    • hello.o: 这是输入的目标文件,它是由 NASM 编译器生成的,包含了汇编代码的机器代码。
  5. 最后通过 ./hello 执行程序

内存段

​ 上面讨论的汇编程序的三个部分,也代码各种内存段。

​ 有趣的是,如果将 section 关键字替换为 segment,将会得到相同的结果,这是因为对于汇编器而言,这两个关键字在某些上下文中是可以互相使用的,这两个关键字都是为了告诉汇编器下面的代码是代码段。

内存段

​ 在分段内存模型中,系统内存被划分为不同的独立段组,每个段组由位于段寄存器中的指针引用。

​ 每个段用于包含特定类其型的数据。其中一个段用于包含指令代码,另一个段用于存储数据元素,第三个段用于保存程序堆栈。

​ 这种划分使得程序可以更灵活地管理内存,有选择地引用不同类型的数据和指令,从而更有效地执行各种计算任务。

​ 因此,我们可以将各种内存段指定为:

  • 数据段:.data 部分和 .bss 部分表示。 .data 部分用于声明内存区域,其中为程序存储数据元素,该部分在数据元素声明后无法扩展,并且在整个程序中保持静态。.bss 部分也是一个静态内存部分,其中包含稍后在程序中声明的数据的缓冲区。该缓冲区被零填充。
  • 代码段:它由 .text 部分表示。这定义了内存中存储指令代码的区域。这也是一个固定区域。
  • 堆栈:该段包含传递给程序内的函数和过程的数据值。

寄存器

​ 处理器操作主要涉及对数据的处理,而数据通常存储在内存中。然而,内存访问可能会降低处理器速度,因为它需要通过控制总线发送请求并进行复杂的内存访问。

​ 为了提高速度,处理器包含一些内部存储位置,称为寄存器。

处理器寄存器

​ IA-32架构中包含 10 个 32 位和 6 个 16 位的处理器寄存器,主要分为三类:

  1. 通用寄存器:通用寄存器用于存储临时数据,进行算术、逻辑运算等操作。
  2. 控制寄存器:控制寄存器用于控制和反映处理器的状态。
  3. 段寄存器:段寄存器用于存储各个段的起始地址,实现内存访问和管理。

​ 通用寄存器进一步可以分为:

  1. 数据寄存器
  2. 指针寄存器
  3. 索引寄存器

数据寄存器

​ 在IA-32架构中,有四个32位的数据寄存器,分别是EAX、EBX、ECX、EDX。这些寄存器可以按照不同的位数划分为更小的寄存器,具体如下:

  • 作为完整的32位数据寄存器:EAX、EBX、ECX、EDX。
  • 32 位寄存器的下半部分可用作四个 16 位数据寄存器:AX、BX、CX 和 DX。
  • 上述4个16位寄存器的下半部分和上半部分可以用作8个8位数据寄存器:AH、AL、BH、BL、CH、CL、DH和 DL 。

​ 一些数据寄存器在算术运算中具有特定用途:

  • AX: 主累加器,用于输入/输出和大多数算术指令。例如,在乘法运算中,根据操作数的大小将一个操作数存储在EAX或AX或AL寄存器中。
  • BX: 被称为基址寄存器,用于索引寻址。
  • CX: 被称为计数寄存器,与ECX一样,存储迭代操作中的循环计数。
  • DX: 数据寄存器,用于输入/输出操作,与AX寄存器和DX一起使用,用于涉及大值的乘法和除法运算。

指针寄存器

​ 指针寄存器是指 32 位的 EIP、ESP 和 EBP 寄存器以及相应的 16 位 右部分 IP、SP 和 BP。

​ 指针寄存器可以分为三类:

  1. 指令指针(IP):16 位 IP 寄存器存储下一条要执行的指令的偏移地址。 IP 与 CS 寄存器(代码段)(如CS : IP)关联,给出了代码段中当前指令的完整地址。
  2. 堆栈指针(SP): 16 位 SP 寄存器提供程序堆栈内的偏移值。 SP 与 SS 寄存器(堆栈段)(SS:SP)相关,指的是程序堆栈中数据或地址的当前位置。
  3. 基址指针(BP): 16 位 BP 寄存器主要帮助引用传递给子程序的参数变量。 SS 寄存器中的地址与 BP 中的偏移量相结合,得到参数的位置。 BP 还可以与 DI、SI(索引寄存器) 组合作为基址寄存器进行特殊寻址。

索引寄存器

​ 索引寄存器包括32位的 ESI 和 EDI 以及它们的 16 位最右边的部分。SI 和 DI 通常用于索引寻址,并有时用于执行加法和减法操作。这两个索引指针分别是:

  1. 来源索引 (SI): 用作字符串操作的源索引。在字符串处理中,SI通常用于指向源字符串的当前位置。
  2. 目的地索引 (DI): 用作字符串操作的目标索引。DI通常用于指向目标字符串的当前位置,特别是在字符串复制等操作中。

控制寄存器

​ 控制寄存器包括 32 位指令指针寄存器和 32 位标志寄存器,用于管理程序的执行流程和状态。其中的常见标志位有:

  • 溢出标志 (OF): 表示有符号算术运算后数据的高位(最左位)是否溢出。

  • 方向标志 (DF): 确定移动或比较字符串数据的左或右方向。DF为0时,字符串操作从左到右;DF为1时,字符串操作从右到左。

  • 中断标志 (IF): 决定是否忽略或处理外部中断,如键盘输入。IF为0时禁用外部中断,为1时启用中断。

  • 陷阱标志 (TF): 允许将处理器设置为单步模式,以便一次执行一条指令,常用于调试。

  • 符号标志 (SF): 显示算术运算结果的符号,由最左边位的高位表示。SF为0表示正结果,为1表示负结果。

  • 零标志 (ZF): 表示算术或比较运算的结果是否为零。ZF为1表示零结果,为0表示非零结果。

  • 辅助进位标志 (AF): 包含算术运算后从位 3 到位 4 的进位,用于特殊的算术操作。

  • 奇偶校验标志 (PF): 表示算术运算结果中1位的总数,用于奇偶校验。PF为1表示奇数个1位,为0表示偶数个1位。

  • 进位标志 (CF): 包含算术运算后从高位(最左边)的进位,也存储shift或rotate操作的最后一位内容。

段寄存器

​ 段在计算机内存中是为了组织和管理存储空间而引入的概念。在汇编编程中,处理器通过段寄存器来访问内存位置。以下是关于段的主要信息:

  1. 代码段(CS):
    • 包含要执行的指令的区域。
    • 由 16 位代码段寄存器(CS 寄存器)存储代码段的起始地址。
  2. 数据段(DS):
    • 包含数据、常量和工作区的区域。
    • 由 16 位数据段寄存器(DS 寄存器)存储数据段的起始地址。
  3. 堆栈段(SS):
    • 包含过程或子例程的数据和返回地址,实现为堆栈数据结构。
    • 由16位堆栈段寄存器(SS 寄存器)存储堆栈的起始地址。
  4. 其他段寄存器:
    • 额外段(ES): 提供额外的段来存储数据。
    • FS 和 GS: 提供额外的段用于特定目的。

​ 在汇编编程中,程序需要访问内存位置。段内的所有内存位置都相对于段的起始地址。段从可被 16 整除的地址开始,因此所有这类内存地址中最右边的十六进制数字通常是 0。为了引用段中的任何内存位置,处理器将段寄存器中的段地址与该位置的偏移值组合起来。

示例

​ 下面的程序会在代码中输出 9 个连续的星号。

section    .text
   global _start     ;必须为链接器声明(gcc)

_start:             ;告诉链接器入口点
   mov    edx,len  ;消息长度
   mov    ecx,msg  ;要写入的消息
   mov    ebx,1    ;文件描述符(stdout)
   mov    eax,4    ;系统调用号(sys_write)
   int    0x80     ;调用内核

   mov    edx,9    ;消息长度
   mov    ecx,s2   ;要写入的消息
   mov    ebx,1    ;文件描述符(stdout)
   mov    eax,4    ;系统调用号(sys_write)
   int    0x80     ;调用内核

   mov    eax,1    ;系统调用号(sys_exit)
   int    0x80     ;调用内核

section    .data
msg db 'Displaying 9 stars',0xa ;一条消息
len equ $ - msg  ;消息的长度
s2 times 9 db '*' ;9个星号

​ 我们使用以下命令进行编译和执行:

nasm -f elf nine_stars.asm 
ld -m elf_i386 -s -o nine_stars nine_stars.o

​ 输出结果如下:

Displaying 9 stars
*********

系统调用

​ 系统调用是用户空间和内核空间之间接口的 API。我们之前已经使用过了 sys_write 和 sys_exit 这两个系统调用,分别用于写入屏幕和退出程序。

Linux 系统调用

​ 我们在汇编程序中使用系统调用,需要按照如下步骤:

  1. 将系统调用号放入 EAX 寄存器中;
  2. 将系统调用的参数存储在 EBX、ECX 等寄存器中
  3. 调用相关中断(0x80),然后执行 EAX 中的系统调用号对应的程序
  4. 结果通常返回 EAX 寄存器中

​ 可以存储系统调用参数的存储器有 基址寄存器 EBX、计数寄存器 ECX、数据寄存器 EDX、源索引寄存器 ESI、目标索引寄存器 EDI、基址指针寄存器 EBP。

​ 下面给大家演示一下几个示例:

​ (1)使用 sys_exit:

mov eax, 1 ; 系统调用号 sys_exit
int 0x80   ; 调用内核

​ (2)使用 sys_write:

mov edx, 4        ; 消息长度
mov ecx, msg    ; 要写入的消息
mov ebx, 1        ; 文件描述符
mov eax, 4        ; 系统调用号
int 0x80        ; 调用内核

常见系统调用

%eax Name %ebx %ecx %edx %esx %edi
1 sys_exit int - - - -
2 sys_fork struct pt_regs - - - -
3 sys_read unsigned int char * size_t - -
4 sys_write unsigned int const char * size_t - -
5 sys_open const char * int int - -
6 sys_close unsigned int - - - -

示例

​ 下面举一个复杂一点的例子,包含了之前我们讲过的 data、bss、text 三个部分,也希望通过这个例子,加深一下大家对 data 部分和 bss 部分的区别

section .data                           ; 数据段
   userMsg db '请输入一个数字: '        ; 提示用户输入数字的消息
   lenUserMsg equ $-userMsg             ; 消息的长度
   dispMsg db '您输入的是: '
   lenDispMsg equ $-dispMsg                 

section .bss           ; 未初始化的数据
   num resb 5            ; 用于存储用户输入的变量,5字节

section .text          ; 代码段
   global _start        ; 声明程序入口点

_start:                ; 程序入口
   ; 输出提示消息 '请输入一个数字: '
   mov eax, 4
   mov ebx, 1
   mov ecx, userMsg
   mov edx, lenUserMsg
   int 80h
   ; 读取并存储用户输入
   mov eax, 3
   mov ebx, 2
   mov ecx, num  
   mov edx, 5          ; 读取5字节的信息(数字和符号,1字节)
   int 80h    
   ; 输出消息 '您输入的是: '
   mov eax, 4
   mov ebx, 1
   mov ecx, dispMsg
   mov edx, lenDispMsg
   int 80h  
   ; 输出用户输入的数字
   mov eax, 4
   mov ebx, 1
   mov ecx, num
   mov edx, 5
   int 80h  
   ; 退出程序
   mov eax, 1
   mov ebx, 0
   int 80h

​ 同样,我们需要通过下述命令来编译运行:

nasm -f elf get_num.asm                 # 将汇编程序编译成机器码
ld -m elf_i386 -s -o get_num get_num.o    # 将目标文件和其他必要的文件组合成可执行文件
./get_num                                 # 运行可执行文件

​ 输出结果如下:

请输入一个数字: 
123
您输入的是: 123

寻址模式

​ 下面来介绍一个汇编语言中三种基本寻址方式:

  • 寄存器寻址
  • 立即寻址
  • 内存寻址

寄存器寻址

​ 寄存器寻址模式,其中操作数直接存储在寄存器中,而不涉及内存。这种寻址模式在处理数据时提供了高效的速度,因为它是直接从寄存器中读取或向寄存器中写入数据,而无需涉及到主存储器。

​ 在此模式下,根据指令的不同,寄存器可能是第一个操作数,也有可能是第二个操作数,或者两个操作数都是,如下:

MOV DX, TAX_RATE
MOV COUNT, CX
MOV EAX, EBX

立即寻址

​ 立即寻址模式,其中一个操作数是常量或者表达式,而不是从内存中获取的。

​ 我们可以通过这种方式定义变量、更改变量值、赋值等操作,例如:

BYTE_VALUE DB 150
ADD BYTE_VALUE, 65
MOV AX, 45H

直接内存寻址

​ 直接内存寻址用于操作内存中的数据。在该模式下,偏移值直接指定为指令的一部分,通常由变量名指示。这种寻址方式涉及两个操作:定位内存位置和执行操作。

​ 举例如下:

ADD BYTE_VALUE DL    ; 将寄存器 DL 中的值加到内存位置 BYTE_VALUE 的字节值上
MOV BX, WORD_VALUE    ; 将内存中的操作数直接赋值给 BX 寄存器

​ 上述两种情况下,汇编器会维护一个符号表,其中存储了程序中所使用的所有变量的偏移值,这些偏移值用于在运行时计算实际的内存地址。这种方式使用了一种简单而直接的方法来引用内存中的数据,但相对寄存器寻址或间接寻址来说,它可能导致访问效率稍低。

直接偏移寻址

​ 直接偏移寻址是一种在汇编语言中用于访问数据表的寻址模式。通过使用算术运算符,你直接可以直接计算或指定相对于数据表起始地址的偏移量,从而访问表格中的特定数据。

​ 我们先定义以下数据表,以供我们后续的操作:

BYTE_TABLE DB 14, 15, 22, 23

​ 然后我们可以通过索引和偏移量的方式操作数据表中的数据:

MOV CL, BYTE_TABLE[2]    ; 元素索引方式
MOV CL, BYTE_TABLE + 2    ; 偏移量方式

间接内存寻址

​ 间接内存寻址是一种利用计算机的段:偏移寻址能力的寻址模式。通常,基址寄存器(例如 EBX、EBP,或简写为 BX、BP)和索引寄存器(DI、SI)被包含在方括号内,用于存储器引用,从而实现对内存中数据的访问。这种寻址模式通常用于处理包含多个元素的变量,比如数组。在数组的情况下,数组的起始地址通常存储在基址寄存器中。

​ 通过下面的代码,演示一下如何通过间接内存寻址访问数组的不同元素:

MY_TABLE TIMES 10 DW 0    ; 分配了10个字,每个字2字节并初始化为0
MOV EBX, [MY_TABLE]        ; 将 MY_TABLE 的有效地址存储到 EBX 寄存器中
MOV [EBX], 110            ; 将值 110 存储到 MY_TABLE 的第一个有效地址
ADD EBX, 2                ; EBX = EBX + 2
MOV [EBX], 123            ; 将值 123 存储到 MY_TABLE 的第二个元素

:::warning

​ 用 [] 和 不用 [] 的区别?

​ 对于 MOV [EBX], 110MOV EBX, 110 来说:

  • MOV [EBX], 110:是一条间接寻址指令,它将立即数 110 存储到 EBX 寄存器中存储的内存地址指向的位置。
  • MOV EBX, 110:这是一条直接寻址指令,它将立即数 100 直接加载到 EBX 寄存器中,此时 EBX 中存储的是一个数,而不是内存地址。

:::

MOV 指令

​ MOV 指令是 x86 汇编语言中用于将数据从一个存储空间移动到另一个存储空间的指令,它通常需要两个操作数,语法如下:

MOV destination, source

​ MOV 指令可能有以下五种形式,register(寄存器)、immediate(立即数)、memory(内存):

MOV register, register
MOV register, immediate
MOV memory, immediate
MOV register, memory
MOV memory, register

:::warning

​ 需要注意的是:

  1. 两个操作数的大小必须同
  2. 源操作数的值是不变的

​ 我们上面写的代码其实是存在一些问题的:

MY_TABLE TIMES 10 DW 0    ; 分配了10个字,每个字2字节并初始化为0
MOV EBX, [MY_TABLE]        ; 将 MY_TABLE 的有效地址存储到 EBX 寄存器中
MOV [EBX], 110            ; 将值 110 存储到 MY_TABLE 的第一个有效地址

​ 由于 x86 架构中内存访问是按字节寻址的,MOV [EBX], 110 这条指令可能会被解释为存储到 MY_TABLE 的第一个字节;但可能程序员的目的是存储一个整数值 110 到 MY_TABLE,并且 MY_TABLE 中的每个元素是字,那就会有歧义。

​ 因此,我们需要使用类型说明符来明确指令操作的数据类型和占用的字节数,于是可以像下面这样写:

MOV [EBX], WORD 110     ; 将一个字(两个字节)的值 110 存储到 MY_TABLE[0] 中

​ 常见的类型说明符如下:

类型说明符 寻址字节数
BYTE 1
WORD 2
DWORD 4
QWORD 8
TBYTE 10

:::

变量

​ 在汇编语言中,变量的定义和数据的存储通常涉及到不同的指令和规则。

​ NASM 提供了不同的 define 指令,用于为变量分配存储空间。这些指令用于在数据段中保留和初始化一个或多个字节,常见的有:

指令 用途 存储空间
DB 定义 Byte 分配1个字节
DW 定义 Word 分配2个字节
DD 定义 Doubleword 分配4个字节
DQ 定义 Quadword 分配8个字节
DT 定义十个字节 分配10个字节

​ 使用示例如下:

choice      DB 'y'
number      DW 12345
neg_number  DW -12345
big_number  DQ 123456789
real_number1 DD 1.234
real_number2 DQ 123.456

:::warning

  1. 字符的每个字节都以其十六进制的 ASCII 值存储:

    • 每个字符都有一个对应的 ASCII 值,它是一个唯一的数值表示。例如,字母 'A' 的 ASCII 值是 65(十六进制为 41),而字母 'B' 的 ASCII 值是 66(十六进制为 42)。

    • 当你在程序中定义一个字符变量,它的每个字节将被存储为对应字符的 ASCII 值的十六进制表示。

  2. 每个十进制值都会自动转换为其 16 位二进制等效值并存储为十六进制数:

    • 当你在程序中定义一个十进制值,汇编器会将其自动转换为其 16 位的二进制等效值,并以十六进制形式存储。

    • 例如,十进制值 10 会被转换为二进制值 1010,然后以十六进制形式存储为 "A"。

  3. 处理器使用小尾数字节排序:

    • 处理器采用小尾数(Little Endian)字节排序,这意味着较低有效字节(最低位字节)存储在内存中的较低地址处,而较高有效字节(最高位字节)存储在内存中的较高地址处。

    • 例如,对于十六进制值 0x12345678,在内存中的存储顺序是:78 56 34 12。

  4. 负数将转换为其 2 的补码表示形式:

    • 在计算机中,负数通常以 2 的补码形式表示。这种表示方式使得在计算中可以统一处理加法和减法,而不需要额外的逻辑。

    • 2 的补码表示形式是通过将正数的二进制表示取反然后加 1 得到的。例如,-5 的二进制表示是将 5 的二进制表示(0000 0101)取反得到(1111 1010),然后加 1 得到(1111 1011)。

  5. 短浮点数和长浮点数分别使用 32 位或 64 位表示:

    • 浮点数表示采用 IEEE 754 标准,其中短浮点数(float)通常使用 32 位表示,而长浮点数(double)通常使用 64 位表示。

    • 32 位浮点数包括符号位、8 位指数和 23 位尾数。64 位浮点数包括符号位、11 位指数和 52 位尾数。

:::

​ 下面程序演示了 define 指令的使用:

section .text
        global _start

section .data
        choice db 'y', 0xa
        len equ $ - choice

_start:
        mov edx, len
        mov ecx, choice
        mov ebx, 1
        mov eax, 4
        int 80h

        mov eax, 1
        int 80h

​ 编译运行后输出如下:

y

为未初始化数据分配空间

​ 在程序中,有时我们需要为一些数据保留一些存储空间,但不需要为它们初始化,而是在后续程序运行过程中被赋予实际值。

​ 这个时候,我们就需要用到汇编语言中的保留指令,这些指令用于在内存中分配指定大小的空间,但不对其中的数据进行初始化。

​ 常见的保留指令有:

指令 用途
RESB 保留一个 Byte(一个字节)
RESW 保留一个 Word(两个字节)
RESD 保留一个 Doubleword(四个字节)
RESQ 保留一个 Quadword(八个字节)
REST 保留十个字节空间(十个字节)

多重定义

​ 一个程序可以定义多个数据定义的语句,例如:

choice DB 'y'
number DW 123
bigbumber DQ 123456789

​ 这样定义的语句,编辑器会为这些变量分配连续的内存。

多次初始化

​ times 指令允许对同一值进行多次初始化,这样在定义数组和表示非常有用。可以使用如下语句,创建名为 stars、数据元素类型为DW 的数组,包含 9 个元素,每个初始化为 0.

stars TIMES 9 DW 0    ;

​ 我们重温一下上面输出 9 个 * 的汇编程序:

section .text
        global _start

section .data
        stars times 9 db '*'

_start:
        mov edx, 9
        mov ecx, stars
        mov ebx, 1
        mov eax, 4
        int 0x80

        mov eax, 1
        int 0x80

​ 同样编译运行后,会输出:

*********

有疑问加站长微信联系(非本文作者)

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

395245 次点击  ∙  1 赞  
加入收藏 微博
1 回复  |  直到 2024-01-15 15:16:14
starlion
starlion · #1 · 大约1年之前

你的网站在chrome 版本 120.0.6099.217 下,显示空白, :laughing:

添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传