【随笔】汇编(寄存器、内存模型、常用指令、语法)

文章目录

一、简介

汇编语言(英语:assembly language)是任何一种用于电子计算机、微处理器、微控制器,或其他可编程器件的低级语言 。在不同的设备中,汇编语言对应着不同的机器语言指令集。一种汇编语言专用于某种计算机系统结构,而不像许多高级语言,可以在不同系统平台之间移植。

使用汇编语言编写的源代码,然后通过相应的汇编程序将它们转换成可执行的机器代码。这一过程被称为汇编过程

汇编语言使用助记符Mnemonics)来代替和表示特定低级机器语言的操作 。特定的汇编目标指令集可能会包括特定的操作数。许多汇编程序可以识别代表地址和常量的标签(Label)和符号(Symbols),这样就可以用字符来代表操作数而无需采取写死的方式。普遍地说,每一种特定的汇编语言和其特定的机器语言指令集是一一对应的。

许多汇编程序为程序开发、汇编控制、辅助调试提供了额外的支持机制。有的汇编语言编写工具经常会提供宏,它们也被称为宏汇编器。

现在汇编语言已不像其他大多数的程序设计语言一样被广泛用于程序设计,在今天的实际应用中,它通常被应用在底层硬件操作和高要求的程序优化的场合。驱动程序、嵌入式操作系统和实时运行程序中都会需要汇编语言

最早的时候,编写程序就是手写二进制指令,然后通过各种开关输入计算机,比如要做加法了,就按一下加法开关。后来,发明了纸带打孔机,通过在纸带上打孔,将二进制指令自动输入计算机。

为了解决二进制指令的可读性问题,工程师将那些指令写成了八进制。二进制转八进制是轻而易举的,但是八进制的可读性也不行。很自然地,最后还是用文字表达,加法指令写成 ADD。内存地址也不再直接引用,而是用标签表示。

这样的话,就多出一个步骤,要把这些文字指令翻译成二进制,这个步骤就称为 assembling,完成这个步骤的程序就叫做 assembler。它处理的文本,自然就叫做 aseembly code。标准化以后,称为 assembly language,缩写为 asm,中文译为汇编语言。

每一种 CPU 的机器指令都是不一样的,因此对应的汇编语言也不一样。本文介绍的是目前最常见的 x86 汇编语言。

  1. x86/x64 汇编:这是应用最广泛的汇编语言类型,用于 Intel 和 AMD 等 x86 架构的处理器,包括 32 位 (x86) 和 64 位 (x64)。这种汇编语言在 PC、服务器和许多嵌入式系统中被广泛使用。
  2. ARM 汇编:ARM 架构是另一种常见的处理器架构,广泛应用于移动设备、嵌入式系统以及一些服务器。ARM 汇编用于编写针对 ARM 处理器的低级代码。
  3. MIPS 汇编:MIPS 是一种 RISC 架构(精简指令集计算机),在一些嵌入式系统和早期的个人计算机中使用。MIPS 汇编用于编写针对 MIPS 处理器的低级代码。
  4. 68k 汇编:68k 汇编用于 Motorola 68k 系列处理器,这些处理器在历史上曾经被广泛应用于个人电脑、工作站和嵌入式系统中。
  5. AVR 汇编:AVR 是一种常见的低功耗、高性能的 8 位和 32 位微控制器架构,用于许多嵌入式系统和物联网设备。AVR 汇编用于编写针对 AVR 微控制器的低级代码。
  6. PowerPC 汇编:PowerPC 是 IBM、Motorola 和苹果公司合作开发的一种处理器架构,曾被广泛应用于苹果电脑和一些服务器中。PowerPC 汇编用于编写针对 PowerPC
    处理器的低级代码。

二、寄存器

寄存器(Register)是CPU 内用来暂存指令、数据和地址的 电脑存储器。寄存器的存贮容量有限,读写速度非常快。在计算机体系结构里,寄存器存储在已知时间点所作计算的中间结果,通过快速地访问数据来加速计算机程序的执行。

寄存器位于存储器层次结构的最顶端,也是CPU可以读写的最快的存储器,事实上所谓的暂存已经不像存储器,而是非常短暂的读写少量信息并马上用到,因为通常程序执行的步骤中,这期间就会一直使用它。寄存器通常都是以他们可以保存的比特数量来计量,举例来说,一个8位寄存器或32位寄存器。在中央处理器中,包含寄存器的部件有指令寄存器(IR)、程序计数器和累加器。寄存器现在都以寄存器数组的方式来实现,但是他们也可能使用单独的触发器、高速的核心存储器、薄膜存储器以及在数种机器上的其他方式来实现出来。

寄存器也可以指代由一个指令之输出或输入可以直接索引到的寄存器组群,这些寄存器的更确切的名称为"架构寄存器"。例如,x86指令集定义八个32位寄存器的集合,但一个实现x86指令集的CPU内部可能会有八个以上的寄存器。

32位 CPU、64位 CPU 这样的名称,其实指的就是寄存器的大小。32 位 CPU 的寄存器大小就是4个字节。

x86架构CPU中常见的寄存器,包括通用寄存器、段寄存器、标志寄存器和指令指针寄存器。这些寄存器在不同的x86处理器中可能会有所不同,但是以下是通用的:

寄存器 描述
EAX 累加器 (Accumulator)
EBX 基址寄存器 (Base)
ECX 计数寄存器 (Counter)
EDX 数据寄存器 (Data)
ESI 源索引寄存器 (Source Index)
EDI 目的索引寄存器 (Destination Index)
ESP 栈指针寄存器 (Stack Pointer)
EBP 基址指针寄存器 (Base Pointer)
EIP 指令指针寄存器 (Instruction Pointer)
CS 代码段寄存器 (Code Segment)
DS 数据段寄存器 (Data Segment)
SS 栈段寄存器 (Stack Segment)
ES 附加段寄存器 (Extra Segment)
FS 附加段寄存器 (Extra Segment)
GS 附加段寄存器 (Extra Segment)
EFLAGS 标志寄存器 (Flags)

这些寄存器的前缀"E"表示32位寄存器,若是64位寄存器,则为"R"。例如,64位下的累加器寄存器为RAX。(16位则没有前缀)

三、内存模型

3.1 Heap

寄存器只能存放很少量的数据,大多数时候,CPU 要指挥寄存器,直接跟内存交换数据。所以,除了寄存器,还必须了解内存怎么储存数据。

程序运行的时候,操作系统会给它分配一段内存,用来储存程序和运行产生的数据。这段内存有起始地址和结束地址,比如从0x1000到0x8000,起始地址是较小的那个地址,结束地址是较大的那个地址。

程序运行过程中,对于动态的内存占用请求 (比如新建对象,或者使用malloc等命令),系统就会从预先分配好的那段内存之中,划出一部分给用户,具体规则是从起始地址开始划分 (实际上,起始地址会有一段静态数据,这里忽略)。举例来说,用户要求得到10个字节内存,那么从起始地址0x1000开始给他分配,一直分配到地址0x100A,如果再要求得到22个字节,那么就分配到0x1020

这种因为用户主动请求而划分出来的内存区域,叫做 Heap(堆)。它由起始地址开始,从低位(地址)向高位(地址)增长。Heap 的一个重要特点就是不会自动消失,必须手动释放,或者由垃圾回收机制来回收。

"堆"(Heap)也可以指代一种数据结构,与内存中的堆不同。在数据结构中,堆通常指的是一种特殊的树形结构,用于实现优先队列。

3.2 Stack

除了 Heap 以外,其他的内存占用叫做 Stack(栈)。简单说,Stack 是由于函数运行而临时占用的内存区域。

c 复制代码
int main() {
   int a = 2;
   int b = 3;
}

