第一篇:[电脑书籍]语言简介与入门
我不想夸大或者贬低汇编语言。但我想说,汇编语言改变了20世纪的历史。与前辈相比,我们这一代编程人员足够的幸福,因为我们有各式各样的编程语言,我们可以操作键盘、坐在显示器面前,甚至使用鼠标、语音识别。我们可以使用键盘、鼠标来驾驭“个人计算机”,而不是和一群人共享一台使用笨重的继电器、开关去操作的巨型机。相比之下,我们的前辈不得不使用机器语言编写程序,他们甚至没有最简单的汇编程序来把助记符翻译成机器语言,而我们可以从上千种计算机语言中选择我们喜欢的一种,而汇编,虽然不是一种“常用”的具有“快速原型开发”能力的语言,却也是我们可以选择的语言中的一种。
每种计算机都有自己的汇编语言——没必要指望汇编语言的可移植性,选择汇编,意味着选择性能而不是可移植或便于调试。这份文档中讲述的是x86汇编语言,此后的“汇编语言”一词,如果不明示则表示ia32上的x86汇编语言。
汇编语言是一种易学,却很难精通的语言。回想当年,我从初学汇编到写出第一个可运行的程序,只用了不到4个小时;然而直到今天,我仍然不敢说自己精通它。编写快速、高效、并且能够让处理器“很舒服地执行”的程序是一件很困难的事情,如果利用业余时间学习,通常需要2-3年的时间才能做到。这份教材并不期待能够教给你大量的汇编语言技巧。对于读者来说,x86汇编语言“就在这里”。然而,不要僵化地局限于这份教材讲述的内容,因为它只能告诉你汇编语言是“这样一回事”。学好汇编语言,更多的要靠一个人的创造力于悟性,我可以告诉你我所知道的技巧,但肯定这是不够的。一位对我的编程生涯产生过重要影响的人曾经对我说过这么一句话:
写汇编语言程序不是汇编语言最难的部分,创新才是。
我想,愿意看这份文档的人恐怕不会问我“为什么要学习汇编语言”这样的问题;不过,我还是想说几句:首先,汇编语言非常有用,我个人主张把它作为C语言的先修课程,因为通过学习汇编语言,你可以了解到如何有效地设计数据结构,让计算机处理得更快,并使用更少的存储空间;同时,学习汇编语言可以让你熟悉计算机内部运行机制,并且,有效地提高调试能力。就我个人的经验而言,调试一个非结构化的程序的困难程度,要比调试一个结构化的程序的难度高很多,因为“结构化”是以牺牲运行效率来提高可读性与可调试性,这对于完成一般软件工程的编码阶段是非常必要的。然而,在一些地方,比如,硬件驱动程序、操作系统底层,或者程序中经常需要执行的代码,结构化程序设计的这些优点有时就会被它的低效率所抹煞。另外,如果你想真正地控制自己的程序,只知道源代码级的调试是远远不够的。
浮躁的人喜欢说,用C++写程序足够了,甚至说,他不仅仅掌握C++,而且精通STL、MFC。我不赞成这个观点,掌握上面的那些是每一个编程人员都应该做到的,然而C++只是我们“常用”的一种语言,它不是编程的全部。低层次的开发者喜欢说,嘿,C++是多么的强大,它可以做任何事情——这不是事实。便于维护、调试,这些确实是我们的追求目标,但是,写程序不能仅仅追求这个目标,因为我们最终的目的是满足设计需求,而不是个人非理性的理想。这份教材适合已经学习过某种结构化程序设计语言的读者。其内容基于我在1995年给别人讲述汇编语言时所写的讲义。当然,如大家所希望的,它包含了最新的处理器所支持的特性,以及相应的内容。我假定读者已经知道了程序设计的一些基本概念,因为没有这些是无法理解汇编语言程序设计的;此外,我希望读者已经有了比较良好的程序设计基础,因为如果你缺乏对于结构化程序设计的认识,编写汇编语言程序很可能很快就破坏了你的结构化编程习惯,大大降低程序的可读性、可维护性,最终让你的程序陷于不得不废弃的代码堆之中。基本上,这份文档撰写的目标是尽可能地便于自学。不过,它对你也有一些要求,尽管不是很高,但我还是强调一下。学习汇编语言,你需要
胆量。不要害怕去接触那些计算机的内部工作机制。知识。了解计算机常用的数制,特别是二进制、十六进制、八进制,以及计算机保存数据的方法。
开放。接受汇编语言与高级语言的差异,而不是去指责它如何的不好读。经验。要求你拥有任意其他编程语言的一点点编程经验。头脑。
祝您编程愉快!
先说一点和实际编程关系不太大的东西。当然,如果你迫切的想看到更实质的内容,完全可以先跳过这一章。
那么,我想可能有一个问题对于初学汇编的人来说非常重要,那就是: 汇编语言到底是什么?
汇编语言是一种最接近计算机核心的编码语言。不同于任何高级语言,汇编语言几乎可以完全和机器语言一一对应。不错,我们可以用机器语言写程序,但现在除了没有汇编程序的那些电脑之外,直接用机器语言写超过1000条以上指令的人大概只能算作那些被我们成为“圣人”的牺牲者一类了。毕竟,记忆一些短小的助记符、由机器去考虑那些琐碎的配位过程和检查错误,比记忆大量的随计算机而改变的十六进制代码、可能弄错而没有任何提示要强的多。熟练的汇编语言编码员甚至可以直接从十六进制代码中读出汇编语言的大致意思。当然,我们有更好的工具——汇编器和反汇编器。
简单地说,汇编语言就是机器语言的一种可以被人读懂的形式,只不过它更容易记忆。至于宏汇编,则是包含了宏支持的汇编语言,这可以让你编程的时候更专注于程序本身,而不是忙于计算和重写代码。
汇编语言除了机器语言之外最接近计算机硬件的编程语言。由于它如此的接近计算机硬件,因此,它可以最大限度地发挥计算机硬件的性能。用汇编语言编写的程序的速度通常要比高级语言和C/C++快很多--几倍,几十倍,甚至成百上千倍。当然,解释语言,如解释型LISP,没有采用JIT技术的Java虚机中运行的Java等等,其程序速度更无法与汇编语言程序同日而语。
永远不要忽视汇编语言的高速。实际的应用系统中,我们往往会用汇编彻底重写某些经常调用的部分以期获得更高的性能。应用汇编也许不能提高你的程序的稳定性,但至少,如果你非常小心的话,它也不会降低稳定性;与此同时,它可以大大地提高程序的运行速度。我强烈建议所有的软件产品在最后Release之前对整个代码进行Profile,并适当地用汇编取代部分高级语言代码。至少,汇编语言的知识可以告诉你一些有用的东西,比如,你有多少个寄存器可以用。有时,手工的优化比编译器的优化更为有效,而且,你可以完全控制程序的实际行为。
我想我在罗嗦了。总之,在我们结束这一章之前,我想说,不要在优化的时候把希望完全寄托在编译器上——现实一些,再好的编译器也不可能总是产生最优的代码。
当时我学过BASIC, Fortran和Pascal,写的是一个
对一个包含100个32bit整数的数组进行快速排序,并且输出出来的小程序。实际上用汇编器写出的机器码与在调试器中用它附带的汇编程序写出的机器码还是有一些细微差别的,前者更大,然而却可能更高效,因为汇编器能够将代码放置到适合处理器的地方这句话假定两个程序进行了同等程度的优化,一个写的不好的汇编程序和一个写的很好的C程序相比,汇编程序不一定更快。
中央处理器(CPU)在微机系统处于“领导核心”的地位。汇编语言被编译成机器语言之后,将由处理器来执行。那么,首先让我们来了解一下处理器的主要作用,这将帮助你更好地驾驭它。
典型的处理器的主要任务包括 从内存中获取机器语言指令,译码,执行
根据指令代码管理它自己的寄存器
根据指令或自己的的需要修改内存的内容 响应其他硬件的中断请求
一般说来,处理器拥有对整个系统的所有总线的控制权。对于Intel平台而言,处理器拥有对数据、内存和控制总线的控制权,根据指令控制整个计算机的运行。在以后的章节中,我们还将讨论系统中同时存在多个处理器的情况。
处理器中有一些寄存器,这些寄存器可以保存特定长度的数据。某些寄存器中保存的数据对于系统的运行有特殊的意义。
新的处理器往往拥有更多、具有更大字长的寄存器,提供更灵活的取指、寻址方式。寄存器
如前所述,处理器中有一些可以保存数据的地方被称作寄存器。
寄存器可以被装入数据,你也可以在不同的寄存器之间移动这些数据,或者做类似的事情。基本上,像四则运算、位运算等这些计算操作,都主要是针对寄存器进行的。
首先让我来介绍一下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 32-bit宽
通常在内存操作指令中作为“目的地址指针”使用。当然,EDI也可以被装入任意的数值,但通常没有人把它当作通用寄存器来用。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 堆栈段或堆栈选择器。这个寄存器的低16 bit连同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? 寄存器(测试寄存器)用于某些条件测试。
最后我们要说的是一个在程序设计中起着非常关键的作用的寄存器:标志寄存器。本节中部份表格来自David Jurgens的HelpPC 2.10快速参考手册。在此谨表谢意。先说一点和实际编程关系不太大的东西。当然,如果你迫切的想看到更实质的内容,完全可以先跳过这一章。
那么,我想可能有一个问题对于初学汇编的人来说非常重要,那就是: 汇编语言到底是什么?
汇编语言是一种最接近计算机核心的编码语言。不同于任何高级语言,汇编语言几乎可以完全和机器语言一一对应。不错,我们可以用机器语言写程序,但现在除了没有汇编程序的那些电脑之外,直接用机器语言写超过1000条以上指令的人大概只能算作那些被我们成为“圣人”的牺牲者一类了。毕竟,记忆一些短小的助记符、由机器去考虑那些琐碎的配位过程和检查错误,比记忆大量的随计算机而改变的十六进制代码、可能弄错而没有任何提示要强的多。熟练的汇编语言编码员甚至可以直接从十六进制代码中读出汇编语言的大致意思。当然,我们有更好的工具——汇编器和反汇编器。
简单地说,汇编语言就是机器语言的一种可以被人读懂的形式,只不过它更容易记忆。至于宏汇编,则是包含了宏支持的汇编语言,这可以让你编程的时候更专注于程序本身,而不是忙于计算和重写代码。
汇编语言除了机器语言之外最接近计算机硬件的编程语言。由于它如此的接近计算机硬件,因此,它可以最大限度地发挥计算机硬件的性能。用汇编语言编写的程序的速度通常要比高级语言和C/C++快很多--几倍,几十倍,甚至成百上千倍。当然,解释语言,如解释型LISP,没有采用JIT技术的Java虚机中运行的Java等等,其程序速度更无法与汇编语言程序同日而语。
永远不要忽视汇编语言的高速。实际的应用系统中,我们往往会用汇编彻底重写某些经常调用的部分以期获得更高的性能。应用汇编也许不能提高你的程序的稳定性,但至少,如果你非常小心的话,它也不会降低稳定性;与此同时,它可以大大地提高程序的运行速度。我强烈建议所有的软件产品在最后Release之前对整个代码进行Profile,并适当地用汇编取代部分高级语言代码。至少,汇编语言的知识可以告诉你一些有用的东西,比如,你有多少个寄存器可以用。有时,手工的优化比编译器的优化更为有效,而且,你可以完全控制程序的实际行为。
我想我在罗嗦了。总之,在我们结束这一章之前,我想说,不要在优化的时候把希望完全寄托在编译器上——现实一些,再好的编译器也不可能总是产生最优的代码。
当时我学过BASIC, Fortran和Pascal,写的是一个
对一个包含100个32bit整数的数组进行快速排序,并且输出出来的小程序。实际上用汇编器写出的机器码与在调试器中用它附带的汇编程序写出的机器码还是有一些细微差别的,前者更大,然而却可能更高效,因为汇编器能够将代码放置到适合处理器的地方这句话假定两个程序进行了同等程度的优化,一个写的不好的汇编程序和一个写的很好的C程序相比,汇编程序不一定更快。
在前一节中的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 mov ecx, edx;ebx内容送入eax;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的内容将是多少?[答案]
下面我们将介绍一些指令。在介绍指令之前,我们约定:
使用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种,这一内容超过了 这份教程的范围,如果您感兴趣,可以参考数字逻 辑电路的相关书籍)状态.对于电路而言,这表现 为高、低电平,或者开、关,分别非常明显,因而 工作比较稳定;另一方面,由于只有两种状态,设 计起来也比较简单。这样,使用二进制意味着低成 本、稳定,多数情况下,这也意味着快速。
与十进制类似,我们可以用下面的式子来换算出一 个任意形如am-1„„a3a2a1a0 的m位r进制数对应的 数值n:
程序设计中常用十六进制和八进制数字代替二进制 数,其原因在于,16和8是2的整次方幂,这样,一 位十六或八进制数可以表示整数个二进制位。十六 进制中,使用字母A、B、C、D、E、F表示10-15,而十六进制或八进制数制表示的的数字比二进制数 更短一些。
EAX的内容为000A3412h.在前面的章节中,我们已经了解了寄存器的基本使用方法。而正如结尾提到的那样,仅仅使用寄存器做一点运算是没有什么太大意义的,毕竟它们不能保存太多的数据,因此,对编程人员而言,他肯定迫切地希望访问内存,以保存更多的数据。
我将分别介绍如何在保护模式和实模式操作内存,然而在此之前,我们先熟悉一下这两种模式中内存的结构。3.1 实模式
事实上,在实模式中,内存比保护模式中的结构更令人困惑。内存被分割成段,并且,操作内存时,需要指定段和偏移量。不过,理解这些概念是非常容易的事情。请看下面的图:
段寄存器这种格局是早期硬件电路限制留下的一个伤疤。地址总线在当时有20-bit。
然而20-bit的地址不能放到16-bit的寄存器里,这意味着有4-bit必须放到别的地方。因此,为了访问所有的内存,必须使用两个16-bit寄存器。
这一设计上的折衷方案导致了今天的段-偏移量格局。最初的设计中,其中一个寄存器只有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;终止程序并返回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 int 21h Main ENDP END main
;采用“SMALL”内存模型;堆栈段
;回车
;换行
;DOS字符串结束符
;定义数据段
;定义显示串
;定义代码段
;将数据段;加载到DS寄存器
;设置DX;显示
;终止程序
561字节?实现相同功能的程序大了这么多!为什么呢?我们看到,程序拥有了完整的堆栈段、数据段、代码段,其中堆栈段足足占掉了512字节,其余的基本上没什么变化。
分成多个段有什么好处呢?首先,它让程序显得更加清晰——你肯定更愿意看一个结构清楚的程序,代码中hard-coded的字符串、数据让人觉得费解。比如,mov dx, 0152h肯定不如mov dx, offset Message来的亲切。此外,通过分段你可以使用更多的内存,比如,代码段腾出的空间可以做更多的事情。exe文件另一个吸引人的地方是它能够实现“重定位”。现在你不需要指定程序入口点的地址了,因为系统会找到你的程序入口点,而不是死板的100h。
程序中的符号也会在系统加载的时候重新赋予新的地址。exe程序能够保证你的设计容易地被实现,不需要考虑太多的细节。
当然,我们的主要目的是将汇编语言作为高级语言的一个有用的补充。如我在开始提到的那样,真正完全用汇编语言实现的程序不一定就好,因为它不便于维护,而且,由于结构的原因,你也不太容易确保它是正确的;汇编语言是一种非结构化的语言,调试一个精心设计的汇编语言程序,即使对于一个老手来说也不啻是一场恶梦,因为你很可能掉到别人预设的“陷阱”中——这些技巧确实提高了代码性能,然而你很可能不理解它,于是你把它改掉,接着就发现程序彻底败掉了。使用汇编语言加强高级语言程序时,你要做的通常只是使用汇编指令,而不必搭建完整的汇编程序。绝大多数(也是目前我遇到的全部)C/C++编译器都支持内嵌汇编,即在程序中使用汇编语言,而不必撰写单独的汇编语言程序——这可以节省你的不少精力,因为前面讲述的那些伪指令,如equ等,都可以用你熟悉的高级语言方式来编写,编译器会把它转换为适当的形式。
需要说明的是,在高级语言中一定要注意编译结果。编译器会对你的汇编程序做一些修改,这不一定符合你的要求(附带说一句,有时编译器会很聪明地调整指令顺序来提高性能,这种情况下最好测试一下哪种写法的效果更好),此时需要做一些更深入的修改,或者用db来强制编码。
3.2 保护模式
实模式的东西说得太多了,尽管我已经删掉了许多东西,并把一些原则性的问题拿到了这一节讨论。这样做不是没有理由的——保护模式才是现在的程序(除了操作系统的底层启动代码)最常用的CPU模式。保护模式提供了很多令人耳目一新的功能,包括内存保护(这是保护模式这个名字的来源)、进程支持、更大的内存支持,等等。
对于一个编程人员来说,能“偷懒”是一件令人愉快的事情。这里“偷懒”是说把“应该”由系统做的事情做的事情全都交给系统。为什么呢?这出自一个基本思想——人总有犯错误的时候,然而规则不会,正确地了解规则之后,你可以期待它像你所了解的那样执行。对于C程序来说,你自己用C语言写的实现相同功能的函数通常没有系统提供的函数性能好(除非你用了比函数库好很多的算法),因为系统的函数往往使用了更好的优化,甚至可能不是用C语言直接编写的。
当然,“偷懒”的意思是说,把那些应该让机器做的事情交给计算机来做,因为它做得更好。我们应该把精力集中到设计算法,而不是编写源代码本身上,因为编译器几乎只能做等价优化,而实现相同功能,但使用更好算法的程序实现,则几乎只能由人自己完成。
举个例子,这样一个函数:
int fun(){ int a=0;register int i;for(i=0;i<1000;i++)a+=i;return a;}
在某种编译模式[DEBUG]下被编译为
push ebp mov ebp,esp sub esp,48h push ebx push esi push edi lea edi,[ebp-48h] mov ecx,12h mov eax,0CCCCCCCCh rep stos dword ptr [edi] mov dword ptr [ebp-4],0 mov dword ptr [ebp-8],0 jmp fun+31h mov eax,dword ptr [ebp-8] add eax,1 mov dword ptr [ebp-8],eax cmp dword ptr [ebp-8],3E8h jge fun+45h mov ecx,dword ptr [ebp-4] add ecx,dword ptr [ebp-8] mov dword ptr [ebp-4],ecx jmp fun+28h mov eax,dword ptr [ebp-4] pop edi pop esi pop ebx mov esp,ebp pop ebp ret;子程序入口
;保护现场
;初始化变量-调试版本特有。
;本质是在堆中挖一块地儿,存CCCCCCCC。;用串操作进行,这将发挥Intel处理器优势;‘a=0’;‘i=0’
;走着;i++
;i<1000?
;a+=i;
;return a;
;恢复现场
;返回
而在另一种模式[RELEASE/MINSIZE]下却被编译为
xor eax,eax xor ecx,ecx add eax,ecx inc ecx cmp ecx,3E8h jl fun+4 ret;a=0;;i=0;;a+=i;;i++;;i<1000?;是->继续继续;return a
如果让我来写,多半会写成
mov eax, 079f2ch ret;return 499500
为什么这样写呢?我们看到,i是一个外界不能影响、也无法获知的内部状态量。作为这段程序来说,对它的计算对于结果并没有直接的影响——它的存在不过是方便算法描述而已。并且我们看到的,这段程序实际上无论执行多少次,其结果都不会发生变化,因此,直接返回计算结果就可以了,计算是多余的(如果说一定要算,那么应该是编译器在编译过程中完成它)。
更进一步,我们甚至希望编译器能够直接把这个函数变成一个符号常量,这样连操作堆栈的过程也省掉了。
第三种结果属于“等效”代码,而不是“等价”代码。作为用户,很多时候是希望编译器这样做的,然而由于目前的技术尚不成熟,有时这种做法会造成一些问题(gcc和g++的顶级优化可以造成编译出的FreeBSD内核行为异常,这是我在FreeBSD上遇到的唯一一次软件原因的kernel panic),因此,并不是所有的编译器都这样做(另一方面的原因是,如果编译器在这方面做的太过火,例如自动求解全部“固定”问题,那么如果你的程序是解决固定的问题“很大”,如求解迷宫,那么在编译过程中你就会找锤子来砸计算机了)。然而,作为编译器制造商,为了提高自己的产品的竞争力,往往会使用第三种代码来做函数库。正如前面所提到的那样,这种优化往往不是编译器本身的作用,尽管现代编译程序拥有编译执行、循环代码外提、无用代码去除等诸多优化功能,但它都不能保证程序最优。最后一种代码恐怕很少有编译器能够做到,不信你可以用自己常用的编译器加上各种优化选项试试:)
发现什么了吗?三种代码中,对于内存的访问一个比一个少。这样做的理由是,尽可能地利用寄存器并减少对内存的访问,可以提高代码性能。在某些情况下,使代码既小又快是可能的。
书归正传,我们来说说保护模式的内存模型。保护模式的内存和实模式有很多共同之处。
毫无疑问,以'protected mode'(保护模式), 'global descriptor table'(全局描述符表), 'local descriptor table'(本地描述符表)和'selector'(选择器)搜索,你会得到完整介绍它们的大量信息。
保护模式与实模式的内存类似,然而,它们之间最大的区别就是保护模式的内存是“线性”的。
新的计算机上,32-bit的寄存器已经不是什么新鲜事(如果你哪天听说你的CPU的寄存器不是32-bit的,那么它——简直可以肯定地说——的字长要比32-bit还要多。新的个人机上已经开始逐步采用64-bit的CPU了),换言之,实际上段/偏移量这一格局已经不再需要了。尽管如此,在继续看保护模式内存结构时,仍请记住段/偏移量的概念。不妨把段寄存器看作对于保护模式中的选择器的一个模拟。选择器是全局描述符表(Global Descriptor Table, GDT)或本地描述符表(Local Descriptor Table, LDT)的一个指针。
如图所示,GDT和LDT的每一个项目都描述一块内存。例如,一个项目中包含了某块被描述的内存的物理的基地址、长度,以及其他一些相关信息。
保护模式是一个非常重要的概念,同时也是目前撰写应用程序时,最常用的CPU模式(运行在新的计算机上的操作系统很少有在实模式下运行的)。
为什么叫保护模式呢?它“保护”了什么?答案是进程的内存。保护模式的主要目的在于允许多个进程同时运行,并保护它们的内存不受其他进程的侵犯。这有点类似于C++中的机制,然而它的强制力要大得多。如果你的进程在保护模式下以不恰当的方式访问了内存(例如,写了“只读”内存,或读了不可读的内存,等等),那么CPU就会产生一个异常。这个异常将交给操作系统处理,而这种处理,假如你的程序没有特别说明操作系统该如何处理的话,一般就是杀掉做错了事情的进程。
我像这样的对话框大家一定非常熟悉(临时写了一个程序故意造成的错误):
好的,只是一个程序崩溃了,而操作系统的其他进程照常运行(同样的程序在DOS中几乎是板上钉钉的死机,因为NULL指针的位置恰好是中断向量表),你甚至还可以调试它。
保护模式还有其他很多好处,在此就不一一赘述了。实模式和保护模式之间的切换问题我打算放在后面的“高级技巧”一章来讲,因为多数程序并不涉及这个。
了解了内存的格局,我们就可以进入下一节——操作内存了。
3.3 操作内存
前两节中,我们介绍了实模式和保护模式中使用的不同的内存格局。现在开始解释如何使用这些知识。
回忆一下前面我们说过的,寄存器可以用作内存指针。现在,是他们发挥作用的时候了。
可以将内存想象为一个顺序的字节流。使用指针,可以任意地操作(读写)内存。
现在我们需要一些其他的指令格式来描述对于内存的操作。操作内存时,首先需要的就是它的地址。
让我们来看看下面的代码:
mov ax,[0]
方括号表示,里面的表达式指定的不是立即数,而是偏移量。在实模式中,DS:0中的那个字(16-bit长)将被装入AX。
然而0是一个常数,如果需要在运行的时候加以改变,就需要一些特殊的技巧,比如程序自修改。汇编支持这个特性,然而我个人并不推荐这种方法——自修改大大降低程序的可读性,并且还降低稳定性,性能还不一定好。我们需要另外的技术。
mov bx,0 mov ax,[bx]
看起来舒服了一些,不是吗?BX寄存器的内容可以随时更改,而不需要用冗长的代码去修改自身,更不用担心由此带来的不稳定问题。
同样的,mov指令也可以把数据保存到内存中:
mov [0],ax
在存储器与寄存器之间交换数据应该足够清楚了。
有些时候我们会需要操作符来描述内存数据的宽度: 操作符 意义
byte ptr 一个字节(8-bit, 1 byte)word ptr 一个字(16-bit)dword ptr 一个双字(32-bit)
例如,在DS:100h处保存1234h,以字存放:
mov word ptr [100h],01234h
于是我们将mov指令扩展为:
mov reg(8,16,32), mem(8,16,32)mov mem(8,16,32), reg(8,16,32)mov mem(8,16,32), imm(8,16,32)
需要说明的是,加减同样也可以在[]中使用,例如:
mov ax,[bx+10] mov ax,[bx+si] mov ax,es:[di+bp]
等等。我们看到,对于内存的操作,即使使用MOV指令,也有许多种可能的方式。下一节中,我们将介绍如何操作串。
感谢 网友 水杉 指出此答案中的一处错误。感谢 Heallven 指出.COM程序实例编译失败的问题
我们前面已经提到,内存可以和寄存器交换数据,也可以被赋予立即数。问题是,如果我们需要把内存的某部分内容复制到另一个地址,又怎么做呢?
设想将DS:SI处的连续512字节内容复制到ES:DI(先不考虑可能的重叠)。也许会有人写出这样的代码:
NextByte: mov cx,512 mov al,ds:[si] mov es:[di],al inc si inc di loop NextByte;循环次数
我不喜欢上面的代码。它的确能达到作用,但是,效率不好。如果你是在做优化,那么写出这样的代码意味着赔了夫人又折兵。
Intel的CPU的强项是串操作。所谓串操作就是由CPU去完成某一数量的、重复的内存操作。需要说明的是,我们常用的KMP算法(用于匹配字符串中的模式)的改进——Boyer算法,由于没有利用串操作,因此在Intel的CPU上的效率并非最优。好的编译器往往可以利用Intel CPU的这一特性优化代码,然而,并非所有的时候它都能产生最好的代码。某些指令可以加上REP前缀(repeat, 反复之意),这些指令通常被叫做串操作指令。举例来说,STOSD指令将EAX的内容保存到ES:DI,同时在DI上加或减四。类似的,STOSB和STOSW分别作1字节或1字的上述操作,在DI上加或减的数是1或2。
计算机语言通常是不允许二义性的。为什么我要说“加或减”呢?没错,孤立地看STOS?指令,并不能知道到底是加还是减,因为这取决于“方向”标志(DF, Direction Flag)。如果DF被复位,则加;反之则减。
置位、复位的指令分别是STD和CLD。
当然,REP只是几种可用前缀之一。常用的还包括REPNE,这个前缀通常被用来比较两个串,或搜索某个特定字符(字、双字)。REPZ、REPE、REPNZ也是非常常用的指令前缀,分别代表ZF(Zero Flag)在不同状态时重复执行。下面说三个可以复制数据的指令: 助记符 意义
movsb 将DS:SI的一字节复制到ES:DI,之后SI++、DI++ movsw 将DS:SI的一字节复制到ES:DI,之后SI+=
2、DI+=2 movsd 将DS:SI的一字节复制到ES:DI,之后SI+=
4、DI+=4 于是上面的程序改写为 cld mov cx, 128 rep movsd;复位DF;512/4 = 128,共128个双字;行动!
第一句cld很多时候是多余的,因为实际写程序时,很少会出现置DF的情况。不过在正式决定删掉它之前,建议你仔细地调试自己的程序,并确认每一个能够走到这里的路径中都不会将DF置位。
错误(非预期的)的DF是危险的。它很可能断送掉你的程序,因为这直接造成缓冲区溢出问题。
什么是缓冲区溢出呢?缓冲区溢出分为两类,一类是写入缓冲区以外的内容,一类是读取缓冲区以外的内容。后一种往往更隐蔽,但随便哪一个都有可能断送掉你的程序。
缓冲区溢出对于一个网络服务来说很可能更加危险。怀有恶意的用户能够利用它执行自己希望的指令。服务通常拥有更高的特权,而这很可能会造成特权提升;即使不能提升攻击者拥有的特权,他也可以利用这种问题使服务崩溃,从而形成一次成功的DoS(拒绝服务)攻击。每年CERT的安全公告中,都有6成左右的问题是由于缓冲区溢出造成的。
在使用汇编语言,或C语言编写程序时,很容易在无意中引入缓冲区溢出。然而并不是所有的语言都会引入缓冲区溢出问题,Java和C#,由于没有指针,并且缓冲区采取动态分配的方式,有效地消除了造成缓冲区溢出的土壤。
汇编语言中,由于REP*前缀都用CX作为计数器,因此情况会好一些(当然,有时也会更糟糕,因为由于CX的限制,很可能使原本可能改变程序行为的缓冲区溢出的范围缩小,从而更为隐蔽)。避免缓冲区溢出的一个主要方法就是仔细检查,这包括两方面:设置合理的缓冲区大小,和根据大小编写程序。除此之外,非常重要的一点就是,在汇编语言这个级别写程序,你肯定希望去掉所有的无用指令,然而再去掉之前,一定要进行严格的测试;更进一步,如果能加上注释,并通过善用宏来做调试模式检查,往往能够达到更好的效果。
正如3.2节提到到的那样,保护模式中,你可以使用32位的线性地址,这意味着直接访问4GB的内存。由于这个原因,选择器不用像实模式中段寄存器那样频繁地修改。顺便提一句,这份教程中所说的保护模式指的是386以上的保护模式,或者,Microsoft通常称为“增强模式”的那种。
在为选择器装入数值的时候一定要非常小心。错误的数值往往会导致无效页面错误(在Windows中经常出现:)。同时,也不要忘记你的地址是32位的,这也是保护模式的主要优势之一。
现在假设存在一个描述符描述从物理的0:0开始的全部内存,并已经加载进DS(数据选择器),则我们可以通过下面的程序来操作VGA的VRAM: mov edi,0a0000h mov byte ptr [edi],0fh;VGA显存的偏移量;将第一字节改为0fh 很明显,这比实模式下的程序 mov ax,0a000h mov ds,ax mov di,0 mov [di],0fh;AX-> VGA段地址;将AX值载入DS;DI清零
;修改第一字节 看上去要舒服一些。
到目前为止,您已经了解了基本的寄存器以及内存的操作知识。事实上,您现在已经可以写出很多的底层数据处理程序了。
下面我来说说堆栈。堆栈实在不是一个让人陌生的数据结构,它是一个先进后出(FILO)的线性表,能够帮助你完成很多很好的工作。
先进后出(FILO)是这样一个概念:最后放进表中 的数据在取出时最先出来。先进后出(FILO)和先 进先出(FIFO, 和先进后出的规则相反),以及随 机存取是最主要的三种存储器访问方式。
对于堆栈而言,最后放入的数据在取出时最先出
现。对于子程序调用,特别是递归调用来说,这 是一个非常有用的特性。
一个铁杆的汇编语言程序员有时会发现系统提供的寄存器不够。很显然,你可以使用普通的内存操作来完成这个工作,就像C/C++中所做的那样。
没错,没错,可是,如果数据段(数据选择器)以及偏移量发生变化怎么办?更进一步,如果希望保存某些在这种操作中可能受到影响的寄存器的时候怎么办?确实,你可以把他们也存到自己的那片内存中,自己实现堆栈。
太麻烦了„„
既然系统提供了堆栈,并且性能比自己写一份更好,那么为什么不直接加以利用呢 系统堆栈不仅仅是一段内存。由于CPU对它实施管理,因此你不需要考虑堆栈指针的修正问题。可以把寄存器内容,甚至一个立即数直接放到堆栈里,并在需要的时候将其取出。同时,系统并不要求取出的数据仍然回到原来的位置。
除了显式地操作堆栈(使用PUSH和POP指令)之外,很多指令也需要使用堆栈,如INT、CALL、LEAVE、RET、RETF、IRET等等。配对使用上述指令并不会造成什么问题,然而,如果你打算使用LEAVE、RET、RETF、IRET这样的指令实现跳转(比JMP更为麻烦,然而有时,例如在加密软件中,或者需要修改调用者状态时,这是必要的)的话,那么我的建议是,先搞清楚它们做的到底是什么,并且,精确地了解自己要做什么。正如前面所说的,有两个显式地操作堆栈的指令:
助记符 功能
PUSH 将操作数存入堆栈,同时修正堆栈指针
POP 将栈顶内容取出并存到目的操作数中,同时修正堆栈指针 我们现在来看看堆栈的操作。执行之前 执行代码 mov ax,1234h mov bx,10 push ax push bx 之后,堆栈的状态为 之后,再执行 pop dx pop cx 堆栈的状态成为
当然,dx、cx中的内容将分别是000ah和1234h。
注意,最后这张图中,我没有抹去1234h和000ah,因为POP指令并不从内存中抹去数值。不过尽管如此,我个人仍然非常反对继续使用这两个数(你可以通过修改SP来再次POP它们),然而这很容易导致错误。
一定要保证堆栈段有足够的空间来执行中断,以及其他一些隐式的堆栈操作。仅仅统计PUSH的数量并据此计算堆栈所需的大小很可能造成问题。CALL指令将返回地址放到堆栈中。绝大多数C/C++编译器提供了“堆栈检查”这个编译选项,其作用在于保证C程序段中没有忘记对堆栈中多余的数据进行清理,从而保证返回地址有效。本章小结
本章中介绍了内存的操作的一些入门知识。限于篇幅,我不打算展开细讲指令,如cmps*,lods*,stos*,等等。这些指令的用法和前面介绍的movs*基本一样,只是有不同的作用而已。
4.0 利用子程序与中断
已经掌握了汇编语言?没错,你现在已经可以去破译别人代码中的秘密。然而,我们还有一件重要的东西没有提到,那就是自程序和中断。这两件东西是如此的重要,以至于你的程序几乎不可能离开它们。
4.1 子程序
在高级语言中我们经常要用到子程序。高级语言中,子程序是如此的神奇,我们能够定义和主程序,或其他子程序一样的变量名,而访问不同的变量,并且,还不和程序的其他部分相冲突。
然而遗憾的是,这种“优势”在汇编语言中是不存在的。
汇编语言并不注重如何减轻程序员的负担;相反,汇编语言依赖程序员的良好设计,以期发挥CPU的最佳性能。汇编语言不是结构化的语言,因此,它不提供直接的“局部变量”。如果需要“局部变量”,只能通过堆或栈自行实现。
从这个意义上讲,汇编语言的子程序更像GWBASIC中的GOSUB调用的那些“子程序”。所有的“变量”(本质上,属于进程的内存和寄存器)为整个程序所共享,高级语言编译器所做的,将局部变量放到堆或栈中的操作,只能自行实现。
参数的传递是靠寄存器和堆栈来完成的。高级语言中,子程序(函数、过程,或类似概念的东西)依赖于堆和栈来传递。
让我们来简单地分析一下一般高级语言的子程序的执行过程。无论C、C++、BASIC、Pascal,这一部分基本都是一致的。
调用者将子程序执行完成时应返回的地址、参数压入堆栈
子程序使用BP指针+偏移量对栈中的参数寻址,并取出、完成操作
子程序使用RET或RETF指令返回。此时,CPU将IP置为堆栈中保存的地址,并继续予以执行
毋庸置疑,堆栈在整个过程中发挥着非常重要的作用。不过,本质上对子程序最重要的还是返回地址。如果子程序不知道这个地址,那么系统将会崩溃。
调用子程序的指令是CALL,对应的返回指令是RET。此外,还有一组指令,即ENTER和LEAVE,它们可以帮助进行堆栈的维护。
CALL指令的参数是被调用子程序的地址。使用宏汇编的时候,这通常是一个标号。CALL和RET,以及ENTER和LEAVE配对,可以实现对于堆栈的自动操作,而不需要程序员进行PUSH/POP,以及跳转的操作,从而提高了效率。
作为一个编译器的实现实例,我用Visual C++编译了一段C++程序代码,这段汇编代码是使用特定的编译选项得到的结果,正常的RELEASE代码会比它精简得多。包含源代码的部分反汇编结果如下(取自Visual C++调试器的运行结果,我删除了10条int 3指令,并加上了一些注释,除此之外,没有做任何修改): 1: int myTransform(int nInput){ 00401000 push ebp;保护现场原先的EBP指针 00401001 mov ebp,esp 2: return(nInput*2 + 3)% 7;00401003 mov eax,dword ptr [nInput];取参数
00401006 lea eax,[eax+eax+3];LEA比ADD加法更快
0040100A cdq;DWORD->QWORD(扩展字长)0040100B mov ecx,7;除数 00401010 idiv eax,ecx;除
00401012 mov eax,edx;商->eax(eax中保存返回值)3: } 00401014 pop ebp;恢复现场的ebp指针
00401015 ret;返回
;此处删除10条int 3指令,它们是方便调试用的,并不影响程序行为。4: 5: int main(int argc, char* argv[])6: { 00401020 push ebp;保护现场原先的EBP指针 00401021 mov ebp,esp 00401023 sub esp,10h;为取argc, argv修正堆栈指针。7: int a[3];8: for(register int i=0;i<3;i++){ 00401026 mov dword ptr [i],0;0->i 0040102D jmp main+18h(00401038);判断循环条件 0040102F mov eax,dword ptr [i];i->eax 00401032 add eax,1;eax ++ 00401035 mov dword ptr [i],eax;eax->i 00401038 cmp dword ptr [i],3;循环条件: i与3比较
0040103C jge main+33h(00401053);如果不符合条件,则应结束循环 9: a[i] = myTransform(i);0040103E mov ecx,dword ptr [i];i->ecx 00401041 push ecx;ecx(i)-> 堆栈 00401042 call myTransform(00401000);调用myTransform 00401047 add esp,4;esp+=4: 在堆中的新单元;准备存放返回结果 0040104A mov edx,dword ptr [i];i->edx 0040104D mov dword ptr a[edx*4],eax;将eax(myTransform返回值);放回a[i] 10: } 00401051 jmp main+0Fh(0040102f);计算i++,并继续循环 11: return 0;00401053 xor eax,eax;返回值应该是0 12: } 00401055 mov esp,ebp;恢复堆栈指针 00401057 pop ebp;恢复BP 00401058 ret;返回调用者(C++运行环境)上述代码确实做了一些无用功,当然,这是因为编译器没有对这段代码进行优化。让我们来关注一下这段代码中,是如何调用子程序的。不考虑myTransform这个函数实际进行的数值运算,最让我感兴趣的是这一行代码:
00401003 mov eax,dword ptr [nInput];取参数 这里nInput是一个简简单单的变量符号吗?Visual C++的调试器显然不能告诉我们答案——它的设计目标是为了方便程序调试,而不是向你揭示编译器生成的代码的实际构造。我用另外一个反汇编器得到的结果是:
00401003 mov eax,dword ptr [ebp+8];取参数
这和我们在main()中看到的压栈顺序是完全吻合的(注意,程序运行到这个地方的时候,EBP=ESP)。main()最终将i的值通过堆栈传递给了myTransform()。
剖析上面的程序只是说明了我前面所提到的子程序的一部分用法。对于汇编语言来说,完全没有必要拘泥于结构化程序设计的框架(在今天,使用汇编的主要目的在于提高执行效率,而不是方便程序的维护和调试,因为汇编不可能在这一点上做得比C++更好)。考虑下面的程序:
void myTransform1(int nCount, char* sBytes){ for(register int i=1;i void myTransform2(int nCount, char* sBytes){ for(register int i=0;i 很容易看出,这两个函数包含了公共部分,即 for(i=0;i 目前,还没有编译器能够做到将这两部分合并。依然沿用刚才的编译选项,得到的反汇编结果是(同样地删除了int 3): 1: void myTransform1(int nCount, char* sBytes){ 00401000 push ebp 00401001 mov ebp,esp 00401003 push ecx 2: for(register int i=1;i 非常明显地,0040103d-0040106e和00401074-004010a5这两段代码存在少量的差别,但很显然只是对寄存器的偏好不同(编译器在优化时,这可能会减少堆栈操作,从而提高性能,但在这里只是使用了不同的寄存器而已) 对代码进行合并的好处是非常明显的。新的操作系统往往使用页式内存管理。当内存不足时,程序往往会频繁引发页面失效(Page faults),从而引发操作系统从磁盘中读取一些东西。磁盘的速度赶不上内存的速度,因此,这一行为将导致性能的下降。通过合并一部分代码,可以减少程序的大小,这意味着减少页面失效的可能性,从而软件的性能会有所提高 当然,这样做的代价也不算低——你的程序将变得难懂,并且难于维护。因此,再进行这样的优化之前,一定要注意: 优化前的程序必须是正确的。如果你不能确保这一点,那么这种优化必将给你的调试带来极大的麻烦。 优化前的程序实现最好是最优的。仔细检查你的设计,看看是否已经使用了最合适(即,对于此程序而言最优)的算法,并且已经在高级语言许可的范围内进行了最好的实现。优化最好能够非常有效地减少程序大小(例如,如果只是减少十几个字节,恐怕就没什么必要了),或非常有效地提高程序的运行速度(如果代码只是运行一次,并且只是节省几个时钟周期,那么在多数场合都没有意义)。否则,这种优化将得不偿失。4.2 中断 中断应该说是一个陈旧的话题。在新的系统中,它的作用正在逐渐被削弱,而变成操作系统专用的东西。并不是所有的计算机系统都提供中断,然而在x86系统中,它的作用是不可替代的。 中断实际上是一类特殊的子程序。它通常由系统调用,以响应突发事件。 例如,进行磁盘操作时,为了提高性能,可能会使用DMA方式进行操作。CPU向DMA控制器发出指令,要求外设和内存直接交换数据,而不通过CPU。然后,CPU转去进行起他的操作;当数据交换结束时,CPU可能需要进行一些后续操作,但此时它如何才能知道DMA已经完成了操作呢? 很显然不是依靠CPU去查询状态——这样DMA的优势就不明显了。为了尽可能地利用DMA的优势,在完成DMA操作的时候,DMA会告诉CPU“这事儿我办完了”,然后CPU会根据需要进行处理。 这种处理可能很复杂,需要若干条指令来完成。子程序是一个不错的主意,不过,CALL指令需要指定地址,让外设强迫CPU执行一条CALL指令也违背了CPU作为核心控制单元的设计初衷。考虑到这些,在x86系统中引入了中断向量的概念。 中断向量表是保存在系统数据区(实模式下,是0:0开始的一段区域)的一组指针。这组指针指向每一个中断服务程序的地址。整个中断向量表的结构是一个线性表。 每一个中断服务有自己的唯一的编号,我们通常称之为中断号。每一个中断号对应中断向量表中的一项,也就是一个中断向量。外设向CPU发出中断请求,而CPU自己将根据当前的程序状态决定是否中断当前程序并调用相应的中断服务。不难根据造成中断的原因将中断分为两类:硬件中断和软件中断。硬件中断有很多分类方法,如根据是否可以屏蔽分类、根据优先级高低分类,等等。考虑到这些分类并不一定科学,并且对于我们介绍中断的使用没有太大的帮助,因此我并不打算太详细地介绍它(在本教程的高级篇中,关于加密解密的部分会提到某些硬件中断的利用,但那是后话)。 在设计操作系统时,中断向量的概念曾经带来过很大的便利。操作系统随时可能升级,这样,通过CALL来调用操作系统的服务(如果说每个程序都包含对于文件系统、进程表这些应该由操作系统管理的数据的直接操作的话,不仅会造成程序的臃肿,而且不利于系统的安全)就显得不太合适了——没人能知道,以后的操作系统的服务程序入口点会不会是那儿。软件中断的存在为解决这个问题提供了方便。对于一台包含了BIOS的计算机来说,启动的时候系统已经提供了一部分服务,例如显示服务。无论你的BIOS、显示卡有多么的“个性”,只要他们和IBM PC兼容,那么此时你肯定可以通过调用16(10h)号中断来使用显示服务。调用中断的指令是 int 中断号 这将引发CPU去调用一个中断。CPU将保存当前的程序状态字,清除Trap和Interrupt两个标志,将即将执行的指令地址压入堆栈,并调用中断服务(根据中断向量表)。 编写中断服务程序不是一件容易的事情。很多时候,中断服务程序必须写成可重入代码(或纯代码,pure code)。所谓可重入代码是指,程序的运行过程中可以被打断,并由开始处再次执行,并且在合理的范围内(多次重入,而不造成堆栈溢出等其他问题),程序可以在被打断处继续执行,并且执行结果不受影响。 由于在多线程环境中等其他一些地方进行程序设计时也需要考虑这个因素,因此这里着重讲一下可重入代码的编写。 可重入代码最主要的要求就是,程序不应使用某个指定的内存地址的内存(对于高级语言来说,这通常是全局变量,或对象的成员)。如果可能的话,应使用寄存器,或其他方式来解决。如果不能做到这一点,则必须在开始、结束的时候分别禁止和启用中断,并且,运行时间不能太长。 下面用C语言分别举一个可重入函数,和两个非可重入函数的例子(注.这些例子应该是在某本多线程或操作系统的书上看到的,遗憾的是我想不起来是哪本书了,在这里先感谢那位作者提供的范例): 可重入函数: void strcpy(char* lpszDest, char* lpszSrc){ while(*dest++=*src++);*dest=0;} 非可重入函数 char cTemp;// 全局变量 void SwapChar(char* lpcX, char* lpcY){ cTemp = *lpcX;*lpcX = *lpcY;lpcY = cTemp;// 引用了全局变量,在分享内存的多个线程中可能造成问题 } 非可重入函数 void SwapChar2(char* lpcX, char* lpcY){ static char cTemp;// 静态变量 cTemp = *lpcX;*lpcX = *lpcY;lpcY = cTemp;// 引用了静态变量,在分享内存的多个线程中可能造成问题 } 中断利用的是系统的栈。栈操作是可重入的(因为栈可以保证“先进后出”),因此,我们并不需要考虑栈操作的重入问题。使用宏汇编器写出可重入的汇编代码需要注意一些问题。简单地说,干脆不要用标号作为变量是一个不错的主意。 使用高级语言编写可重入程序相对来讲轻松一些。把持住不访问那些全局(或当前对象的)变量,不使用静态局部变量,坚持只适用局部变量,写出的程序就将是可重入的。书归正传,调用软件中断时,通常都是通过寄存器传进、传出参数。这意味着你的int指令周围也许会存在一些“帮手”,比如下面的代码: mov ax, 4c00h int 21h 就是通过调用DOS中断服务返回父进程,并带回错误反馈码0。其中,ax中的数据4c00h就是传递给DOS中断服务的参数。 到这里,x86汇编语言的基础部分就基本上讲完了,《简明x86汇编语言教程》的初级篇——汇编语言基础也就到此告一段落。当然,目前为止,我只是蜻蜓点水一般提到了一些学习x86汇编语言中我认为需要注意的重要概念。许多东西,包括全部汇编语句的时序特性(指令执行周期数,以及指令周期中各个阶段的节拍数等)、功能、参数等等,限于个人水平和篇幅我都没有作详细介绍。如果您对这些内容感兴趣,请参考Intel和AMD两大CPU供应商网站上提供的开发人员参考。 在以后的简明x86汇编语言教程中级篇和高级篇中,我将着重介绍汇编语言的调试技术、优化,以及一些具体的应用技巧,包括反跟踪、反反跟踪、加密解密、病毒与反病毒等等。 5.0 编译优化概述 优化是一件非常重要的事情。作为一个程序设计者,你肯定希望自己的程序既小又快。DOS时代的许多书中都提到,“某某编译器能够生成非常紧凑的代码”,换言之,编译器会为你把代码尽可能地缩减,如果你能够正确地使用它提供的功能的话。目前,Intel x86体系上流行的C/C++编译器,包括Intel C/C++ Compiler, GNU C/C++ Compiler,以及最新的Microsoft和Borland编译器,都能够提供非常紧凑的代码。正确地使用这些编译器,则可以得到性能足够好的代码。 但是,机器目前还不能像人那样做富于创造性的事情。因而,有些时候我们可能会不得不手工来做一些事情。 使用汇编语言优化代码是一件困难,而且技巧性很强的工作。很多编译器能够生成为处理器进行过特殊优化处理的代码,一旦进行修改,这些特殊优化可能就会被破坏而失效。因此,在你决定使用自己的汇编代码之前,一定要测试一下,到底是编译器生成的那段代码更好,还是你的更好。 本章中将讨论一些编译器在某些时候会做的事情(从某种意义上说,本章内容更像是计算机专业的基础课中《编译程序设计原理》、《计算机组成原理》、《计算机体系结构》课程中的相关内容)。本章的许多内容和汇编语言程序设计本身关系并不是很紧密,它们多数是在为使用汇编语言进行优化做准备。编译器确实做这些优化,但它并不总是这么做;此外,就编译器的设计本质来说,它确实没有义务这么做——编译器做的是等义变换,而不是等效变换。考虑下面的代码: // 程序段1 int gaussianSum(){ int i, j=0;for(i=0;i<100;i++)j+=i; return j;} 好的,首先,绝大多数编译器恐怕不会自作主张地把它“篡改”为 // 程序段1(改进1)int gaussianSum(){ int i, j=0;for(i=1;i<100;i++)j+=i; return j;} 多数(但确实不是全部)编译器也不会把它改为 // 程序段1(改进2)inline int gaussianSum(){ return 5050;} 这两个修改版本都不同于原先程序的语义。首先我们看到,让i从0开始是没有必要的,因为j+=i时,i=0不会做任何有用的事情;然后是,实际上没有必要每一次都计算1+...+100的和——它可以被预先计算,并在需要的时候返回。 这个例子也许并不恰当(估计没人会写出最初版本那样的代码),但这种实践在程序设计中确实可能出现。我们把改进2称为编译时表达式预先计算,而把改进1成为循环强度削减。 然而,一些新的编译器的确会进行这两种优化。不过别慌,看看下面的代码: // 程序段2 int GetFactorial(int k){ int i, j=1;if((k<0)||(k>=10))return-1; if((k<=1))return 1 for(i=1;i return j;} 程序采用的是一个时间复杂度为O(n)的算法,不过,我们可以把他轻易地改为O(1)的算法: // 程序段2(非规范改进)int GetFactorial(int k){ int i, j=1;static const int FractorialTable[]={1, 1, 2, 6, 24, 120, 720, 5040, 40320, 362880, 3628800}; if((k<0)||(k>=10))return-1; return FractorialTable[k];} 这是一个典型的以空间换时间的做法。通用的编译器不会这么做——因为它没有办法在编译时确定你是不是要这么改。可以说,如果编译器真的这样做的话,那将是一件可怕的事情,因为那时候你将很难知道编译器生成的代码和自己想的到底有多大的差距。 当然,这类优化超出了本文的范围——基本上,我把它们归入“算法优化”,而不是“程序优化”一类。类似的优化过程需要程序设计人员对于程序逻辑非常深入地了解和全盘的掌握,同时,也需要有丰富的算法知识。 自然,如果你希望自己的程序性能有大幅度的提升,那么首先应该做的是算法优化。例如,把一个O(n2)的算法替换为一个O(n)的算法,则程序的性能提升将远远超过对于个别语句的修改。此外,一个已经改写为汇编语言的程序,如果要再在算法上作大幅度的修改,其工作量将和重写相当。因此,在决定使用汇编语言进行优化之前,必须首先考虑算法优化。但假如已经是最优的算法,程序运行速度还是不够快怎么办呢? 好的,现在,假定你已经使用了已知最好的算法,决定把它交给编译器,让我们来看看编译器会为我们做什么,以及我们是否有机会插手此事,做得更好。 5.1 循环优化:强度削减和代码外提 比较新的编译器在编译时会自动把下面的代码: for(i=0;i<10;i++){ j = i;k = j + i;} 至少变换为 for(i=0;i<10;i++);j=i;k=j+i; 甚至 j=i=10;k=20; 当然,真正的编译器实际上是在中间代码层次作这件事情。 原理 如果数据项的某个中间值(程序执行过程中的计算结果)在使用之前被另一中间值覆盖,则相关计算不必进行。 也许有人会问,编译器不是都给咱们做了吗,管它做什么?注意,这里说的只是编译系统中优化部分的基本设计。不仅在从源代码到中间代码的过程中存在优化问题,而且编译器生成的最终的机器语言(汇编)代码同样存在类似的问题。目前,几乎所有的编译器在最终生成代码的过程中都有或多或少的瑕疵,这些瑕疵目前只能依靠手工修改代码来解决。 5.2 局部优化:表达式预计算和子表达式提取 表达式预先计算非常简单,就是在编译时尽可能地计算程序中需要计算的东西。例如,你可以毫不犹豫地写出下面的代码: const unsigned long nGiga = 1024L * 1024L * 1024L; 而不必担心程序每次执行这个语句时作两遍乘法,因为编译器会自动地把它改为 const unsigned long nGiga = 1073741824L; 而不是傻乎乎地让计算机在执行到这个初始化赋值语句的时候才计算。当然,如果你愿意在上面的代码中掺上一些变量的话,编译器同样会把常数部分先行计算,并拿到结果。 表达式预计算并不会让程序性能有飞跃性的提升,但确实减少了运行时的计算强度。除此之外,绝大多数编译器会把下面的代码: // [假设此时b, c, d, e, f, g, h都有一个确定的非零整数值,并且,// a[]为一个包括5个整数元素的数组,其下标为0到4] a[0] = b*c;a[1] = b+c;a[2] = d*e;a[3] = b*d + c*d;a[4] = b*d*e + c*d*e; 优化为(再次强调,编译器实际上是在中间代码的层次,而不是源代码层次做这件事情!): // [假设此时b, c, d, e, f, g, h都有一个确定的非零整数值,并且,// a[]为一个包括5个整数元素的数组,其下标为0到4] a[0] = b*c;a[1] = b+c;a[2] = d*e;a[3] = a[1] * d;a[4] = a[3] * e; 更进一步,在实际代码生成过程中,一些编译器还会对上述语句的次序进行调整,以使其运行效率更高。例如,将语句调整为下面的次序: // [假设此时b, c, d, e, f, g, h都有一个确定的非零整数值,并且,// a[]为一个包括5个整数元素的数组,其下标为0到4] a[0] = b*c;a[1] = b+c;a[3] = a[1] * d;a[4] = a[3] * e;a[2] = d*e; 在某些体系结构中,刚刚计算完的a[1]可以放到寄存器中,以提高实际的计算性能。上述5个计算任务之间,只有1, 3, 4三个计算任务必须串行地执行,因此,在新的处理器上,这样做甚至能够提高程序的并行度,从而使程序效率变得更高。 5.3 全局寄存器优化 [待修订内容] 本章中,从这一节开始的所有优化都是在微观层面上的优化了。换言之,这些优化是不能使用高级语言中的对应设施进行解释的。这一部分内容将进行较大规模的修订。 通常,此类优化是由编译器自动完成的。我个人并不推荐真的由人来完成这些工作——这些工作多半是枯燥而重复性的,编译器通常会比人做得更好(没说的,肯定也更快)。但话说回来,使用汇编语言的程序设计人员有责任了解这些内容,因为只有这样才能更好地驾驭处理器。 在前面的几章中我已经提到过,寄存器的速度要比内存快。因此,在使用寄存器方面,编译器一般会做一种称为全局寄存器优化的优化。 例如,在我们的程序中使用了4个变量:i, j, k, l。它们都作为循环变量使用: for(i=0;i<1000;i++){ for(j=0;j<1000;j++){ for(k=0;k<1000;k++){ for(l=0;l<1000;l++)do_something(i, j, k, l);} } } 这段程序的优化就不那么简单了。显然,按照通常的压栈方法,i, j, k, l应该按照某个顺序被压进堆栈,然后调用do_something(),然后函数做了一些事情之后返回。问题在于,无论如何压栈,这些东西大概都得进内存(不可否认某些机器可以用CPU的Cache做这件事情,但Cache是写通式的和回写式的又会造成一些性能上的差异)。 聪明的读者马上就会指出,我们不是可以在定义do_something()的时候加上inline修饰符,让它在本地展开吗?没错,本地展开以增加代码量为代价换取性能,但这只是问题的一半。编译器尽管完成了本地展开,但它仍然需要做许多额外的工作。因为寄存器只有那么有限的几个,而我们却有这么多的循环变量。 把四个变量按照它们在循环中使用的频率排序,并决定在do_something()块中的优先顺序(放入寄存器中的优先顺序)是一个解决方案。很明显,我们可以按照l, k, j, i的顺序(从高到低,因为l将被进行1000*1000*1000*1000次运算!)来排列,但在实际的问题中,事情往往没有这么简单,因为你不知道do_something()中做的到底是什么。而且,凭什么就以for(l=0;l<1000;l++)作为优化的分界点呢?如果do_something()中还有循环怎么办? 如此复杂的计算问题交给计算机来做通常会有比较满意的结果。一般说来,编译器能够对程序中变量的使用进行更全面地估计,因此,它分配寄存器的结果有时虽然让人费解,但却是最优的(因为计算机能够进行大量的重复计算,并找到最好的方法;而人做这件事相对来讲比较困难)。 编译器在许多时候能够作出相当让人满意的结果。考虑以下的代码: int a=0;for(int i=1;i<10;i++)for(int j=1;j<100;j++){ a +=(i*j);} 让我们把它变为某种形式的中间代码: 00: 0-> a 01: 1-> i 02: 1-> j 03: i*j-> t 04: a+t-> a 05: j+1-> j 06: evaluate j < 100 07: TRUE? goto 03 08: i+1-> i 09: evaluate i < 10 10: TRUE? goto 02 11: [继续执行程序的其余部分] 程序中执行强度最大的无疑是03到05这一段,涉及的需要写入的变量包括a, j;需要读出的变量是i。不过,最终的编译结果大大出乎我们的意料。下面是某种优化模式下Visual C++ 6.0编译器生成的代码(我做了一些修改): xor eax, eax;a=0(eax: a)mov edx, 1;i=1(edx: i)push esi;保存esi(最后要恢复,esi作为代替j的那个循环变量)nexti: mov ecx, edx;[t=i] mov esi, 999;esi=999: 此处修改了原程序的语义,但仍为1000次循环。nextj: add eax, ecx;[a+=t] add ecx, edx;[t+=i] dec esi;j--jne SHORT nextj;jne 等价于 jnz.[如果还需要,则再次循环] inc edx;i++ cmp edx, 10;i与10比较 jl SHORT nexti;i < 10, 再次循环 pop esi;恢复esi 这段代码可能有些令人费解。主要是因为它不仅使用了大量寄存器,而且还包括了5.2节中曾提到的子表达式提取技术。表面上看,多引入的那个变量(t)增加了计算时间,但要注意,这个t不仅不会降低程序的执行效率,相反还会让它变得更快!因为同样得到了计算结果(本质上,i*j即是第j次累加i的值),但这个结果不仅用到了上次运算的结果,而且还省去了乘法(很显然计算机计算加法要比计算乘法快)。 这里可能会有人问,为什么要从999循环到0,而不是按照程序中写的那样从0循环到999呢?这个问题和汇编语言中的取址有关。在下两节中我将提到这方面的内容。 5.4 x86体系结构上的并行最大化和指令封包 考虑这样的问题,我和两个同伴现在在山里,远处有一口井,我们带着一口锅,身边是树林;身上的饮用水已经喝光了,此处允许砍柴和使用明火(当然我们不想引起火灾:),需要烧一锅水,应该怎么样呢? 一种方案是,三个人一起搭灶,一起砍柴,一起打水,一起把水烧开。 另一种方案是,一个人搭灶,此时另一个人去砍柴,第三个人打水,然后把水烧开。 这两种方案画出图来是这样: 仅仅这样很难说明两个方案孰优孰劣,因为我们并不明确三个人一起打水、一起砍柴、一起搭灶的效率更高,还是分别作效率更高(通常的想法,一起做也许效率会更高)。但假如说,三个人一个只会搭灶,一个只会砍柴,一个只会打水(当然是说这三件事情),那么,方案2的效率就会搞一些了。 在现实生活中,某个人拥有专长是比较普遍的情况;在设计计算机硬件的时候则更是如此。你不可能指望加法器不做任何改动就能去做移位甚至整数乘法,然而我们注意到,串行执行的程序不可能在同一时刻同时用到处理器的所有功能,因此,我们(很自然地)会希望有一些指令并行地执行,以充分利用CPU的计算资源。 CPU执行一条指令的过程基本上可以分为下面几个阶段:取指令、取数据、计算、保存数据。假设这4个阶段各需要1个时钟周期,那么,只要资源够用,并且4条指令之间不存在串行关系(换言之这些指令的执行先后次序不影响最终结果,或者,更严格地说,没有任何一条指令依赖其他指令的运算结果)指令也可以像下面这样执行: 指令1 取指令 取数据 计 算 存数据 指令2 取指令 取数据 计 算 存数据 指令3 取指令 取数据 计 算 存数据 指令4 取指令 取数据 计 算 存数据 这样,原本需要16个时钟周期才能够完成的任务就可以在7个时钟周期内完成,时间缩短了一半还多。如果考虑灰色的那些方格(这些方格可以被4条指令以外的其他指令使用,只要没有串行关系或冲突),那么,如此执行对于性能的提升将是相当可观的(此时,CPU的所有部件都得到了充分利用)。 当然,作为程序来说,真正做到这样是相当理想化的情况。实际的程序中很难做到彻底的并行化。假设CPU能够支持4条指令同时执行,并且,每条指令都是等周期长度的4周期指令,那么,程序需要保证同一时刻先后发射的4条指令都能够并行执行,相互之间没有关联,这通常是不太可能的。 最新的Intel Pentium 4-XEON处理器,以及Intel Northwood Pentium 4都提供了一种被称为超线程(Hyper-Threading TM)的技术。该技术通过在一个处理器中封装两组执行机构来提高指令并行度,并依靠操作系统的调度来进一步提升系统的整体效率。 由于线程机制是与操作系统密切相关的,因此,在本文的这一部分中不可能做更为深入地探讨。在后续的章节中,我将介绍Win32、FreeBSD 5.x以及Linux中提供的内核级线程机制(这三种操作系统都支持SMP及超线程技术,并且以线程作为调度单位)在汇编语言中的使用方法。 关于线程的讨论就此打住,因为它更多地依赖于操作系统,并且,无论如何,操作系统的线程调度需要更大的开销并且,到目前为止,真正使用支持超线程的CPU,并且使用相应操作系统的人是非常少的。因此,我们需要关心的实际上还是同一执行序列中的并发执行和指令封包。不过,令人遗憾的是,实际上在这方面编译器做的几乎是肯定要比人好,因此,你需要做的只是开启相应的优化;如果你的编译器不支持这样的特性,那么就把它扔掉„„据我所知,目前在Intel平台上指令封包方面做的最好的是Intel的C++编译器,经过Intel编译器编译的代码的性能令人惊异地高,甚至在AMD公司推出的兼容处理器上也是如此。5.5 存储优化 从前一节的图中我们不难看出,方案2中,如果谁的动作慢,那么他就会成为性能的瓶颈。实际上,CPU也不会像我描述的那样四平八稳地运行,指令执行的不同阶段需要的时间(时钟周期数)是不同的,因此,缩短关键步骤(即,造成瓶颈的那个步骤)是缩短执行时间的关键。 至少对于使用Intel系列的CPU来说,取数据这个步骤需要消耗比较多的时间。此外,假如数据跨越了某种边界(如4或8字节,与CPU的字长有关),则CPU需要启动两次甚至更多次数的读内存操作,这无疑对性能构成不利影响。 基于这样的原因,我们可以得到下面的设计策略: 程序设计中的内存数据访问策略 尽可能减少对于内存的访问。在不违背这一原则的前提下,如果可能,将数据一次处理完。尽可能将数据按4或8字节对齐,以利于CPU存取 尽可能一段时间内访问范围不大的一段内存,而不同时访问大量远距离的分散数据,以利于Cache缓存* 第一条规则比较简单。例如,需要求一组数据中的最大值、最小值、平均数,那么,最好是在一次循环中做完。 “于是,这家伙又攒了一段代码”„„ int a[]={1,2,3,4,5,6,7,8,9,0,1,2,3,4,5,6,7,8,9,0};int i;int avg, max, min;avg=max=min=a[0]; for(i=1;i<(sizeof(a)/sizeof(int));i++){ avg+=a[i];if(max < a[i])max = a[i];else if(min > a[i])min = a[i];} avg /= i; Visual C++编译器把最开始一段赋值语句翻译成了一段简直可以说是匪夷所思的代码: ;int a[]={1,2,3,4,5,6,7,8,9,0,1,2,3,4,5,6,7,8,9,0}; mov edi, 2;此时edi没有意义 mov esi, 3;esi也是!临时变量而已。mov DWORD PTR _a$[esp+92], edi mov edx, 5;黑名单加上edx mov eax, 7;eax也别跑:)mov DWORD PTR _a$[esp+132], edi mov ecx, 9;就差你了,ecx ;int i;;int avg, max, min;;avg=max=min=a[0]; mov edi, 1;edi摇身一变,现在它是min了。mov DWORD PTR _a$[esp+96], esi mov DWORD PTR _a$[esp+104], edx mov DWORD PTR _a$[esp+112], eax mov DWORD PTR _a$[esp+136], esi mov DWORD PTR _a$[esp+144], edx mov DWORD PTR _a$[esp+152], eax mov DWORD PTR _a$[esp+88], 1;编译器失误? 此处edi应更好 mov DWORD PTR _a$[esp+100], 4 mov DWORD PTR _a$[esp+108], 6 mov DWORD PTR _a$[esp+116], 8 mov DWORD PTR _a$[esp+120], ecx mov DWORD PTR _a$[esp+124], 0 mov DWORD PTR _a$[esp+128], 1 mov DWORD PTR _a$[esp+140], 4 mov DWORD PTR _a$[esp+148], 6 mov DWORD PTR _a$[esp+156], 8 mov DWORD PTR _a$[esp+160], ecx mov DWORD PTR _a$[esp+164], 0 mov edx, edi;edx是max。 mov eax, edi;期待已久的avg, 它被指定为eax 这段代码是最优的吗?我个人认为不是。因为编译器完全可以在编译过程中直接把它们作为常量数据放入内存。此外,如果预先对a[0..9]10个元素赋值,并利用串操作指令(rep movsdw),速度会更快一些。 当然,犯不上因为这些问题责怪编译器。要求编译器知道a[0..9]和[10..19]的内容一样未免过于苛刻。我们看看下面的指令段: ;for(i=1;...mov esi, edi for_loop: ;avg+=a[i]; mov ecx, DWORD PTR _a$[esp+esi*4+88] add eax, ecx ;if(max < a[i]) cmp edx, ecx jge SHORT elseif_min ;max = a[i]; mov edx, ecx ;else if(min > a[i]) jmp SHORT elseif_min elseif_min: cmp edi, ecx jle SHORT elseif_end ;min = a[i];mov edi, ecx elseif_end: ;[for i=1];i<20;i++){ inc esi cmp esi, 20 jl SHORT for_loop;};avg /= i;cdq idiv esi ;esi: i ;ecx: 暂存变量, =a[i];eax: avg ;edx: max ;有趣的代码...并不是所有的时候都有用;但是也别随便删除;edi: min ;i++;i与20比较 ;avg /= i 上面的程序倒是没有什么惊人之处。唯一一个比较吓人的东西是那个jmp SHORT指令,它是否有用取决于具体的问题。C/C++编译器有时会产生这样的代码,我过去曾经错误地把所有的此类指令当作没用的代码而删掉,后来发现程序执行时间没有明显的变化。通过查阅文档才知道,这类指令实际上是“占位指令”,他们存在的意义在于占据那个地方,一来使其他语句能够正确地按CPU觉得舒服的方式对齐,二来它可以占据CPU的某些周期,使得后续的指令能够更好地并发执行,避免冲突。另一个比较常见的、实现类似功能的指令是NOP。 占位指令的去留主要是靠计时执行来判断。由于目前流行的操作系统基本上都是多任务的,因此会对计时的精确性有一定影响。如果需要进行测试的话,需要保证以下几点: 计时测试需要注意的问题 测试必须在没有额外负荷的机器上完成。例如,专门用于编写和调试程序的计算机 尽量终止计算机上运行的所有服务,特别是杀毒程序 切断计算机的网络,这样网络的影响会消失 将进程优先级调高。对于Windows系统来说,把进程(线程)设置为Time-Critical;对于*nix系统来说,把进程设置为实时进程 将测试函数运行尽可能多次运行,如10000000次,这样能够减少由于进城切换而造成的偶然误差 最后,如果可能的话,把函数放到单进程的系统(例如FreeDOS)中运行。 对于绝大多数程序来说,计时测试是一个非常重要的东西。我个人倾向于在进行优化后进行计时测试并比较结果。目前,我基于经验进行的优化基本上都能够提高程序的执行性能,但我还是不敢过于自信。优化确实会提高性能,但人做的和编译器做的思路不同,有时,我们的确会做一些费力不讨好的事情。 一、入门教材: 1、曼昆《经济学原理》上下册,88元。梁小民教授翻译。曼昆为哈佛高才生,天才横溢,属新古典凯恩斯主义学派,研究范围偏重宏观经济分析。 该书为大学一年级学生而写,主要特点是行文简单、说理浅显、语言有趣。界面相当友好,引用大量的案例和报刊文摘,与生活极其贴近,诸如美联储为何存在,如何运作,Greenspan 如何降息以应付经济低迷等措施背后的经济学道理。该书几乎没有用到数学,而且自创归纳出“经济学10大原理”,为初学者解说,极其便利完全没有接触过经济学的人阅读。学此书,可了解经济学的基本思维,常用的基本原理,用于看待生活中的经济现象。可知经济学之功用及有趣,远超一般想象之外。推荐入门首选阅读。目前国内已经有某些教授依据此书编著《西方经济学》教材,在书中出现“经济学10大原理”一词,一眼便可看出是抄袭而来。 2、萨缪尔森《经济学》(Economics) 萨缪尔森,新古典综合学派的代表人物,1970年成为第一个荣获诺贝尔经济学奖的美国人。研究范围横跨经济学、统计学和数学多个领域,对政治经济学、部门经济学和技术经济学有独到的见解。目前经济学各种教科书,所使用的分析框架及分析方法,多采用由他1947年的《微观经济分析》发展糅合凯恩斯主义和传统微观经济学而成的“新古典综合学派”理论框架。他一直热衷于把数学工具运用于静态均衡和动态过程的分析,以物理学和数学论证推理方式研究经济。目前经济学理论数学化大行其道,此翁实始作俑者。 全书结构宏伟,篇幅巨大。可谓博大精深。渗透老萨数十年经济学见解。字里行间,三言两语,每有深意。其中诸如“热情的心,冷静的头脑”、“相关未必因果”等言语,可谓经济学之《老子》。读完该书,可了解经济学所探讨问题在经济学体系中之位置及分析框架,对经济学有一个完备之认识框架。知识庞杂,有一体系框架,则适宜以后更进一步学习。学之愈深,愈知此框架之重要。尽管该框架在宏观经济学的微观基础方面仍有断层,但不失为一个好框架。此书国内有机工版发行之英文版。建议直接阅读英文版。 3、斯蒂格利姿《经济学》及系列辅助教材。斯蒂格利姿在信息经济学成就甚高,此书可作为前二者的补充,前二者所涉及经济学内容主要是以价格理论及边际分析为基础,不包括不对称信息经济学、不确定性分析部分。斯蒂格利姿之《经济学》可填充前二者之空白。 尽管三位作者政策倾向不同,但教材体现凯恩斯主义的特征稍多一点,总体上讲,教材相当客观和公允。很适宜做入门教材。 4、《经济学、原理、问题与政策》及《经济学原理与问题》、〈经济学案例〉、〈经济学小品〉、《经济学悖论》、〈社会问题经济学〉等。此类书之特点是先提问题,再论原理,主要是针对社会习见问题,逐步解释原理,水平、内容大多较好,唯缺乏体系与框架,适宜略懂经济学者补充学习。 5、国内老师自行编写之《西方经济学》教材:目前国内各大学自己编写的直接冠以《西方经济学》或〈经济学原理〉均属入门教材。如高鸿业、历以宁、宋承先、梁小民、朱锡庆、尹伯成、司春林等等。然皆远逊外国教材。 说明: 1)、越基础性之教材越需深入浅出,将复杂抽象的道理联系到生活实际上,才讲的透彻,又能调起初学者之兴趣。国外教材,形成一竞争市场,多极高明之著作,教材之撰写也充分考虑学生学习之便利,如曼昆之教材,以完全不带数学式而著称,又或更新换版本极快,以及时吸收新知识,如斯蒂格利姿《经济学》之增加不对称信息部分。低手所写教材自然被市场淘汰。故市面之基础教材,多为大高手所写就。 2)、国内教材,建国以来,除商务系列丛书初期之100年前古典学派部分,政府同意翻译以作为马克思批判之反面教材得以出版外,80年代以前,近50年间国外经济学研究学问之成就,国人皆不得见。80年代末期,邹至庄先生力倡西方经济学,邓大人首肯之后,国内始渐有〈西方经济学〉之类教材出现。此类教材,多为新出道之老师,为进阶升职,凑出版物之数而编抄西人著作而成,机制所限,不敢添加“反动”之知识,又无竞争机制,购买者多为其听课学生。故质量甚差,若非特殊目的如考研指定者,慎勿购买。 3)、按经济学有入门低、中级、高级之分。高级乃指其运用之数学工具及阐述观点之纷争更多而言,并非此学问高人一等。一如高等数学未必高初等数学一等之意。越是高级,则越多分歧,也越追求数理逻辑之严谨,反不如低级来的实用。初级的入门教材一般是针对初学者,所以大多举案例和现象,加以文字解释,偶尔插加二维图案,高级教材注重数理逻辑,而二维图案及文字已难以表达、解决所说明之问题,故多用数学证明或代数方程,夹杂现代数学工具。中级教材则介乎其中,界定甚为模糊。教材难度不同,跨度也相差很大。 二、中级微观教材。 中级教材一般以微观、宏观两科为主,兼修其他应用科目。传统经济学,本无宏观、微观之分,自凯恩斯针对名义变量进行宏观经济分析之后,始有宏观一科。故历来次序,先修微观,再修宏观,后及其他。 微观经济学为各科之基础。其分析,乃基于马歇尔的一般均衡分析及边际效用学派之边际分析,而后由萨谬而森发展数学方法及框架而成,涵盖范围甚广,大致包括: 基础部分:传统厂商理论(技术、利润、成本)、传统消费者理论(效用、偏好、选择、需求)、局部均衡理论(完全竞争市场之稳定性)、一般均衡理论(福利经济学二大定理、交换方框图) 分支部分:寡占市场理论(寡头、定价、市场细分)、博奕论(纯策略均衡、混合博奕、广延型结构、厂商博奕、颤抖的手)、公共物品理论(公共物品、税收制度设计、投票、外部效应)、不确定性经济学(风险、博采、保险、投资)、信息经济学(不对称信息、逆向选择、信号)、激励理论(委托-代理理论、契约理论)、法和经济学(制度经济学、企业性质分析、法律)、拍卖理论(拍卖机制设计)、匹配理论等。 学习者可根据上述内容,与教材所列提纲比较,则可知教材侧重点之所在。 6《管理经济学》,有版本数种,特点各不相同。此类教材多为mba系列教材。其目的针对生产过程决策而设,故与经济学之中级微观教材相较而言,减少少量分支部分理论,增加回归分析及计量统计部分。目前数种版本中,以人大版〈工商管理经典译从〉难度最低。机工版哈耶所写之〈管理经济学-战略与决策〉与标准中级教材难度大致相当,内容也接近。唯其中也已采用函数表达式。机工版莫瑞斯(有英文版及中文版,中文为陈章武所译)〈管理经济学〉难度最高,其侧重内容与中级教材大不相同,除回归分析已采用大量数据,要求建立模型,内容接近计量预测外,内容涉及对偶理论、不同代替效应之图解,附录采用微分法,难度较高。此类书籍,侧重经济学中与管理交叉管理。 7、平狄克《微观经济学》。人大版,此书乃标准中级微观经济学教材。在美国多个大学供mba采用,国内英文版有清华版,中文版有人大版。此书内容适中,主题广泛,均是各部分理论之要点,不旁及其他分歧内容,其中定价部分较为详细。图形清晰,语言流畅。所采用数学工具甚浅,有函数但不涉及微分,只用差值。曲线只用标准严格凹性曲线,不及拟凹部分、线性仿射内容,成本函数也均为线性。建议此书应通读,可作进阶之用。 8、曼斯非尔特《微观经济学》人大版,内容、难度、书价与平狄克相仿,唯编排次序不同。体系稍显庞杂,不如平狄克之明晰,然也为一国外通行教材。若修习近平狄克有不明之处,则可先参照此教材或先修学其他国内出版之书籍。如北大系列教材之周惠中〈微观经济学〉,北大版朱善利之《微观经济学》等。此书不属必读。 9、《国外经济学教材库》系列之《应用微观经济学》,32开,经济科学出版社。此书有大量案例及微观经济原理之运用,所用数学甚少,读此书,可补充平狄克教材之案例。加深对经济学之了解。 10、〈微观经济学: 现代观点〉(Intermediate microeconomics)[美] 范里安(Varian, Hal R.)著,费方域翻译。据美国W.W.诺顿图书公司 1990年版译出,三联版。此书是极规范之经济学专业的中级微观教材。美国MIT,哈佛、伯克利经济学本科指定教材。32开,800多页。易懂而深刻。本书为第二版,内容除论述了市场、消费者偏好、需求、技术、利润、生产等问题,还增加了两章, 分别论述了要素供给和信息经济等。内容上相当关注技术细节问题,比平狄克要更深一些。范里安微观经济学与数学造诣极深。然此书乃其为学生所写之中级教材,刻意避免数学之应用,大部分数学推导放于附录,微分运用相当少,适宜学完平狄克后重点阅读。可作平狄克中各部分理论内容之拓展。 三、中级宏观教材。若无意进一步学习高级微观经济学,则可同时学习宏观经济学。微观的特点是精深,宏观则是驳杂。因为宏观流派很多,观点各不相同。 11、《宏观经济学》曼昆,人大版。中文翻译。此书秉承曼昆〈经济学原理〉之优点,以简单,浅显为特点。虽只有很少量的数学,但对原理及内容均提炼得甚为简洁。前半部分写得相当清晰。可读完萨谬而森《经济学》并略懂一点微观后直接学习。适宜一个循环学习,即以书入手,修完《全球视角》后,再回头重修此书,有提纲挈领之用。缺点是作者似乎限于门户之见,对真实周期学派、奥地利学派等其他学派提得很少。建议阅读。 12、《宏观经济学》多恩布什。人大版中文翻译,东北财大有影印英文版。此书是标准的中级宏观教材,属正统教材。体系清楚,描述准确,通行于美国各大学多年。采用凯恩斯IS-LM体系为框架,对各个流派评价及描述相当公平。推荐必读。 13、《宏观经济学》人大版,中文翻译。罗伯特霍尔,整本书显得有点凌乱,适宜读过其他中级宏观再做印证之用,内容比上述两本教材略深。不属必读范围。 14、《宏观经济学》巴罗。清华,影印英文版。巴罗宏观经济学造诣很深,主要研究领域在经济增长理论。但写的书却销路很差。学这本书可作为对上述教材所属凯恩斯学派的一个补充。不属必读范围。 15、《全球视角的宏观经济学》三联版 杰佛里萨克斯,32开,1000页。萨克斯成功处理了南美高通货膨胀的问题,但书一样写的相当好,整本书注意细节而有条理。很适宜读完多恩布什《宏观经济学》后进一步阅读。以拓展知识。上述5种教材所用符号各不相同,对学习者实在甚为不便。 16、《国际经济学》 保罗克鲁格曼,今日之宏观经济学,已很难讨论封闭的宏观经济,此书可谓进一步拓展的宏观经济学,包括国际贸易和国际金融两个部分,渗透克鲁格曼的经济思想,所采用框架为AS-AD框架,可作IS-LM框架的补充。推荐阅读。 17《现代宏观经济学发展与反思》及《现代宏观经济学指南-各思想流派分析》及《与经济学大师对话》系列三册,前两册为商务版。此书乃对各不同流派经济学大师的采访和评论,对各个流派的异同可以有清楚的了解,而且是直面经济学大师,可以看到各个大师之间彼此的观点不同,甚至成见立场,互相抨击之处,实在有趣。推荐阅读。 四、其他教材: 18、人大版《经济科学译丛》系列之其他大多数教材:《经济思想史》、《财政学》、《公共部门经济学》、《人事经济学》、《金融学》(博迪)、《投资学》、《货币银行学》(米十金)等等实务应用之科目。适当补充阅读〈公共选择理论〉、奥地利学派、哈耶克、剑桥之争、非瓦尔拉斯均衡分析、等等内容。 19、三联丛书黄皮书系列,其中显要者如《公共经济学》(Lectures on public economics)(阿特金森(Atkinson, Anthony B.)[美] 斯蒂格里茨(Stiglitz, Joseph E.)著)、〈政治与市场: 世界的政治—经济制度》、《财产权利与制度变迁: 产权学派与新制度学派译文集》、《经济史中的结构与变迁》、《货币、银行与经济》(Money, Banking, and the Economy)〔美国〕托马斯•梅耶(Thomas Mayer)、〈法和经济学〉等等。可对经济学之应用领域获得一个深刻视角。三联丛书,推荐全部阅读。 20、张五常《卖橘者言》、《佃农理论》、《经济解释》。张老先生近年是国内焦点所在,也写了几本〈随笔〉,发表不少演讲,大体而言,〈随笔〉不堪一读,其中论书法、摄影部分,不关主旨,且水平甚低,多属偏颇之见,今不论之。唯上述专著中之〈佃农理论〉,见解独到,尤有过人之处。建议修完中级微观后仔细阅读。《经济解释》则为论文集,然其中也有不少过激之言论及偏见,不可以教材视之。其中“合约理论”部分,可以一读。论“共产主义”部分,则未必有理。 21、杨小凯〈经济学原理〉〈新兴超边际古典经济学〉,杨先生气魄甚大,欲以一己之力重写传统经济学体系,与汪丁丁先生有异曲同工之妙。可谓经济学之异端,读之可开阔视野。推荐阅读。 22、〈波斯纳文集〉苏力翻译。老先生以法学专才,写〈法之经济学分析〉,实一极高明之人士,于此不可不提。推荐阅读。 23、商务丛书《汉译世界名著》系列:此丛书系列,自二十世纪初商务王云五先生主持,与是事者不计其数,除文革中断十余年外,每年陆续出版,涵盖哲学(红皮)、历史(黄皮)、政治(绿皮)、经济(蓝皮)、语言学、人类学(未成),所翻译者,非经典不收,皆大师之精华,所主持翻译之人,多博学鸿儒或一代大师。单经济一门,翻译之著作,至今已近百种。百年间,传播知识无数,可谓功德无量。读完蓝皮经济类之全部,则可通晓经济学之来龙去脉。 至此,无意于经济学一门谋生者,已然足够。然上述书籍,常人阅读,少者耗时约需1、2年以上。多者3、5年。且其中论著,多高明之作,或有一读再读之需,而读完,也或有“屠龙之技”之感也未之定,一笑! 五、数学工具:即所谓数理经济学一科。 若数学水平较高,有意进一步玩弄经济学之数学智力游戏,则可参读以下数学工具:中国大学本科考研究生之数学三(高数、线性代数、概率论与数理统计)为必修之基础课,其他之数学工具则包括拓扑学初步(凸集、凹集、微分方程稳定性)、线性规划(代数理论、几何理论、对偶理论)、非线性规划(不等式约束规划)、变分法(欧拉方程、泛函函数、收敛问题、可变端点、横截条件、勒让得必要条件、相图分析)、最优控制理论(最大值原理、汉密尔顿函数)、连续时间优化规划、离散时间优化规划(不动点性质、值函数)、时间序列分析、非线性混沌系统、随机变量等等。 24、《经济学中的数学》(入门水平) 25、蒋中一《数理经济学基础》(基础水平) 26、《动态优化基础》(进阶水平) 27、高山成(takayama)《经济学中的优化方法》(推荐阅读) 28、龚六堂《经济学中的优化方法》(推荐阅读) 29、《经济学中的动态递归方法》(推荐阅读) 30、〈数理经济学手册〉人大版(重点阅读) 六、中高级微观经济学:下文书籍,未必尽是高明著作,然国内此类教材甚少,下述书籍,聊胜于无。 31、平新乔的《微观经济学18讲》,北大出版。内容属于中高级微观经济学,涉及微观领域较多,引入大量的数学运算,除文字内容外,强调逻辑推理。惟书中有不少印刷错误,且理论内容跳跃太快,不利学习理解,数学运用庞杂,不够明快清晰。在国内中高级教材中属中上之作,接近国外大学本科高年级水平。最大的优点是书后付有大量需要运算的习题,均需花时间读书和思考才能解决,很适宜学习训练。对从中级到高级过渡有帮助。不属必读范围。 32、张定胜《高级微观经济学》。武大出版。此书属于中高级内容,因涉及主题较少,故比平新乔之〈18讲〉显得清晰。适宜找不到其他中高级教材,而高级教材又甚困难,可以此书做过渡。 33、Nicholson < Microeconomic Theory>>。国内中文翻译出版。此书微积分运用、数学运算简洁明晰,全书难度、体系一致,排版清楚、内容重点突出,主题有深度,实为一极佳之中高级教材。书后之参考书目适宜进一步学习参考,为中级教材之中,最适宜和高级教材接轨者,唯书价稍贵,习题难度不深,习题量稍显不足。此书似乎出版发行量不多,除北大、复旦等处书店有少量可见外,其他大学及城市似甚少见。推荐阅读。 34、蒋殿春《高级微观经济学》,经济管理出版社。此书主题基础部分已达高级水平,难度甚大。至博奕论以后部分,则难度甚浅。或与日本经济学之教授方法有关。对传统的价格理论的数学描述相当清楚。数学证明部分清楚。推荐阅读。 管理学入门书籍 科目推荐书目 孙耀君,《西方管理学名著提要》,江西人民出版社 ****管理学**** 斯蒂芬·P·罗宾斯,《管理学》,中国人民大学出版社 邢以群,《管理学》,浙江大学出版社 周三多,《管理学》,复旦大学出版社 ****管理信息系统**** Kenneth C.Laudon/ Jane P.Laudon,《管理信息系统—网络化企业的组织与技术》(第六版,影印版),高等教育出版社 斯蒂芬 ·哈格等,《信息时代的管理信息系统》,机械工业出版社 薛华成,《管理信息系统》(第三版),清华大学出版社 ****市场营销**** 菲利浦·科特勒,《市场营销》,清华大学出版社 小威廉D.佩勒尔特 E.杰罗姆.麦卡锡,《市场营销学基础:全球管理(英文版.第12版)--国际通用MBA教材》,机械工业出版社 郭毅等,《市场营销学原理》,电子工业出版社 Malhotra,N.K.著,《市场营销研究应用导向(第3版)》,电子工业出版社 ****战略管理**** 项保华,《战略管理——艺术与实务》,华夏出版社 迈克尔·波特,《竞争战略》,华夏出版社 迈克尔·波特,《竞争优势》,华夏出版社 享利·明茨伯格(美),《战略历程》,机械工业出版社 斯蒂文斯(英),《战略性思维》,机械工业出版社 Arthur A.Thompson, Jr.And A.J.Stricklan Ⅲ.Crafting&Implementing Strategy.6th Ed.Richard D.Irwin, Inc., 1995中文版《战略管理学:概念与案例(英文版.第十版)--国际通用MBA教材》,机械 工业出版社 David Besanko, David Dranove, & Mark Shanley.The Economics of Strategy.John Wiley & Sons, Inc.,1996 Alan J.Rowe;et al..Strategic Management: A Methodological Approach.4th Ed.Addison-Wesley Publishing Company, Inc., 199 4****组织行为学**** 斯蒂芬·P·罗宾斯,《组织行为学精要(原书第7版)--MBA教材精品译丛》,机械工业出版社 Steven L.McShane;Mary Ann Von Glinow,《组织行为学(英文版)--21世纪经典原版经济管理教材文库》,电子工业出版社 卢盛忠等,《组织行为学:理论与实践》,浙江教育出版社 ****人力资源管理**** 雷蒙德.A.诺伊,约翰.霍伦拜克,拜雷.格哈特,帕特雷克.莱特,《人力资源管理——赢得竞争优势》(第三版)中国人民大学出版社英文版《Human Resource Management: Gaining a Competitive Advantage》,清华大学出版社 约翰.M.伊万切维奇,《人力资源管理(英文版.原书第8版)--国际通用MBA教材》,机械工业出版社 Luis R.Gomez-Mejia,David B.Balkin,Robert L.Cardy,《管理人力资源 Managing Human Resources 》 (英文版第三版),北京大学出版社 ****财务管理**** 斯蒂芬·罗丝,《公司理财》,机械工业出版社 ****管理统计**** David R.Anderson Dennis J.Sweeney Thomas A.William,《商务与经济统计(第七版)--经济教材译 丛》,机械工业出版社 David R.Anderson;Dennis J.Sweeney;Thomas A.William,《商务与经济统计(英文版.第8版)--21世纪 经典原版经济管理教材文库》,机械工业出版社 ****会计学**** 张启銮等,《会计学――工商管理硕士(MBA)系列教材》,大连理工大学出版社 Cktde P.Stickney Roman L.Weil,《财务会计(英文版第9版)--国际通用MBA教材》,机械工业出版社 Ronald W.Hilton,《管理会计(英文版.第三版)--国际通用MBA教材》,机械工业出版社 ****西方经济学**** [美]平狄克等:《微观经济学》,中国人民大学出版社 梁小民,《宏观经济学》,中国社会科学出版社 梁小民,《西方经济学导论》,北京大学出版社 宋承先,《现代西方经济学》(宏观经济学),复旦大学出版社 [美] H.克雷格.彼得森等:《管理经济学》,中国人民大学出版社 [美]S.Charles Maurice & Christopher R.Thomas, ManagerialEconomics, 机械工业出版社,2000年影印 版 ****经济法**** 高程德,《经济法》(第九版),上海人民出版社 ****电子商务**** Daniel Amor,《电子商务:变革与演进--经济教材译丛》,机械工业出版社 ****组织理论**** [美]达夫特著,李维安译.《组织理论与设计精要》,机械工业出版社 [英]Bernard Burnes.《变革时代的管理》,云南大学出版社 [美]F·赫塞尔本等.未来的组织,四川人民出版社 ****质量管理**** 刘广第,《质量管理学(第二版)》,清华大学出版社 ****生产管理**** 陈荣秋、马士华,生产与运作管理,高等教育出版社,1999 潘家轺等,现代生产管理学,北京:清华大学出版社,1994 黄卫伟,生产与作业管理,北京:人民大学出版社,1997 ****项目管理**** 毕星、翟丽,《项目管理》,复旦大学出版社,2000 [美]杰克.吉多等著, 《成功的项目管理》,机械工业出版社,1999白思俊主编, 《现代项目管理》(上,中,下),机械工业出版社,2002 股票入门书籍推荐 1.道氏理论 《股市晴雨表》(美)汉密尔顿 2.K线 《股票K线战法》(美)史蒂夫·尼森 《日本蜡烛图技术》(美)史蒂夫·尼森 3。波浪理论 《波浪原理》(美)艾略特 4。混沌和分形 《证券混沌操作法》(美)比尔。廉姆斯 《资本市场的混沌与秩序》(美)埃德加。E。彼得斯 《分形市场分析--将混沌理论应用到投资与经济理论上》 埃德加•E•彼得斯 5。交易心理 《直觉交易商》(美)罗伯特。库佩尔 《冷静自信的交易策略》(美)理查。迈克尔 《逆向思考的艺术》(美)汉弗莱•B•尼尔 《逆向投资策略》(美)戴维•德瑞曼 《大众心理与走势预测》(美)托尼•普卢默 6。技术方法 《股市趋势技术分析》(美)约翰.墨吉 《期货市场技术分析》(美)约翰•墨菲 《期货交易技术分析》(美)施威格 《股票操作学》(台湾)张龄松 《短线交易秘诀》(美)拉瑞。威谦姆斯 《专业投机原理》(美)维克多•斯波朗迪 《技术分析精论》(美)马丁。J。普林格 《股市无敌》(美)小理查德阿姆斯 7。系统方法 《通向金融王国的自由之路》(美)范。K。撒普 《系统交易方法》 波涛 《证券期货投资计算机化技术分析原理》 波涛 8。理念 《克罗谈投资策略》 斯坦利.克罗 《期货交易策略》 斯坦利.克罗 《投机智慧》(香港)许沂光 《华尔街点金人》(美)施威格 《美国期货专家经验谈》(美)威可夫 9。人物传记 《股票作手回忆录》(美)爱德温.李费佛 《交易冠军》 马丁.舒华兹 《投机生涯》(美)维克多。尼德霍夫 作为一个没有做过股票的朋友,如果现在开始准备做股票了 我觉得第一步要知道做股票的游戏规则,就是怎么玩 记得《专业投机原理》的开篇讲了一个很生动的故事“赌博例” 乔是扑克好手,但他却在一次和农民玩的扑克赌局中输的身无分文 不是农民的技巧有多高,一切只因为乔不了解当地扑克的游戏规则 那么,做股票也是同样的道理 你首先要充分了解整个股票的操作流程和交易规则 越详细越具体越好 例如,怎么买入怎么卖出怎么参与配股,st和非st有什么区别,代星号的st又代表什么意思,具体的成交顺序,交易时间,买入最小单位是1手,卖出可以零碎,1手表示多少股,怎么参与新股配售,各项交易费用,交易者的权利,证券公司对你应尽的义务。。。。还有很多很多的必备知识,我不再一一举例 反正个人认为 最好能做到象营业部大厅里的咨询小姐那样的了解程度就可以了 对于这些股票交易知识的资料可以问问自己开户的营业部有没有 也可以到网上搜索查找一下 新华书店应该也有这样子的书 不过可能不容易找到 印象里,《炒股就这么几招》的前面几册曾有比较详细的介绍,适合新股民看看 呵呵,其实有些老股民也应该补补课 (二)在掌握了基本的交易知识以后,下面就可以开始交易技能的学习第一本书很重要 因为到此为止,你或许还是一张白纸 白纸上落的第一笔,在某种程度上预示着以后发展的方向 我考虑了许久 决定推荐《股市操练大全》 这一推荐可能会引起争议 因为多数读过书的交易老手都不建议看国内的书 甚至把国内书定为糟粕 但我不这么认为 国内书总体格局小,却也不乏好书 在入门书上一下子就去读国外书容易发生断层 这套书非常本土化,使人产生很强的亲切感 尤其是新颖的写作模式让人有不忍释卷的冲动 我不打算用自己的语言来多介绍这套书 想偷个懒,贴这套书的目录和简介 股市操练大全》作者自述: 参与股市的人都知道,股市如战场,输赢全在一瞬间。为了把握股市胜机,很多人忙着听股评,参加股市沙龙,钻研股票操作书,可谓乐此不疲。但是,令人遗憾的是,最终效果并不理想。一场股市风暴来临,大多数人仍然免不了被套的结局。那么,是什么原因造成这种状况呢?我们以为,其真正的原因是股民在学习股票操作技巧时缺少了“强化训练”这一环节。从现代教学理论来说,人们要掌握一门知识和技巧,强化训练是必不可少的。譬如,要掌握 开车技术,就先要在练车场接受一套严格的驾驶技术的训练,只有在练车场过关后,才能正式到马路上开车;又如,学生参加高考,事先都要有针对性复习,做大量练习题,并进行反复多次的模拟考试训练,高考才能取得好成绩。但是,奇怪的是,这种在现代教育中屡试不爽的强化训练,一到股市就被人遗忘了,丢弃在一边。谓子不信,请看:有的人买卖股票只凭自己的感觉,有的人买卖股票完全依赖于股评,有的人买卖股票跟着市场消息转,等等。这里面不要说什么强化训练,恐怕连基本的操作原则都没有。虽然也有人买卖股票时,阅读了不少股票操作技巧方面的书,但那也只是理论上的学习,自己并没有做过大量有针对性的练习,因此,也就谈不上接受了股市操作的强化训练。于是乎,作为现代教育中最重要的强化训练,在股市中完全没有了踪影。正因为这个原因,许多人买卖股票时失去了方向,陷入了屡买屡套,屡战屡败的怪圈。可见,缺少股市操作强化训练,对投资者,尤其是对广大中小散户会造成多么严重的伤害。 股市操作强化训练的重要性这里不再多说了。但是如何开展股市操作强化训练呢?首先要有这方面的参考书籍,否则一切就无从谈起。而迄今为止,图书市场上还没有这样的书出现。别看现在的股票书如汗牛充栋,多得令人目不暇接,如你要想找一本用于股市操练的书,踏破铁鞋无觅处。有鉴于此,我们在上海三联书店的大力支持下,组织有关专家和股市操盘手按照现代教育强化训练的理论,并根据沪深股市实际情况,设计编写了中国股市第一本强化训练习题集。该习题集采用了“先易后难,循序渐进,反复比较,集中做题”的方式,对股民进行强化训练,以此来帮助投资者真正掌握股市操作技巧,达到熟能生巧、运用自如的目的。我们在设计这本股市操作强化训练习题集时,注意到很多投资者是新入市不久的股民,他们对股市操作技巧知之甚少。因此本书编写的格调又有别于类似学生复习迎考习题集那种编写方式,在指导读者做习题的同时,兼顾了股市操作知识的介绍,并把它系统化、条理化。读者如果把书中习题与参考答案连起来阅读,它就成了一本浅近易懂,专事介绍股市操作技巧的科普读物。所以,本书实际上是一本兼有股市知识、技巧学习和股市操作强化训练两方面用途的实用性操盘工具书。 作为用于强化训练类的书,最大特点是:案例多、习题多。本书也不例外。本书的图形就有600多幅,它几乎把沪深股市10年来有代表性的K线走势和技术图形都选了进去;习题100多个,概念题、选择题、是非题、.简答题、问答题,应有尽有。另外,每幅图例都有专家、名人的精辟分析,每道题都有值得投资者一读的参考答案,内容十分丰富。为了发挥本书的效用、读者在阅读本书时,不能只看不练,而要多做练习。因为只有通过练习,才能发现问题,纠正操作中的错误;只有通过练习,才会驾轻就熟,把股市操作技巧用好用活。在学中练,在练中学,反复比较,反复运用,唯此才能达到股市操作强化训练的预期目的。这点必须引起投资者的高度重视。 本书在选用沪深K线图走势时,将个股的名称、价位作了删除(只有极少数图案中的个股名称、价位,因书中内容需要作了保留),这主要是为了避免因投资者对某些股票的偏爱和价位上的错觉,造成对K线技术图形上的买卖信号作出错误判断,而所作的一种特殊处理。读者对此不要有什么误解。从K线、技术图形操作理论上来说,任何股票都有强弱转化的时候,这里面股质本身是次要的,重要的是我们能不能在技术上找到买卖它们的理由和依据。当K线和技术图形走势告诉我们可以买的时候,无论是绩优股、绩差股,无论高价股、低价股,都要敢于买。反之,则要敢于卖。沪深股市上就有不少投资者因为对绩优股的偏爱,当它们股价高高在上时,仍然不断追涨,对K线、技术图形发出的见顶信号置之不理,结果被深套,损失惨重。这个教训是十分深刻的。因此,我们认为删除图形上的个股名称、价位,对促使投资者养成按图形信号操作的习惯有很大的好处。 本书每一章结束都安排了两套测验题,这些测验题既是对读者掌握K线或技术图形操作技巧熟练程度的检测,也是对本书内容的一个总结。因此,读者必须认真待之。测验题每一小 栏都有得分多少说明,并附有参考答案。读者做完测验题后可自行打分。其评分标准和一般考试评分标准相同。60分为及格,80分以上为良好,90分以上为优秀。如第一次测验成绩较差者,说明你对股市操盘技巧还没掌握,参于股市操作的风险就较大。此时,最佳的办法是暂停股市买卖活动,重新学习,反复练习,待第二次测验成绩提高后再积极参于股市运作,这样投资收益就会有很大的提高。书读到这里,应该开始看国外的经典了 呵呵,所谓“取法乎上,得乎其中” 如果老是在国内的书里打转,恐怕会限制自己的视野,无法往高层进阶 有时候,决定你是否能在市场里成为赢家的关键,不是在于你有多高的技巧,而更重要的是你的理念 《期货市场技术分析》这是一本很适合初学者看的技术分析的经典书籍 讲得不深,但很透彻,把每项技术分析的重点都拎了出来 加上丁圣元先生流畅贴切的翻译 使之成为中文版技术分析书籍中难得的好书 当然,这本书的价格可能稍贵 如果想追求性价比的朋友,可以选择该书的更新版《金融市场技术分析》 《金融市场技术分析》是《期货市场技术分析》的新版,增加了一些内容 翻译者是陈鑫。陈鑫先生也是国内翻译交易书籍的一把好手 不过,对于我个人来说 捧着《期货市场技术分析》这本大书 好像感觉更舒心和温暖些 接下去要看的书是《股市趋势技术分析》 记得我曾经说过这样的话,如果要出远门,而只能让我带一本交易书籍在身上,我会选择《期货交易策略》 现在我要说的是,如果要拿走我书架上的交易书籍,而只能留一本下来,我会选择把《股市趋势技术分析》留下 这本书可以称之为伟大,她的伟大主要体现在 1)用具体图表走势对道氏理论进行了细致和精彩的描述 道氏理论之重要,交易者皆知,可惜很多人看道氏理论觉得空洞和枯燥,不过《股市趋势技术分析》却能给你一种豁然开朗的欣喜;她不仅能让你领略到道氏理论的精髓,更可以给你一种对道氏理论震撼性的再认识。我无意吹捧该书,因为这些都是我当年读书时的感觉。2)图表分析的巅峰之作 在我印象里,应该没有哪本书论述图表分析能达到《股市趋势技术分析》的高度 该书在图表分析这一项,不仅告诉我们应该如何去分析图表中各种力量的消长,平衡,突变,尺度,及我们就此所该做的应对策略和手段,她更引导我们去思考探寻隐藏在图表背后的东西,即引起这些变化的实质。我觉得,如果我们能领会该书的要义,那当面对任何一张图表的时候,我们都不仅能够看出“然”,且会明了“所以然”。 《股市趋势技术分析》的中文版有个缺憾,就是翻译好像并不理想(李先生翻译)这对初学者的阅读会带来些障碍 但在读完上面几本书后,我还是愿意向初学者推荐该书 因为读这本书能够大幅度提升你的格局,包括交易思想,交易策略以及技术分析的大局观 在读完上面这些书后 你可能在脑子里已经形成了初步的交易思想 并且对各种技术分析方法有了总体上的认识 但如果你打算现在就去进行交易的话 那还是远远不够的 面对宏大的市场 你会不知从何下手 所以,接下来你需要做的是构建属于自己的交易体系 这是一个成熟的交易者必须要去做的一件事 当然,在开始做此事以前 因为很可能你并不知道如何取舍,还不知道哪些东西是适合你的 所以你需要看一些书 希望这些书能够给你一些启发,便于让你作出你的抉择 这些书包括 1)《股票作手回忆录》 2)《交易冠军》 3)《华尔街操盘高手》(金融怪杰)4)《华尔街点金人》(新金融怪杰)5)《操盘建议--全球顶尖交易员的成功实践和心路历程》 6)《美国期货专家经验谈》 7)《攻守四大战技》 8)《投机智慧》 9)《期货交易策略》 10)《刀疤老二的文章》 金融学入门书籍 奋斗 2008-05-10 12:44:29 阅读10 评论0 字号:大中小 1、《经济学原理》 N·格里高利·曼昆(N.Gregory Mankiw),中国人民大学出版社。 2、《应用经济计量学》 拉姆·拉玛纳山(Ramu Ramanathan),机械工业出版社。 3、《金融经济学》(德)于尔根·艾希贝格尔,西南财经大学出版社。 4、《货币金融学》 弗雷德里克·S·米什金(Fredcric S.Mishkin),中国人民大学出版社。 5、《金融学》 兹维·博迪、罗伯特·默顿,中国人民大学出版社。 6、《国际经济学》(美)保罗·克鲁德曼,中国人民大学出版社。 7、《比较金融系统》 富兰克林·艾伦,王晋斌等译,中国人民大学出版社。 8、《世界经济千年史》(英)安格斯·麦迪森(Angus Maddison),北京大学出版社。 9、《金融市场与金融机构基础》(美)弗朗哥·莫迪利亚尼,清华大学出版社1999版。 10、《微观银行学》弗雷克斯,西南财经大学出版社。 11、《公司理财》斯蒂芬·A·罗斯,机械工业出版社。 12、《投资学》兹维·博迪,中国人民大学出版社 先看《经济学原理》 然后学习一般的宏微观经济学和货币银行学,再补充一些公司理财、投资学这样的知识 下来就可以学习金融经济学了,但同时要学习计量经济学的课程,可以先学计量也可以与金融经济学同时学 上述课程学完之后就可以去看看专业一点的书籍了第二篇:金融学入门书籍[范文模版]
第三篇:管理学入门书籍[范文模版]
第四篇:股票入门书籍推荐
第五篇:金融学入门书籍