x86 CPU汇编语言入门教程

已剪辑自: https://www.ruanyifeng.com/blog/2018/01/assembly-language-primer.html

学习编程其实就是学高级语言,即那些为人类设计的计算机语言。

但是,计算机不理解高级语言,必须通过编译器转成二进制代码,才能运行。学会高级语言,并不等于理解计算机实际的运行步骤。

img

计算机真正能够理解的是低级语言,它专门用来控制硬件。汇编语言就是低级语言,直接描述/控制 CPU 的运行。如果你想了解 CPU 到底干了些什么,以及代码的运行步骤,就一定要学习汇编语言。

汇编语言不容易学习,就连简明扼要的介绍都很难找到。下面我尝试写一篇最好懂的汇编语言教程,解释 CPU 如何执行代码。

img

一、汇编语言是什么?

我们知道,CPU 只负责计算,本身不具备智能。你输入一条指令(instruction),它就运行一次,然后停下来,等待下一条指令。

这些指令都是二进制的,称为操作码(opcode),比如加法指令就是00000011编译器的作用,就是将高级语言写好的程序,翻译成一条条操作码。

对于人类来说,二进制程序是不可读的,根本看不出来机器干了什么。为了解决可读性的问题,以及偶尔的编辑需求,就诞生了汇编语言。

img

汇编语言是二进制指令的文本形式,与指令是一一对应的关系。比如,加法指令00000011写成汇编语言就是 ADD。只要还原成二进制,汇编语言就可以被 CPU 直接执行,所以它是最底层的低级语言。

二、来历

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

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

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

img

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

三、寄存器

学习汇编语言,首先必须了解两个知识点:寄存器和内存模型。

先来看寄存器。CPU 本身只负责运算,不负责储存数据。数据一般都储存在内存之中,CPU 要用的时候就去内存读写数据。但是,CPU 的运算速度远高于内存的读写速度,为了避免被拖慢,CPU 都自带一级缓存和二级缓存。基本上,CPU 缓存可以看作是读写速度较快的内存。

但是,CPU 缓存还是不够快,另外数据在缓存里面的地址是不固定的,CPU 每次读写都要寻址也会拖慢速度。因此,除了缓存之外,CPU 还自带了寄存器(register),用来储存最常用的数据。也就是说,那些最频繁读写的数据(比如循环变量),都会放在寄存器里面,CPU 优先读写寄存器,再由寄存器跟内存交换数据。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sX3cVkLq-1682773146807)(null)]

寄存器不依靠地址区分数据,而依靠名称。每一个寄存器都有自己的名称,我们告诉 CPU 去具体的哪一个寄存器拿数据,这样的速度是最快的。有人比喻寄存器是 CPU 的零级缓存。

四、寄存器的种类

早期的 x86 CPU 只有8个寄存器,而且每个都有不同的用途。现在的寄存器已经有100多个了,都变成通用寄存器,不特别指定用途了,但是早期寄存器的名字都被保存了下来。

  • EAX
  • EBX
  • ECX
  • EDX
  • EDI
  • ESI
  • EBP
  • ESP

上面这8个寄存器之中,前面七个都是通用的。ESP 寄存器有特定用途,保存当前 Stack 的地址(详见下一节)。

img

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

五、内存模型:Heap

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

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9TJ5LKtI-1682773146659)(null)]

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ts4mthgI-1682773146761)(null)]

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

六、内存模型:Stack

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

img

请看下面的例子。

int main() {
   int a = 2;
   int b = 3;
}

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

img

如果函数内部调用了其他函数,会发生什么情况?

int main() {
   int a = 2;
   int b = 3;
   return add_a_and_b(a, b);
}

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

img

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

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kd4sX2Eq-1682773146710)(null)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Qv6AdZYY-1682773146878)(null)]

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

img

七、CPU 指令

7.1 一个实例

了解寄存器和内存模型以后,就可以来看汇编语言到底是什么了。下面是一个简单的程序example.c

int add_a_and_b(int a, int b) {
   return a + b;
}