上面代码中,系统开始执行main函数时,会为它在内存里面建立一个frame),所有main的内部变量(比如a和b)都保存在这个帧里面。main函数执行结束后,该帧就会被回收,释放所有的内部变量,不再占用空间。

c 复制代码
int main() {
   int a = 2;
   int b = 3;
   return add_ab(a, b);
}

上面代码中,main函数内部调用了add_ab函数。执行到这一行的时候,系统也会为add_ab新建一个帧,用来储存它的内部变量。也就是说,此时同时存在两个帧:main和add_ab。一般来说,调用栈有多少层,就有多少帧。

等到add_ab运行结束,它的帧就会被回收,系统会回到函数main刚才中断执行的地方,继续往下执行。通过这种机制,就实现了函数的层层调用,并且每一层都能使用自己的本地变量。

所有的帧都存放在 Stack,由于帧是一层层叠加的,所以 Stack 叫做栈。生成新的帧,叫做"入栈"(push);栈的回收叫做"出栈(pop)。Stack 的特点就是,最晚入栈的帧最早出栈(因为最内层的函数调用,最先结束运行),这就叫做"后进先出(LIFO)"的数据结构。每一次函数执行结束,就自动释放一个帧,所有函数执行结束,整个 Stack 就都释放了。

Stack 是由内存区域的结束地址开始,从高位(地址)向低位(地址)分配。 比如,内存区域的结束地址是0x8000,第一帧假定是16字节,那么下一次分配的地址就会从0x7FF0开始;第二帧假定需要64字节,那么地址就会移动到0x7FB0。

四、指令

4.1 示例

C语言:

转成汇编:

c 复制代码
gcc -S temp.c

汇编代码:一个高级语言的简单操作,底层可能由几个,甚至几十个 CPU 指令构成。CPU 依次执行这些指令,完成这一步操作。(注释是我加的)

bash 复制代码
        .file   "temp.c"             ; 源文件名称
        .text                        ; 开始文本段
        .globl  add_ab               ; 声明 add_ab 是一个全局函数
        .type   add_ab, @function    ; 指定 add_ab 是一个函数类型
add_ab:                             ; 函数 add_ab 的开始
.LFB0:
        .cfi_startproc
        endbr64                     ; 提高对分支目标地址(BTA)的保护
        pushq   %rbp                ; 保存旧的栈基址指针
        .cfi_def_cfa_offset 16      ; 指定栈上的数据偏移量
        .cfi_offset 6, -16          ; 指定寄存器 %rbp 的偏移
        movq    %rsp, %rbp          ; 设置 %rbp 指向当前栈顶,建立新的栈帧
        .cfi_def_cfa_register 6     ; 指定 %rbp 为当前帧指针
        movl    %edi, -4(%rbp)      ; 将函数参数 %edi(第一个参数)存储到栈上的位置
        movl    %esi, -8(%rbp)      ; 将函数参数 %esi(第二个参数)存储到栈上的位置
        movl    -4(%rbp), %edx      ; 从栈中加载第一个参数 %edx
        movl    -8(%rbp), %eax      ; 从栈中加载第二个参数 %eax
        addl    %edx, %eax          ; 将 %edx 和 %eax 相加
        popq    %rbp                ; 恢复之前的栈基址指针
        .cfi_def_cfa 7, 8           ; 指定栈顶为 %rsp,栈基址为 %rbp
        ret                         ; 函数返回
        .cfi_endproc
.LFE0:
        .size   add_ab, .-add_ab    ; 指定函数 add_ab 的大小
        .section        .rodata      ; 开始只读数据段
.LC0:
        .string "x+y=%d"             ; 定义一个字符串常量,用于 printf 函数
        .text                        ; 返回文本段
        .globl  main                 ; 声明 main 函数为全局函数
        .type   main, @function      ; 指定 main 是一个函数类型
