📄 哈哈哈,超酷汇编教程-- 简明x86汇编语言教程(4).txt
字号:
在某种编译模式[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程序实例编译失败的问题
地址转换
2EA8:D678 -> 物理的 3C0F8
694E:175A -> 物理的 6AC4A
26CF:8D5F -> 物理的 2FA4F
2B3C:D218 -> 物理的 385E8
453A:CFAD -> 物理的 5235D
728F:6578 -> 物理的 78E68
2933:31A6 -> 物理的 2C4D6
68E1:A7DC -> 物理的 735FC
编程
shl eax,4
add eax,bx
注意 编程问题答案并不唯一,但给出的这份参考答案应该已经是“优化到头”了。
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -