首页 简明_X86_汇编语言教程

简明_X86_汇编语言教程

举报
开通vip

简明_X86_汇编语言教程 简明 X86 汇编语言教程 原创:司徒彦南 徐远超于 2010-02-25 收集整理 目录 第 Ο 章 写在前面...................................................................................................................1 第一章 汇编语言简介......................................................................

简明_X86_汇编语言教程
简明 X86 汇编语言教程 原创:司徒彦南 徐远超于 2010-02-25 收集整理 目录 第 Ο 章 写在前面...................................................................................................................1 第一章 汇编语言简介.............................................................................................................2 第二章 认识处理器.................................................................................................................3 2.1 寄存器........................................................................................................................3 2.2 使用寄存器................................................................................................................6 第三章 操作内存...................................................................................................................11 3.1 实模式......................................................................................................................12 3.2 保护模式..................................................................................................................15 3.3 操作内存..................................................................................................................18 3.4 串操作......................................................................................................................20 3.5 关于保护模式中内存操作的一点说明.................................................................22 3.6 堆栈.........................................................................................................................22 本章小结.........................................................................................................................24 第四章 利用子程序与中断...................................................................................................24 4.1 子程序......................................................................................................................25 4.2 中断..........................................................................................................................30 第五章 编译优化概述...........................................................................................................33 5.1 循环优化:强度削减和代码外提.........................................................................35 5.2 局部优化:表达式预计算和子表达式提取.........................................................36 5.3 全局寄存器优化.....................................................................................................37 5.4 x86 体系结构上的并行最大化和指令封包..........................................................40 5.5 存储优化..................................................................................................................41 第Ο章 写在前面 我不想夸大或者贬低汇编语言。但我想说,汇编语言改变了 20 世纪的历史。与前辈相 比,我们这一代编程人员足够的幸福,因为我们有各式各样的编程语言,我们可以操作键盘、 坐在显示器面前,甚至使用鼠标、语音识别。我们可以使用键盘、鼠标来驾驭“个人计算机”, 而不是和一群人共享一台使用笨重的继电器、开关去操作的巨型机。相比之下,我们的前辈 不得不使用机器语言编写程序,他们甚至没有最简单的汇编程序来把助记符翻译成机器语 言,而我们可以从上千种计算机语言中选择我们喜欢的一种,而汇编,虽然不是一种“常用” 的具有“快速原型开发”能力的语言,却也是我们可以选择的语言中的一种。 每种计算机都有自己的汇编语言——没必要指望汇编语言的可移植性,选择汇编,意味 着选择性能而不是可移植或便于调试。这份文档中讲述的是 x86 汇编语言,此后的“汇编语 言”一词,如果不明示则表示 IA32 上的 x86 汇编语言。 汇编语言是一种易学,却很难精通的语言。回想当年,我从初学汇编到写出第一个可运 行的程序,只用了不到 4 个小时;然而直到今天,我仍然不敢说自己精通它。编写快速、高 效、并且能够让处理器“很舒服地执行”的程序是一件很困难的事情,如果利用业余时间学习, 通常需要 2-3 年的时间才能做到。这份教材并不期待能够教给你大量的汇编语言技巧。对于 读者来说,x86 汇编语言"就在这里"。然而,不要僵化地局限于这份教材讲述的内容,因为 它只能告诉你汇编语言是“这样一回事”。学好汇编语言,更多的要靠一个人的创造力与悟性, 我可以告诉你我所知道的技巧,但肯定这是不够的。一位对我的编程生涯产生过重要影响的 人曾经对我说过这么一句话: 写汇编语言程序不是汇编语言最难的部分,创新才是。 我想,愿意看这份文档的人恐怕不会问我“为什么要学习汇编语言”这样的问题;不过, 我还是想说几句:首先,汇编语言非常有用,我个人主张把它作为 C 语言的先修课程,因 为通过学习汇编语言,你可以了解到如何有效地 设计 领导形象设计圆作业设计ao工艺污水处理厂设计附属工程施工组织设计清扫机器人结构设计 数据结构,让计算机处理得更快,并使 用更少的存储空间;同时,学习汇编语言可以让你熟悉计算机内部运行机制,并且,有效地 提高调试能力。就我个人的经验而言,调试一个非结构化的程序的困难程度,要比调试一个 结构化的程序的难度高很多,因为“结构化”是以牺牲运行效率来提高可读性与可调试性,这 对于完成一般软件工程的编码阶段是非常必要的。然而,在一些地方,比如,硬件驱动程序、 操作系统底层,或者程序中经常需要执行的代码,结构化程序设计的这些优点有时就会被它 的低效率所抹煞。另外,如果你想真正地控制自己的程序,只知道源代码级的调试是远远不 够的。 浮躁的人喜欢说,用 C++写程序足够了,甚至说,他不仅仅掌握 C++,而且精通 STL、 MFC。我不赞成这个观点,掌握上面的那些是每一个编程人员都应该做到的,然而 C++只 是我们"常用"的一种语言,它不是编程的全部。低层次的开发者喜欢说,嘿,C++是多么的 强大,它可以做任何事情——这不是事实。便于维护、调试,这些确实是我们的追求目标, 但是,写程序不能仅仅追求这个目标(还有性能、功耗整理者注),因为我们最终的目的是满足 设计需求,而不是个人非理性的理想。 这份教材适合已经学习过某种结构化程序设计语言的读者。其内容基于我在 1995 年给 别人讲述汇编语言时所写的讲义。当然,如大家所希望的,它包含了最新的处理器所支持的 特性,以及相应的内容。我假定读者已经知道了程序设计的一些基本概念,因为没有这些是 无法理解汇编语言程序设计的;此外,我希望读者已经有了比较良好的程序设计基础,因为 如果你缺乏对于结构化程序设计的认识,编写汇编语言程序很可能很快就破坏了你的结构化 编程习惯,大大降低程序的可读性、可维护性,最终让你的程序陷于不得不废弃的代码堆之 中。 基本上,这份文档撰写的目标是尽可能地便于自学。不过,它对你也有一些要求,尽管 不是很高,但我还是强调一下。学习汇编语言,你需要: 胆量。不要害怕去接触那些计算机的内部工作机制。 知识。了解计算机常用的数制,特别是二进制、十六进制、八进制,以及计算机保存数据的 方法 快递客服问题件处理详细方法山木方法pdf计算方法pdf华与华方法下载八字理论方法下载 。 开放。接受汇编语言与高级语言的差异,而不是去指责它如何的不好读。 经验。要求你拥有任意其他编程语言的一点点编程经验。 头脑。 祝您编程愉快! 第一章 汇编语言简介 先说一点和实际编程关系不太大的东西。当然,如果你迫切的想看到更实质的内容,完 全可以先跳过这一章。 那么,我想可能有一个问题对于初学汇编的人来说非常重要,那就是:汇编语言到底是 什么?汇编语言是一种最接近计算机核心的编码语言。不同于任何高级语言,汇编语言几乎 可以完全和机器语言一一对应。不错,我们可以用机器语言写程序,但现在除了没有汇编程 序的那些电脑之外,直接用机器语言写超过 1000 条以上指令的人大概只能算作那些被我们 成为“圣人”的牺牲者一类了。毕竟,记忆一些短小的助记符、由机器去考虑那些琐碎的配位 过程和检查错误,比记忆大量的随计算机而改变的十六进制代码、可能弄错而没有任何提示 要强的多。熟练的汇编语言编码员甚至可以直接从十六进制代码中读出汇编语言的大致意 思。当然,我们有更好的工具——汇编器和反汇编器。 简单地说,汇编语言就是机器语言的一种可以被人读懂的形式,只不过它更容易记忆。 至于宏汇编,则是包含了宏支持的汇编语言,这可以让你编程的时候更专注于程序本身,而 不是忙于计算和重写代码。 汇编语言除了机器语言之外最接近计算机硬件的编程语言。由于它如此的接近计算机硬 件,因此,它可以最大限度地发挥计算机硬件的性能。用汇编语言编写的程序的速度通常要 比高级语言和 C/C++快很多--几倍,几十倍,甚至成百上千倍。当然,解释语言,如解释型 LISP,没有采用 JIT 技术的 Java 虚拟机中运行的 Java 等等,其程序速度更无法与汇编语言 程序同日而语 。 永远不要忽视汇编语言的高速。实际的应用系统中,我们往往会用汇编彻底重写某些经 常调用的部分以期获得更高的性能。应用汇编也许不能提高你的程序的稳定性,但至少,如 果你非常小心的话,它也不会降低稳定性;与此同时,它可以大大地提高程序的运行速度。 我强烈建议所有的软件产品在最后 Release 之前对整个代码进行 Profile,并适当地用汇编取 代部分高级语言代码。至少,汇编语言的知识可以告诉你一些有用的东西,比如,你有多少 个寄存器可以用。有时,手工的优化比编译器的优化更为有效,而且,你可以完全控制程序 的实际行为。 我想我在罗嗦了。总之,在我们结束这一章之前,我想说,不要在优化的时候把希望完 全寄托在编译器上——现实一些,再好的编译器也不可能总是产生最优的代码。 第二章 认识处理器 中央处理器(CPU)在微机系统处于“领导核心”的地位。汇编语言被编译成机器语言之 后,将由处理器来执行。那么,首先让我们来了解一下处理器的主要作用,这将帮助你更好 地驾驭它。 典型的处理器的主要任务包括从内存中获取机器语言指令,译码,执行根据指令代码管 理它自己的寄存器根据指令或自己的需要修改内存的内容响应其他硬件的中断请求。一般说 来,处理器拥有对整个系统的所有总线的控制权。对于 Intel 平台而言,处理器拥有对数据、 内存和控制总线的控制权,根据指令控制整个计算机的运行。在以后的章节中,我们还将讨 论系统中同时存在多个处理器的情况。 处理器中有一些寄存器,这些寄存器可以保存特定长度的数据。某些寄存器中保存的数 据对于系统的运行有特殊的意义。新的处理器往往拥有更多、具有更大字长的寄存器,提供 更灵活的取指、寻址方式。 2.1 寄存器 如前所述,处理器中有一些可以保存数据的地方被称作寄存器。寄存器可以被装入数据, 你也可以在不同的寄存器之间移动这些数据,或者做类似的事情。基本上,像四则运算、位 运算等这些计算操作,都主要是针对寄存器进行的。 首先让我来介绍一下 80386 上最常用的 4 个通用寄存器。先瞧瞧下面的图形,试着理解 一下: 上图中,数字表示的是位。我们可以看出,EAX 是一个 32-bit 寄存器。同时,它的低 16-bit 又可以通过 AX 这个名字来访问;AX 又被分为高、低 8bit 两部分,分别由 AH 和 AL 来表示。对于 EAX、AX、AH、AL 的改变同时也会影响与被修改的那些寄存器的值。从而 事实上只存在一个 32-bit 的寄存器 EAX,而它可以通过 4 种不同的途径访问。 也许通过名字能够更容易地理解这些寄存器之间的关系。EAX 中的 E 的意思是“扩展 的”,整个 EAX 的意思是扩展的 AX。X 的意思 Intel 没有明示,我个人认为表示它是一个 可变的量。而 AH、AL 中的 H 和 L 分别代表高和低。 为什么要这么做呢?主要由于历史原因。早期的计算机是 8 位的,8086 是第一个 16 位 处理器,其通用寄存器的名字是 AX,BX 等等;80386 是 Intel 推出的第一款 IA-32 系列处 理器,所有的寄存器都被扩充为 32 位。为了能够兼容以前的 16 位应用程序,80386 不能将 这些寄存器依旧命名为 AX、BX,并且简单地将他们扩充为 32 位——这将增加处理器在处 理指令方面的成本。 Intel 微处理器的寄存器列表(在本章只介绍 80386 的寄存器,MMX 寄存器以及其他新 一代处理器的新寄存器将在以后的章节介绍) 通用寄存器 下面介绍通用寄存器及其习惯用法。顾名思义,通用寄存器是那些你可以根据自己的意 愿使用的寄存器,修改他们的值通常不会对计算机的运行造成很大的影响。通用寄存器最多 的用途是计算。 EAX 32-bit 宽 通用寄存器。相对其他寄存器,在进行运算方面比较常用。在保护模式中,也可以 作为内存偏移指针(此时,DS 作为段 寄存器或选择器) EBX 32-bit 宽 通用寄存器。通常作为内存偏移指针使用(相对于 EAX、ECX、EDX),DS 是默认的 段寄存器或选择器。在保护模式中,同样可以起这个作用。 ECX 32-bit 宽 通用寄存器。通常用于特定指令的计数。在保护模式中,也可以作为内存偏移指针 (此时,DS 作为 寄存器或段选择器)。 EDX 32-bit 宽 通用寄存器。在某些运算中作为 EAX 的溢出寄存器(例如乘、除)。在保护模式中, 也可以作为内存偏移指针(此时,DS 作为段 寄存器或选择器)。 上述寄存器同 EAX 一样包括对应的 16-bit 和 8-bit 分组。 用作内存指针的特殊寄存器 ESI 32-bit 宽 通常在内存操作指令中作为“源地址指针”使用。当然,ESI 可以被装入任意的数 值,但通常没有人把它当作通用寄存器来用。DS 是默认段寄存器或选择器。 EDI 通常在内存操作指令中作为“目的地址指针”使用。当然,EDI 也可以被装入任意 32-bit 宽 的数值,但通常没有人把它当作通用寄存器来用。DS 是默认段寄存器或选择器。 EBP 32-bit 宽 这也是一个作为指针的寄存器。通常,它被高级语言编译器用以建造‘堆栈帧’来 保存函数或过程的局部变量,不过,还是那句话,你可以在其中保存你希望的任何 数据。SS 是它的默认段寄存器或选择器。 注意,这三个寄存器没有对应的 8-bit 分组。换言之,你可以通过 SI、DI、BP 作为别名访 问他们的低 16 位,却没有办法直接访问他们的低 8 位。 段寄存器和选择器 实模式下的段寄存器到保护模式下摇身一变就成了选择器。不同的是,实模式下的“段寄存 器”是 16-bit 的,而保护模式下的选择器是 32-bit 的。 CS 代码段,或代码选择器。同 IP 寄存器(稍后介绍)一同指向当前正在执行的那个地 址。处理器执行时从这个寄存器指向的段(实模式)或内存(保护模式)中获取指 令。除了跳转或其他分支指令之外,你无法修改这个寄存器的内容。 DS 数据段,或数据选择器。这个寄存器的低 16 bit 连同 ESI 一同指向的指令将要处 理的内存。同时,所有的内存操作指令 默认情况下都用它指定操作段(实模式)或 内存(作为选择器,在保护模式。这个寄存器可以被装入任意数值,然而在这么做 的时候需要小心一些。方法是,首先把数据送给 AX,然后再把它从 AX 传送给 DS(当 然,也可以通过堆栈来做). ES 附加段,或附加选择器。这个寄存器的低 16 bit 连同 EDI 一同指向的指令将要处理的内存。同样的,这个寄存器可以被装入任意数值,方法和 DS 类似。 FS F 段或 F 选择器(推测 F 可能是 Free?)。可以用这个寄存器作为默认段寄存器或选择器的一个替代品。它可以被装入任何数值,方法和 DS 类似。 GS G 段或 G 选择器(G 的意义和 F 一样,没有在 Intel 的文档中解释)。它和 FS 几乎完全一样。 SS 堆栈段或堆栈选择器。这个寄存器的低 16bit 连同 ESP 一同指向下一次堆栈操作 (push 和 pop)所要使用的堆栈地址。这个寄存器也可以被装入任意数值,你可以通 过入栈和出栈操作来给他赋值,不过由于堆栈对于很多操作有很重要的意义,因此, 不正确的修改有可能造成对堆栈的破坏。 * 注意 一定不要在初学汇编的阶段把这些寄存器弄混。他们非常重要,而一旦你掌握了他 们,你就可以对他们做任意的操作了。段寄存器,或选择器,在没有指定的情况下都是使用 默认的那个。这句话在现在看来可能有点稀里糊涂,不过你很快就会在后面知道如何去做。 特殊寄存器(指向到特定段或内存的偏移量): EIP 这个寄存器非常的重要。这是一个 32 位宽的寄存器 ,同 CS 一同指向即将执行的 那条指令的地址。不能够直接修改这个寄存器的值,修改它的唯一方法是跳转或分 支指令。(CS 是默认的段或选择器) ESP 这个 32 位寄存器指向堆栈中即将被操作的那个地址。尽管可以修改它的值,然而 并不提倡这样做,因为如果你不是非常明白自己在做什么,那么你可能造成堆栈的 破坏。对于绝大多数情况而言,这对程序是致命的。(SS 是默认的段或选择器) IP: Instruction Pointer, 指令指针 SP: Stack Pointer, 堆栈指针 好了,上面是最基本的寄存器。下面是一些其他的寄存器,你甚至可能没有听说过它们。 (都是 32 位宽): CR0, CR2, CR3(控制寄存器)。举一个例子,CR0 的作用是切换实模式和保护模式。 还有其他一些寄存器,D0, D1, D2, D3, D6 和 D7(调试寄存器)。他们可以作为调试器的硬件 支持来设置条件断点。 TR3, TR4, TR5, TR6 和 TR? 寄存器(测试寄存器)用于某些条件测试。 最后我们要说的是一个在程序设计中起着非常关键的作用的寄存器:标志寄存器。 2.2 使用寄存器 在前一节中的 x86 基本寄存器的介绍,对于一个汇编语言编程人员来说是不可或缺的。 现在你知道,寄存器是处理器内部的一些保存数据的存储单元。仅仅了解这些是不足以写出 一个可用的汇编语言程序的,但你已经可以大致读懂一般汇编语言程序了(不必惊讶,因为 汇编语言的祝记符和英文单词非常接近),因为你已经了解了关于基本寄存器的绝大多数知 识。 在正式引入第一个汇编语言程序之前,我粗略地介绍一下汇编语言中不同进制整数的表 示方法。如果你不了解十进制以外的其他进制,请把鼠标移动到这里。 汇编语言中的整数常量表示 十进制整数 这是汇编器默认的数制。直接用我们熟悉的表示方式表示即可。例如,1234 表示十进制的 1234。不过,如果你指定了使用其他数制,或者有凡事都进行完整定义的小爱好,也可以写 成[十进制数]d 或[十进制数]D 的形式。 十六进制数 这是汇编程序中最常用的数制,我个人比较偏爱使用十六进制表示数据,至于为什么,以后 我会作说明。十六进制数表示为 0[十六进制数]h 或 0[十六进制数]H,其中,如果十六进制 数的第一位是数字,则开头的 0 可以省略。例如,7fffh, 0ffffh 等等。 二进制数 这也是一种常用的数制。二进制数表示为[二进制数]b 或[二进制数]B。一般程序中用二进制 数表示掩码(mask code)等数据非常的直观,但需要些很长的数据(4 位二进制数相当于一 位十六进制数)。例如,1010110b。 八进制数 八进制数现在已经不是很常用了(确实还在用,一个典型的例子是 Unix 的文件属性)。八进 制数的形式是[八进制数]q、[八进制数]Q、[八进制数]o、[八进制数]O。例如,777Q。 需要说明的是,这些方法是针对宏汇编器(例如,MASM、TASM、NASM)说的,调试器 默认使用十六进制表示整数,并且不需要特别的声明(例如,在调试器中直接用 FFFF 表示 十进制的 65535,用 10 表示十进制的 16)。 现在我们来写一小段汇编程序,修改 EAX、EBX、ECX、EDX 的数值。 我们假定程序执行之前,寄存器中的数值是全 0: X ? H L EAX 0000 00 00 EBX 0000 00 00 ECX 0000 00 00 EDX 0000 00 00 正如前面提到的,EAX 的高 16bit 是没有办法直接访问的,而 AX 对应它的低 16bit,AH、AL 分别对应 AX 的高、低 8bit。 mov eax, 012345678h mov ebx, 0abcdeffeh mov ecx, 1 mov edx, 2 ; 将 012345678h 送入 eax ; 将 0abcdeffeh 送入 ebx ; 将 000000001h 送入 ecx ; 将 000000002h 送入 edx 则执行上述程序段之后,寄存器的内容变为: X ? H L EAX 1234 56 78 EBX abcd ef fe ECX 0000 00 01 EDX 0000 00 02 那么,你已经了解了 mov 这个指令(mov 是 move 的缩写)的一种用法。它可以将数送到寄 存器中。我们来看看下面的代码: mov eax, ebx ; ebx 内容送入 eax mov ecx, edx ; edx 内容送入 ecx 则寄存器内容变为: X ? H L EAX abcd ef fe EBX abcd ef fe ECX 0000 00 02 EDX 0000 00 02 我们可以看到,“move”之后,数据依然保存在原来的寄存器中。不妨把 mov 指令理解为“送 入”,或“装入”。 练习题 把寄存器恢复成都为全 0 的状态,然后执行下面的代码: mov eax, 0a1234h mov bx, ax mov ah, bl mov al, bh ; 将 0a1234h 送入 eax ; 将 ax 的内容送入 bx ; 将 bl 内容送入 ah ; 将 bh 内容送入 al 思考:此时,EAX 的内容将是多少?[ 答案 八年级地理上册填图题岩土工程勘察试题省略号的作用及举例应急救援安全知识车间5s试题及答案 ] 下面我们将介绍一些指令。在介绍指令之前,我们约定: 使用 Intel 文档中的寄存器表示方式 reg32 32-bit 寄存器(表示 EAX、EBX 等) reg16 16-bit 寄存器(在 32 位处理器中,表示 AX、BX 等) reg8 8-bit 寄存器(表示 AL、BH 等) imm32 32-bit 立即数(可以理解为常数) imm16 16-bit 立即数 imm8 8-bit 立即数 在寄存器中载入另一寄存器,或立即数的值: mov reg32, (reg32 | imm8 | imm16 | imm32) mov reg32, (reg16 | imm8 | imm16) mov reg8, (reg8 | imm8) 例如,mov eax, 010h 表示,在 eax 中载入 00000010h。需要注意的是,如果你希望在寄存 器中装入 0,则有一种更快的方法,在后面我们将提到。 交换寄存器的内容: xchg reg32, reg32 xchg reg16, reg16 xchg reg8, reg8 例如,xchg ebx, ecx,则 ebx 与 ecx 的数值将被交换。由于系统提供了这个指令,因 此,采用其他方法交换时,速度将会较慢,并需要占用更多的存储空间,编程时要避免这种 情况,即,尽量利用系统提供的指令,因为多数情况下,这意味着更小、更快的代码,同时 也杜绝了错误(如果说 Intel 的 CPU 在交换寄存器内容的时候也会出错,那么它就不用卖 CPU 了。而对于你来说,检查一行代码的正确性也显然比检查更多代码的正确性要容易)刚 才的习题的程序用下面的代码将更有效: mov eax, 0a1234h mov bx, ax xchg ah, al ; 将 0a1234h 送入 eax ; 将 ax 内容送入 bx ; 交换 ah, al 的内容 递增或递减寄存器的值: inc reg(8,16,32) dec reg(8,16,32) 这两个指令往往用于循环中对指针的操作。需要说明的是,某些时候我们有更好的方法来处 理循环,例如使用 loop 指令,或 rep 前缀。这些将在后面的章节中介绍。 将寄存器的数值与另一寄存器,或立即数的值相加,并存回此寄存器: add reg32, reg32 / imm(8,16,32) add reg16, reg16 / imm(8,16) add reg8, reg8 / imm(8) 例如,add eax, edx,将 eax+edx 的值存入 eax。减法指令和加法类似,只是将 add 换成 sub。 需要说明的是,与高级语言不同,汇编语言中,如果要计算两数之和(差、积、商,或 一般地说,运算结果),那么必然有一个寄存器被用来保存结果。在 PASCAL 中,我们可以用 nA := nB + nC 来让 nA 保存 nB+nC 的结果,然而,汇编语言并不提供这种方法。如果你希 望保持寄存器中的结果,需要用另外的指令。这也从另一个侧面反映了“寄存器”这个名字 的意义。数据只是“寄存”在那里。如果你需要保存数据,那么需要将它放到内存或其他地 方。 类似的指令还有 and、or、xor(与,或,异或)等等。它们进行的是逻辑运算。 我们称 add、mov、sub、and 等称为为指令助记符(这么叫是因为它比机器语言容易记 忆,而起作用就是方便人记忆,某些资料中也称为指令、操作码、opcode[operation code] 等);后面的参数成为操作数,一个指令可以没有操作数,也可以有一两个操作数,通常有 一个操作数的指令,这个操作数就是它的操作对象;而两个参数的指令,前一个操作数一般 是保存操作结果的地方,而后一个是附加的参数。 我不打算在这份教程中用大量的篇幅介绍指令——很多人做得比我更好,而且指令本身 并不是重点,如果你学会了如何组织语句,那么只要稍加学习就能轻易掌握其他指令。更多 的指令可以参考 Intel 提供的资料。编写程序的时候,也可以参考一些在线参考手册。 Tech!Help 和 HelpPC 2.10 尽管已经很旧,但足以应付绝大多数需要。 聪明的读者也许已经发现,使用 sub eax, eax,或者 xor eax, eax,可以得到与 mov eax, 0 类似的效果。在高级语言中,你大概不会选择用 a=a-a 来给 a 赋值,因为测试会告诉你这 么做更慢,简直就是在自找麻烦,然而在汇编语言中,你会得到相反的结论,多数情况下, 以由快到慢的速度排列,这三条指令将是 xor eax, eax、sub eax, eax 和 mov eax, 0。 为什么呢?处理器在执行指令时,需要经过几个不同的阶段:取指、译码、取数、执行。 我们反复强调,寄存器是 CPU 的一部分。从寄存器取数,其速度很显然要比从内存中取数快。 那么,不难理解,xor eax, eax 要比 mov eax, 0 更快一些。 那么,为什么 a=a-a 通常要比 a=0 慢一些呢?这和编译器的优化有一定关系。多数编译器会 把 a=a-a 翻译成类似下面的代码(通常,高级语言通过 ebp 和偏移量来访问局部变量;程序 中,x 为 a 相对于本地堆的偏移量,在只包含一个 32-bit 整形变量的程序中,这个值通常 是 4): mov eax, dword ptr [ebp-x] sub eax, dword ptr [ebp-x] mov dword ptr [ebp-x],eax 而把 a=0 翻译成 mov dword ptr [ebp-x], 0 上面的翻译只是示意性的,略去了很多必要的步骤,如保护寄存器内容、恢复等等。如 果你对与编译程序的实现过程感兴趣,可以参考相应的书籍。多数编译器(特别是 C/C++编 译器,如 Microsoft Visual C++)都提供了从源代码到宏汇编语言程序的附加编译输出选 项。这种情况下,你可以很方便地了解编译程序执行的输出结果;如果编译程序没有提供这 样的功能也没有关系,调试器会让你看到编译器的编译结果。 如果你明确地知道编译器编译出的结果不是最优的,那就可以着手用汇编语言来重写那 段代码了。怎么确认是否应该用汇编语言重写呢? 使用汇编语言重写代码之前需要确认的几件事情 首先,这种优化最好有明显的效果。比如,一段循环中的计算,等等。一条语句的执行 时间是很短的,现在新的 CPU 的指令周期都在 0.000000001s 以下,Intel 甚至已经做出了 4GHz 主频(主频的倒数是时钟周期)的 CPU,如果你的代码自始至终只执行一次,并且你只 是减少了几个时钟周期的执行时间,那么改变将是无法让人察觉的;很多情况下,这种“优 化”并不被提倡,尽管它确实减少了执行时间,但为此需要付出大量的时间、人力,多数情 况下得不偿失(极端情况,比如你的设备内存价格非常昂贵的时候,这种优化也许会有意义)。 其次,确认你已经使用了最好的算法,并且,你优化的程序的实现是正确的。汇编语言能够 提供同样算法的最快实现,然而,它并不是万金油,更不是解决一切的灵丹妙药。用高级语 言实现一种好的算法,不一定会比汇编语言实现一种差的算法更慢。不过需要注意的是,时 间、空间复杂度最小的算法不一定就是解决某一特定问题的最佳算法。举例说,快速排序在 完全逆序的情况下等价于冒泡排序,这时其他方法就比它快。同时,用汇编语言优化一个不 正确的算法实现,将给调试带来很大的麻烦。最后,确认你已经将高级语言编译器的性能发 挥到极致。Microsoft 的编译器在 RELEASE 模式和 DEBUG 模式会有差异相当大的输出,而对 于 GNU 系列的编译器而言,不同级别的优化也会生成几乎完全不同的代码。此外,在编程时 对于问题的严格定义,可以极大地帮助编译器的优化过程。如何优化高级语言代码,使其编 译结果最优超出了本教程的范围,但如果你不能确认已经发挥了编译器的最大效能,用汇编 语言往往是一种更为费力的方法。 还有一点非常重要,那就是你明白自己做的是什么。好的高级语言编译器有时会有一些让人 难以理解的行为,比如,重新排列指令顺序等等。如果你发现这种情况,那么优化的时候就 应该小心——编译器很可能比你拥有更多的关于处理器的知识,例如,对于一个超标量处理 器,编译器会对指令序列进行“封包”,使他们尽可能的并行执行;此外,宏汇编器有时会 自动插入一些 nop 指令,其作用是将指令凑成整数字长(32-bit,对于 16-bit 处理器,是 16-bit)。这些都是提高代码性能的必要措施,如果你不了解处理器,那么最好不要改动编 译器生成的代码,因为这种情况下,盲目的修改往往不会得到预期的效果。 曾经在一份杂志上看到过有人用纯机器语言编写程序。不清楚到底这是不是编辑的失 误,因为一个头脑正常的人恐怕不会这么做程序,即使它不长、也不复杂。首先,汇编器能 够完成某些封包操作,即使不行,也可以用 db 伪指令来写指令;用汇编语言写程序可以防 止很多错误的发生,同时,它还减轻了人的负担,很显然,“完全用机器语言写程序”是完 全没有必要的,因为汇编语言可以做出完全一样的事情,并且你可以依赖它,因为计算机不 会出错,而人总有出错的时候。此外,如前面所言,如果用高级语言实现程序的代价不大(例 如,这段代码在程序的整个执行过程中只执行一遍,并且,这一遍的执行时间也小于一秒), 那么,为什么不用高级语言实现呢? 一些比较狂热的编程爱好者可能不太喜欢我的这种观点。比方说,他们可能希望精益求 精地优化每一字节的代码。但多数情况下我们有更重要的事情,例如,你的算法是最优的吗? 你已经把程序在高级语言许可的范围内优化到尽头了吗?并不是所有的人都有资格这样说。 汇编语言是这样一件东西,它足够的强大,能够控制计算机,完成它能够实现的任何功能; 同时,因为它的强大,也会提高开发成本,并且,难于维护。因此,我个人的建议是,如果 在软件开发中使用汇编语言,则应在软件接近完成的时候使用,这样可以减少很多不必要的 投入。 第二章中,我介绍了 x86 系列处理器的基本寄存器。这些寄存器对于 x86 兼容处理器仍 然是有效的,如果你偏爱 AMD 的 CPU,那么使用这些寄存器的程序同样也可以正常运行。 不过现在说用汇编语言进行优化还为时尚早——不可能写程序,而只操作这些寄存器, 因为这样只能完成非常简单的操作,既然是简单的操作,那可能就会让人觉得乏味,甚至找 一台足够快的机器穷举它的所有结果(如果可以穷举的话),并直接写程序调用,因为这样 通常会更快。但话说回来,看完接下来的两章——内存和堆栈操作,你就可以独立完成几乎 所有的任务了,配合第五章中断、第六章子程序的知识,你将知道如何驾驭处理器,并让它 为你工作。 第三章 操作内存 在前面的章节中,我们已经了解了寄存器的基本使用方法。而正如结尾提到的那样,仅 仅使用寄存器做一点运算是没有什么太大意义的,毕竟它们不能保存太多的数据,因此,对 编程人员而言,他肯定迫切地希望访问内存,以保存更多的数据。 我将分别介绍如何在保护模式和实模式操作内存,然而在此之前,我们先熟悉一下这两 种模式中内存的结构。 3.1 实模式 事实上,在实模式中,内存比保护模式中的结构更令人困惑。内存被分割成段,并且, 操作内存时,需要指定段和偏移量。不过,理解这些概念是非常容易的事情。请看下面的图: 段-寄存器这种格局是早期硬件电路限制留下的一个伤疤。地址总线在当时有 20-bit。 然而 20-bit 的地址不能放到 16-bit 的寄存器里,这意味着有 4-bit 必须放到别的地方。因 此,为了访问所有的内存,必须使用两个 16-bit 寄存器。 这一设计上的折衷 方案 气瓶 现场处置方案 .pdf气瓶 现场处置方案 .doc见习基地管理方案.doc关于群访事件的化解方案建筑工地扬尘治理专项方案下载 导致了今天的段-偏移量格局。最初的设计中,其中一个寄存器 只有 4-bit 有效,然而为了简化程序,两个寄存器都是 16-bit 有效,并在执行时求出加权 和来标识 20-bit 地址。 偏移量是 16-bit 的,因此,一个段是 64KB。下面的图可以帮助你理解 20-bit 地址是 如何形成的: 段-偏移量标识的地址通常记做 段:偏移量 的形式。 由于这样的结构,一个内存有多个对应的地址。例如,0000:0010 和 0001:0000 指的是同一 内存地址。又如, 0000:1234 = 0123:0004 = 0120:0034 = 0100:0234 0001:1234 = 0124:0004 = 0120:0044 = 0100:0244 作为负面影响之一,在段上加 1 相当于在偏移量上加 16,而不是一个“全新”的段。反之, 在偏移量上加 16 也和在段上加 1 等价。某些时候,据此认为段的“粒度”是 16 字节。 练习题 尝试一下将下面的地址转化为 20bit 的地址: 2EA8:D678 26CF:8D5F 453A:CFAD 2933:31A6 5924:DCCF 694E:175A 2B3C:D218 728F:6578 68E1:A7DC 57EC:AEEA 稍高一些的要求是,写一个程序将段为 AX、偏移量为 BX 的地址转换为 20bit 的地址,并保 存于 EAX 中。 [上面习题的答案] 我们现在可以写一个真正的程序了。 经典程序:Hello, world ;;; 应该得到一个 29 字节的.com 文件 .MODEL TINY .CODE CR equ 13 LF equ 10 TERMINATOR equ '$' ORG 100h Main PROC mov dx,offset sMessage mov ah,9 int 21h mov ax,4c00h int 21h Main ENDP sMessage: DB 'Hello, World!' DB CR,LF,TERMINATOR END Main ; .COM 文件的内存模型是‘TINY’ ; 代码段开始 ; 回车 ; 换行 ; DOS 字符串结束符 ; 代码起始地址为 CS:0100h ; 令 DS:DX 指向 Message ; int 21h(DOS 中断)功能 9 - ; 显示字符串到标准输出设备 ; int 21h 功能 4ch - ; 终止程序并返回 AL 的错误代码 ; 程序结束的同时指定入口点为 Main 那么,我们需要解释很多东西。 首先,作为汇编语言的抽象,C 语言拥有“指针”这个数据类型。在汇编语言中,几乎 所有对内存的操作都是由对给定地址的内存进行访问来完成的。这样,在汇编语言中,绝大 多数操作都要和指针产生或多或少的联系。 这里我想强调的是,由于这一特性,汇编语言中同样会出现 C程序中常见的缓冲区溢出 问题。如果你正在设计一个与安全有关的系统,那么最好是仔细检查你用到的每一个串,例 如,它们是否一定能够以你预期的方式结束,以及(如果使用的话)你的缓冲区是否能保证 实际可能输入的数据不被写入到它以外的地方。作为一个汇编语言程序员,你有义务检查每 一行代码的可用性。 程序中的 equ 伪指令是宏汇编特有的,它的意思接近于 C 或 Pascal 中的 const(常量)。 多数情况下,equ 伪指令并不为符号分配空间。 此外,汇编程序执行一项操作是非常繁琐的,通常,在对与效率要求不高的地方,我们 习惯使用系统提供的中断服务来完成任务。例如本例中的中断 21h,它是 DOS 时代的中断服 务,在 Windows 中,它也被认为是 Windows API 的一部分(这一点可以在 Microsoft 的文档 中查到)。中断可以被理解为高级语言中的子程序,但又不完全一样——中断使用系统栈来 保存当前的机器状态,可以由硬件发起,通过修改机器状态字来反馈信息,等等。 那么,最后一段通过 DB 存放的数据到底保存在哪里了呢?答案是紧挨着代码存放。在 汇编语言中,DB 和普通的指令的地位是相同的。如果你的汇编程序并不知道新的助记符(例 如,新的处理器上的 CPUID 指令),而你很清楚,那么可以用 DB 机器码的方式强行写下指 令。这意味着,你可以超越汇编器的能力撰写汇编程序,然而,直接用机器码编程是几乎肯 定是一件费力不讨好的事——汇编器厂商会经常更新它所支持的指令集以适应市场需要,而 且,你可以期待你的汇编其能够产生正确的代码,因为机器查表是不会出错的。既然机器能 够帮我们做将程序转换为代码这件事情,那么为什么不让它来做呢? 细心的读者不难发现,在程序中我们没有对 DS 进行赋值。那么,这是否意味着程序的 结果将是不可预测的呢?答案是否定的。DOS(或 Windows 中的 MS-DOS VM)在加载.com 文 件的时候,会对寄存器进行很多初始化。.com 文件被限制为小于 64KB,这样,它的代码段、 数据段都被装入同样的数值(即,初始状态下 DS=CS)。 也许会有人说,“嘿,这听起来不太好,一个 64KB 的程序能做得了什么呢?还有,你吹 得天花乱坠的堆栈段在什么地方?”那么,我们来看看下面这个新的 Hello world 程序,它 是一个 EXE 文件,在 DOS 实模式下运行。 ;;; 应该得到一个 561 字节的 EXE 文件 .MODEL SMALL .STACK 200h CR equ 13 LF equ 10 TERMINATOR equ '$' .DATA Message DB 'Hello, World !' DB CR,LF,TERMINATOR .CODE Main PROC mov ax, DGROUP mov ds, ax mov dx, offset Message mov ah, 9 int 21h mov ax, 4c00h ; 采用“SMALL”内存模型 ; 堆栈段 ; 回车 ; 换行 ; DOS 字符串结束符 ; 定义数据段 ; 定义显示串 ; 定义代码段 ; 将数据段 ; 加载到 DS 寄存器 ; 设置 DX ; 显示 ; 终止程序 int 21h Main ENDP END main 561 字节?实现相同功能的程序大了这么多!为什么呢?我们看到,程序拥有了完整的 堆栈段、数据段、代码段,其中堆栈段足足占掉了 512 字节,其余的基本上没什么变化。 分成多个段有什么好处呢?首先,它让程序显得更加清晰——你肯定更愿意看一个结构 清楚的程序,代码中 hard-coded 的字符串、数据让人觉得费解。比如,mov dx, 0152h 肯 定不如 mov dx, offset Message 来的亲切。此外,通过分段你可以使用更多的内存,比如, 代码段腾出的空间可以做更多的事情。exe 文件另一个吸引人的地方是它能够实现“重定 位”。现在你不需要指定程序入口点的地址了,因为系统会找到你的程序入口点,而不是死 板的 100h。 程序中的符号也会在系统加载的时候重新赋予新的地址。exe 程序能够保证你的设计容 易地被实现,不需要考虑太多的细节。 当然,我们的主要目的是将汇编语言作为高级语言的一个有用的补充。如我在开始提到 的那样,真正完全用汇编语言实现的程序不一定就好,因为它不便于维护,而且,由于结构 的原因,你也不太容易确保它是正确的;汇编语言是一种非结构化的语言,调试一个精心设 计的汇编语言程序,即使对于一个老手来说也不啻是一场恶梦,因为你很可能掉到别人预设 的“陷阱”中——这些技巧确实提高了代码性能,然而你很可能不理解它,于是你把它改掉, 接着就发现程序彻底败掉了。使用汇编语言加强高级语言程序时,你要做的通常只是使用汇 编指令,而不必搭建完整的汇编程序。绝大多数(也是目前我遇到的全部)C/C++编译器都 支持内嵌汇编,即在程序中使用汇编语言,而不必撰写单独的汇编语言程序——这可以节省 你的不少精力,因为前面讲述的那些伪指令,如 equ 等,都可以用你熟悉的高级语言方式来 编写,编译器会把它转换为适当的形式。 需要说明的是,在高级语言中一定要注意编译结果。编译器会对你的汇编程序做一些修 改,这不一定符合你的要求(附带说一句,有时编译器会很聪明地调整指令顺序来提高性能, 这种情况下最好测试一下哪种写法的效果更好),此时需要做一些更深入的修改,或者用 db 来强制编码。 3.2 保护模式 实模式的东西说得太多了,尽管我已经删掉了许多东西,并把一些原则性的问题拿到了 这一节讨论。这样做不是没有理由的——保护模式才是现在的程序(除了操作系统的底层启 动代码)最常用的 CPU 模式。保护模式提供了很多令人耳目一新的功能,包括内存保护(这 是保护模式这个名字的来源)、进程支持、更大的内存支持,等等。 对于一个编程人员来说,能“偷懒”是一件令人愉快的事情。这里“偷懒”是说把“应 该”由系统做的事情做的事情全都交给系统。为什么呢?这出自一个基本思想——人总有犯 错误的时候,然而规则不会,正确地了解规则之后,你可以期待它像你所了解的那样执行。 对于 C
本文档为【简明_X86_汇编语言教程】,请使用软件OFFICE或WPS软件打开。作品中的文字与图均可以修改和编辑, 图片更改请在作品中右键图片并更换,文字修改请直接点击文字进行修改,也可以新增和删除文档中的内容。
该文档来自用户分享,如有侵权行为请发邮件ishare@vip.sina.com联系网站客服,我们会及时删除。
[版权声明] 本站所有资料为用户分享产生,若发现您的权利被侵害,请联系客服邮件isharekefu@iask.cn,我们尽快处理。
本作品所展示的图片、画像、字体、音乐的版权可能需版权方额外授权,请谨慎使用。
网站提供的党政主题相关内容(国旗、国徽、党徽..)目的在于配合国家政策宣传,仅限个人学习分享使用,禁止用于任何广告和商用目的。
下载需要: 免费 已有0 人下载
最新资料
资料动态
专题动态
is_000906
暂无简介~
格式:pdf
大小:485KB
软件:PDF阅读器
页数:45
分类:互联网
上传时间:2013-12-21
浏览量:32