main:                               ; main 函数的开始
.LFB1:
        .cfi_startproc
        endbr64                     ; 提高对分支目标地址(BTA)的保护
        pushq   %rbp                ; 保存旧的栈基址指针
        .cfi_def_cfa_offset 16      ; 指定栈上的数据偏移量
        .cfi_offset 6, -16          ; 指定寄存器 %rbp 的偏移
        movq    %rsp, %rbp          ; 设置 %rbp 指向当前栈顶,建立新的栈帧
        .cfi_def_cfa_register 6     ; 指定 %rbp 为当前帧指针
        subq    $16, %rsp           ; 为局部变量分配空间
        movl    $2, -8(%rbp)        ; 将常量 2 存储到栈上的位置(第一个局部变量)
        movl    $3, -4(%rbp)        ; 将常量 3 存储到栈上的位置(第二个局部变量)
        movl    -4(%rbp), %edx      ; 从栈中加载第一个局部变量 %edx
        movl    -8(%rbp), %eax      ; 从栈中加载第二个局部变量 %eax
        movl    %edx, %esi          ; 复制 %edx 到 %esi
        movl    %eax, %edi          ; 复制 %eax 到 %edi
        call    add_ab              ; 调用 add_ab 函数
        movl    %eax, %esi          ; 将返回值(和)存储到 %esi 中
        leaq    .LC0(%rip), %rdi    ; 将字符串地址存储到 %rdi 中
        movl    $0, %eax            ; 将 0 存储到 %eax
        call    printf@PLT          ; 调用 printf 函数
        movl    $0, %eax            ; 将 0 存储到 %eax,表示返回值为 0
        leave                       ; 恢复栈帧
        .cfi_def_cfa 7, 8           ; 指定栈顶为 %rsp,栈基址为 %rbp
        ret                         ; 函数返回
        .cfi_endproc
.LFE1:
        .size   main, .-main        ; 指定函数 main 的大小
        .ident  "GCC: (Ubuntu 9.4.0-1ubuntu1~20.04.2) 9.4.0" ; 编译器的版本信息
        .section        .note.GNU-stack,"",@progbits    ; 用于指示栈是否是可执行的
        .section        .note.gnu.property,"a"           ; GNU 属性节
        .align 8
        .long    1f - 0f
        .long    4f - 1f
        .long    5
0:
        .string  "GNU"
1:
        .align 8
        .long    0xc0000002
        .long    3f - 2f
2:
        .long    0x3
3:
        .align 8
4:

4.2 语法

汇编语言的语法可以根据不同的体系结构有所不同,以下是 x86 架构汇编语言的一般语法:

指令格式:

c 复制代码
[label:]   mnemonic   [operands]   [;comment]
  • label:可选的,用于标识代码的位置,以冒号结尾。
  • mnemonic:指令助记符,代表要执行的操作。
  • operands:操作数,指定指令要操作的数据。
  • comment:注释,用于解释指令的作用或其他信息。

基本元素:

  • 指令(Instructions) :表示要执行的操作,如 mov, add, sub, jmp 等。
  • 寄存器(Registers) :用于存储数据和执行操作,如 eax, ebx, ecx 等。
  • 内存操作数(Memory Operands) :表示内存中的数据,如 [address][ebx] 等。
  • 立即数(Immediate Values) :常数值,直接指定在指令中,如 5, 0xFF, 0b1010 等。
  • 标签(Labels):用于标识代码的位置,通常在循环或条件跳转中使用。

指令示例:

c 复制代码
; 注释示例
section .text             ; 代码段开始

    global _start         ; 全局声明,程序入口

_start:
    mov eax, 5            ; 将立即数 5 存入 eax 寄存器
    mov ebx, 3            ; 将立即数 3 存入 ebx 寄存器
    add eax, ebx          ; eax = eax + ebx
    sub eax, 1            ; eax = eax - 1

    cmp eax, 10           ; 比较 eax 和立即数 10
    jl  less_than_ten     ; 如果 eax < 10,则跳转到 less_than_ten 标签

    mov eax, 10           ; 如果不满足条件,将立即数 10 存入 eax