int main() {
   return add_a_and_b(2, 3);
}

gcc 将这个程序转成汇编语言。

$ gcc -S example.c

上面的命令执行以后,会生成一个文本文件example.s,里面就是汇编语言,包含了几十行指令。这么说吧,一个高级语言的简单操作,底层可能由几个,甚至几十个 CPU 指令构成。CPU 依次执行这些指令,完成这一步操作。

example.s经过简化以后,大概是下面的样子。

_add_a_and_b:
   push   %ebx
   mov    %eax, [%esp+8] 
   mov    %ebx, [%esp+12]
   add    %eax, %ebx 
   pop    %ebx 
   ret  

_main:
   push   3
   push   2
   call   _add_a_and_b 
   add    %esp, 8
   ret

可以看到,原程序的两个函数add_a_and_bmain,对应两个标签_add_a_and_b_main。每个标签里面是该函数所转成的 CPU 运行流程。

每一行就是 CPU 执行的一次操作。它又分成两部分,就以其中一行为例。

push   %ebx

这一行里面,push是 CPU 指令,%ebx是该指令要用到的运算子。一个 CPU 指令可以有零个到多个运算子。

下面我就一行一行讲解这个汇编程序,建议读者最好把这个程序,在另一个窗口拷贝一份,省得阅读的时候再把页面滚动上来。

7.2 push 指令

根据约定,程序从_main标签开始执行,这时会在 Stack 上为main建立一个帧,并将 Stack 所指向的地址,写入 ESP 寄存器。后面如果有数据要写入main这个帧,就会写在 ESP 寄存器所保存的地址。

然后,开始执行第一行代码。

push   3

push指令用于将运算子放入 Stack,这里就是将3写入main这个帧。

虽然看上去很简单,push指令其实有一个前置操作。它会先取出 ESP 寄存器里面的地址,将其减去4个字节,然后将新地址写入 ESP 寄存器。使用减法是因为 Stack 从高位向低位发展,4个字节则是因为3的类型是int,占用4个字节。得到新地址以后, 3 就会写入这个地址开始的四个字节。

push   2

第二行也是一样,push指令将2写入main这个帧,位置紧贴着前面写入的3。这时,ESP 寄存器会再减去 4个字节(累计减去8)。

img

7.3 call 指令

第三行的call指令用来调用函数。

call   _add_a_and_b

上面的代码表示调用add_a_and_b函数。这时,程序就会去找_add_a_and_b标签,并为该函数建立一个新的帧。

下面就开始执行_add_a_and_b的代码。

push   %ebx

这一行表示将 EBX 寄存器里面的值,写入_add_a_and_b这个帧。这是因为后面要用到这个寄存器,就先把里面的值取出来,用完后再写回去。

这时,push指令会再将 ESP 寄存器里面的地址减去4个字节(累计减去12)。

7.4 mov 指令

mov指令用于将一个值写入某个寄存器。

mov    %eax, [%esp+8] 

这一行代码表示,先将 ESP 寄存器里面的地址加上8个字节,得到一个新的地址,然后按照这个地址在 Stack 取出数据。根据前面的步骤,可以推算出这里取出的是2,再将2写入 EAX 寄存器。

下一行代码也是干同样的事情。

mov    %ebx, [%esp+12] 

上面的代码将 ESP 寄存器的值加12个字节,再按照这个地址在 Stack 取出数据,这次取出的是3,将其写入 EBX 寄存器。

7.5 add 指令

add指令用于将两个运算子相加,并将结果写入第一个运算子。

add    %eax, %ebx

上面的代码将 EAX 寄存器的值(即2)加上 EBX 寄存器的值(即3),得到结果5,再将这个结果写入第一个运算子 EAX 寄存器。

7.6 pop 指令

pop指令用于取出 Stack 最近一个写入的值(即最低位地址的值),并将这个值写入运算子指定的位置。

pop    %ebx

上面的代码表示,取出 Stack 最近写入的值(即 EBX 寄存器的原始值),再将这个值写回 EBX 寄存器(因为加法已经做完了,EBX 寄存器用不到了)。