less_than_ten:
    mov ecx, eax          ; 将 eax 的值存入 ecx

    ; 调用系统调用 exit
    mov eax, 1            ; 系统调用号 1 表示 exit
    xor ebx, ebx          ; 退出码为 0
    int 0x80              ; 调用系统调用

section .data             ; 数据段开始
    my_var db 'A'         ; 定义一个名为 my_var 的字节数据,值为字符 'A'

汇编语言对空白字符不敏感,但大小写敏感。同时,注释以分号 ; 开始,可以在语句后添加注释。

4.3常用指令

常用的汇编指令以及简要说明和示例:

指令 说明 示例
mov 将数据从一个位置复制到另一个位置 mov eax, ebx
add 加法操作 add eax, ebx
sub 减法操作 sub eax, ebx
mul 无符号乘法 mul ebx
div 无符号除法 div ebx
and 按位与操作 and eax, ebx
or 按位或操作 or eax, ebx
xor 按位异或操作 xor eax, ebx
not 按位取反操作 not eax
shl 逻辑左移 shl eax, 1
shr 逻辑右移 shr eax, 1
cmp 比较两个操作数的值,设置标志位 cmp eax, ebx
test 测试两个操作数的位并设置标志位 test eax, ebx
jmp 无条件跳转 jmp label
je 等于时跳转 je label
jne 不等于时跳转 jne label
jl 小于时跳转 jl label
jg 大于时跳转 jg label
jle 小于等于时跳转 jle label
jge 大于等于时跳转 jge label
call 调用函数或过程 call function_name
ret 从函数返回 ret
push 将数据压入栈 push eax
pop 从栈中弹出数据 pop eax
nop 空操作 nop
int 产生软中断 int 0x80
hlt 暂停处理器执行 hlt

示例:

假设有以下寄存器的初始值:

  • eax = 5
  • ebx = 3

示例汇编指令和注释如下:

c 复制代码
; 将 ebx 加到 eax 中
add eax, ebx    ; eax = 5 + 3 = 8

; 乘以 ebx 的值
mul ebx         ; eax = 8 * 3 = 24

; 将结果保存到变量 result 中
mov [result], eax ; result = 24

; 比较 eax 和 ebx 的值
cmp eax, ebx    ; 设置标志位,用于后续条件跳转

; 如果 eax 大于 ebx,跳转到 label1
jg label1       ; 如果 24 > 3,则跳转

; 否则跳转到 label2
jmp label2

label1:
; 如果跳转到这里,说明 24 > 3
; 这里可以添加一些操作

label2:
; 如果跳转到这里,说明 24 <= 3
; 这里可以添加一些操作

; 函数调用示例
call my_function  ; 调用函数 my_function

; 从函数返回
ret

; 定义一个标签,用于跳转
label3:
; 这里可以添加一些操作
相关推荐
Crossoads9 小时前
【汇编语言】端口 —— 「从端口到时间:一文了解CMOS RAM与汇编指令的交汇」
android·java·汇编·深度学习·网络协议·机器学习·汇编语言
雪碧透心凉_3 天前
8086汇编(16位汇编)学习笔记00.DEBUG命令使用解析及范例大全
汇编
C66668886 天前
C#多线程
开发语言·汇编·c#
傻童:CPU6 天前
汇编源程序的理解
汇编
木槿716 天前
软件包git没有可安装候选
汇编·git
ok0607 天前
各种开源汇编、反汇编引擎的非专业比较
汇编·开源
roboko_7 天前
MIPS指令集(一)基本操作
汇编
Crossoads7 天前
【汇编语言】内中断(三) —— 中断探险:从do0到特殊响应的奇妙旅程
android·开发语言·javascript·网络·汇编·单片机·机器学习
染指11108 天前
49.第二阶段x86游戏实战2-鼠标点击call深追二叉树
汇编·c++·windows·游戏安全·反游戏外挂·游戏逆向
程序leo源10 天前
深入理解指针
android·c语言·开发语言·汇编·c++·青少年编程·c#