注意,pop指令还会将 ESP 寄存器里面的地址加4,即回收4个字节。

7.7 ret 指令

ret指令用于终止当前函数的执行,将运行权交还给上层函数。也就是,当前函数的帧将被回收。

ret

可以看到,该指令没有运算子。

随着add_a_and_b函数终止执行,系统就回到刚才main函数中断的地方,继续往下执行。

add    %esp, 8 

上面的代码表示,将 ESP 寄存器里面的地址,手动加上8个字节,再写回 ESP 寄存器。这是因为 ESP 寄存器的是 Stack 的写入开始地址,前面的pop操作已经回收了4个字节,这里再回收8个字节,等于全部回收。

ret

最后,main函数运行结束,ret指令退出程序执行。

八、参考链接

(完)

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
英文版 以下是我从网上所的相关介绍 内容简介 本书以X86系列微机为背景,从简单的Hello程序开始,系统而详细地阐述了X86微机汇编语言编程的行种基础知识和编程技巧,内容涉及到数据表示、存储器管理、各种数据类型、过程、与汇编语言相关的体系结构、控制结构、文件、宏指令、位处理指令、字符串指令、MMX指令、类和对象,以及混合语言编程等,尤其是在高级汇编语言(HLA)方面,该书给予了细致深入的讲解。对于有意学习X86汇编语言编程的程序员来说,这是一本难得的好书。 本书的作者Randall Hyde拥有十多年的汇编语言教学经验,并且开发了多个商用软件,具有实际的汇编语言开发经验。该书的英文网络版受到全球成千上万的网站和高级程序员的高度评价,被大家公推为高级汇编语言编程的经典之作。该书的英文正版推出不久,即有很多人在“亚马逊”网站上为其作评,而且几乎所有的人都给予5星的高分,可见其内容之好 目录 第1章 进入汇编语言的世界 1.1 本章概述 1.2 HLA程序的结构 1.3 运行第一个HLA程序 1.4 基本的HLA数据声明 1.5 布尔值 1.6 字符值 1.7 Intel80x86处理器简介 1.8 基本的机器指令 1.9 基本的HLA控制结构 1.10 HLA标准库入门 1.11 关于TRY..ENDTRY的其他细节 1.12 高级汇编语言与底级汇编语言比较 1.13 更多信息 第2章 数据表示 2.1 本章概述 2.2 数字系统 2.3 十六进制数字系统 2.4 数据结构 2.5 二进制数与十六进制数的算术运算 2.6 关于数字及其表示法 2.7 位逻辑运算 2.8 二进制数和位串的逻辑运算 2.9 有符号数和无符号数 2.10 符号扩展、零扩展、压缩和饱和 2.11 侈位和缩环移位 2.12 位域和压缩数据 2.13 浮点运算简介 2.14 BCD数据表示 2.15 字符 2.16 Unicode字符集 2.17 更多信息 第3章 存储器的访问与结构 3.1 本章概述 3.2 80x86的寻址方式 3.3 运行时存储器的结构 3.4 HLA如何为变量分配内存 3.5 HLA对数据对齐的支持 3.6 地址表达式 3.7 类型强制转换 3.8 寄存器类型强制转换 3.9 栈段与PUSH及POP指令 3.10 动态内存分配和堆段 3.11 INC和DEC指令 3.12 获取存储器对象的地址 3.13 更多信息 第4章 常量、变量与数据类型 4.1 本章概述 4.2 一些额外的指令:INTMUL、BOUND、INTO 4.3 TBYTE数据类型 4.4 HLA常量和数值声明 4.5 HLA和TYPE段 4.6 ENUM和HLA枚举数据类型 4.7 指针数据类型 4.8 HLA标准库CHARS.HHF模型 4.9 复合数据类型 4.10 字符串 4.11 HLA字符串 4.12 访问字符中的某个字符 4.13 HLA字符串模块和其他与字符串机关的例程 4.14 存储器内转换 4.15 字符集 4.16 在HLA中实现字符集 4.17 HLA字符集常量和字符集表达工 4.18 HLA HLL布尔表达式中的IN操作符 4.19 HLA标准库对字符集的支持 4.20 在HLA程序中使用字符集 4.21 数组 4.22 在HLA程序中声明数组 4.23 HLA数组常量 4.24 访问一维数组的元素 4.25 多维数组 4.26 多维数组的存储空间分配 4.27 汇编语言中多维数组元素的访问 4.28 大数组和MASM(只适用于Windows程序员) 4.29 记录 4.30 记录常量 4.31 记录数组 4.32 数组/记录作为记录字段 4.33 控制记录中的字面偏移量 4.34 对齐记录中的字段 4.35 记录指针 4.36 联合 4.37 匿名联合 4.38 变量类型 4.39 联合常量 4.40 命名空间 4.41 汇编语言中的动态数组 4.42 HLA标准库数组支持 4.43 更多信息 第5章 过程与单元 5.1 本章概述 5.2 过程 5.3 机器状态的保存 5.4 过程的提前返回 5.5 局部变量 5.6 其他局部和全局符号类型 5.7 参数 5.8 函数和函数的结果 5.9 递归 5.10 过程的向前引用 5.11 过程的底层实现与CALL指令 5.12 过程与堆栈 5.13 活动记录 5.14 标准入口序列 5.15 标准出口序列 5.16 自动(局部)变量的底层实现 5.17 参数的度层实现 5.18 过程指针 5.19 过程参数 5.20 无类型的引用参数 5.21 管理大型程序 5.22 #INCLUDE伪指令 5.23 忽略重复的#INCLUDE操作 5.24 单元与EXTERNAL伪指令 5.25 命名空间的污染 5.26 更多信息 第6章 算术运算 6.1 本章概述 6.2 80x86的整数运算指令 6.3 算术表达式 6.4 逻辑(布尔)表达式 6.5 机器特征与运算技巧 6.6 浮点运算 6.7 浮点表达式到汇编语言的转换 6.8 HLA标准库对浮点算术运算的支持 6.9 算术运算小结 第7章 低级控制结构 7.1 本章概述 7.2 低级控制结构 7.3 语句标号 7.4 无条件控制转移(JMP) 7.5 条件跳转指令 7.6 “中级”控制结构:JT和JF 7.7 使用汇编语言实现通用控制结构 7.8 选择 7.9 状态机和间接跳转 7.10 “面条式”代码 7.11 循环 7.12 性能提高 7.13 HLA中的混合控制结构 7.14 更多信息 第8章 文件 8.1 本章概述 8.2 文件组织 8.3 顺序文件 8.4 随机访问文件 8.5 ISAM文件 8.6 截断文件 8.7 更多信息 第9章 高级算术运算 9.1 本章概述 9.2 多精度操作 9.3 对不同长度的操作数进行操作 9.4 十进制算术运算 9.5 表 9.6 更多信息 第10章 宏与HLA编译时语言 10.1 本章概述 10.2 编译时语言 10.3 #PRINT和#ERROR语句 10.4 编译时常量和变量 10.5 编译时表达式和操作符 10.6 编译时函数 10.7 条件编译(编译时决定) 10.8 重复编译(编译时循环) 10.9 宏(编译时过程) 10.10 编写编译时“程序” 10.11 在不同的源文件中使用宏 10.12 更多信息 第11章 位操作 11.1 本章概述 11.2 位数据 11.3 位操作指令 11.4 作为位累加器的进位标志位 11.5 位串的压缩与解压缩 11.6 接合位组与分布位串 11.7 压缩的位串数组 11.8 搜索位 11.9 位的计数 11.10 倒置位串 11.11 合并位串 11.12 提取位串 11.13 搜索位模式 11.14 HLA标准库的位模块 11.15 更多信息 第12章 字符串指令 12.1 本章概述 12.2 80x86字符串指令 12.3 80x86字符串指令的性能 12.4 更多信息 第13章 MMX指令集 13.1 本章概述 13.2 判断CPU是否支持MMX指令集 13.3 MMX编程环境 13.4 设计MMX指令集的目的 13.5 饱和算未能和回转模式 13.6 MMX指令操作数 13.7 MMX技术指令 第14章 类与对象 14.1 本章概述 14.2 通用原则 14.3 HLA中的类 14.4 对象 14.5 继承 14.6 重载 14.7 虚拟方法与静态过程 14.8 编写类方法和过程 14.9 对象实现 14.10 构造函数和对象初始化 14.11 析构函数 14.12 HLA的“_initialize_”和“_finalize_”字符串 14.13 抽像方法 14.14 运行时类型信息(RTTI) 14.15 调用基类的方法 14.16 更多信息 第15章 混合语言编程 15.1 本章概述 15.2 在同一程序中混合使用HLA和MASM/Gas代码 15.3 使用Delphi/Kylix和HLA编程 15.4 使用C/C++和HLA编程 15.5 更多信息 附录A ASCII字符集 附录B 80x86指令集
目录 介 绍 ……………………………………………………………………………2 为什么选用汇编语言 …………………………………………………………2 为什么选择Windows系统……………………………………………………2 Ⅰ开始学习之旅 ……………………………………………………………….…3 编译器 ………………………………………………………………………3 编辑器 …………………………………………………………………………3 Ⅱ第一个程序 ……………………………………………………………………4 控制台程序 ……………………………………………………………………4 窗体程序 ………………………………………………………………………6 ADDR 与 OFFSET ………………………………………………………6 Ⅲ汇编基础…………………………………………………………………………7 cpu寄存器 ……………………………………………………………………7 指令集基础 ……………………………………………………………………8 Push 和 Pop…………………………………………………………………8 Invoke ……………………………………………………………………9 程序例子 ………………………………………………………………………9 IV. 窗体程序基础…………………………………………………………………10 预备知识………………………………………………………………………10 宏……………………………………………………………………………10 过程…………………………………………………………………………10 变量…………………………………………………………………………10 一个简单的窗体程序…………………………………………………………11 IV. 深入汇编和系统………………………………………………………………13 字符串操作……………………………………………………………………13 文件管理………………………………………………………………………13 存储…………………………………………………………………………14 程序例子……………………………………………………………………14 控制……………………………………………………………………………15 附加资源 …………………………………………………………………………16 互联网………………………………………………………………………16 书籍…………………………………………………………………………16 MASM32……………………………………………………………………16 MSDN………………………………………………………………………16 新闻组………………………………………………………………………16 IRC …………………………………………………………………………16 介 绍 “This is for all you folks out there,who want to learn the magic art of Assembly programming” -MAD 介 绍 我最近才开始学习windows系统汇编语言编程,这个教程是我在学习汇编语言的过程中写下来的。我阅读大量的在线教程、书本,以及通过新闻组以及IRC通讯工具请问他人,本人就是通过这些方式学习汇编语言的。互联网上有很多的汇编编程的教程,但这些教程只是侧重于X86汇编。因为这些教材都假定读者已经掌握了高级编程语言以及基本的计算机系统知识。 为什么选用汇编语言汇编语言具有若干的特色,使得在某此情况下,汇编语言是一种很好的选择。 1 快速 汇编语言程序运行的速度比高级语言程序要快。通常,要求运行效率高的子程序是用汇编语言编写的。 2 强大 运用汇编语言,你能得到不受限制的权力。相对的,高级语言则有种种限制,在实现某些特定的要求时变得困难。 3 体积小 汇编语言程序通常比其他语言程序要小得多。这种特性在空间有限的情况下是非常有用的。 为什么选择Windows系统? 在任何操作系统和处理器模式下,都可以编写相应的汇编语言程序的。但是当前,多数人在使用基于x86处理器的Windows系统,所以从编写运行于此种环境下的程序开始我们的教程。一旦一种汇编语言的基础知识掌握了,我们就会很容易写出在其他运行环境下汇编程序。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小熊coder

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值