注册 登录  
 加关注
   显示下一条  |  关闭
温馨提示!由于新浪微博认证机制调整,您的新浪微博帐号绑定已过期,请重新绑定!立即重新绑定新浪微博》  |  关闭

风雨夜归人

专业收集资料,个人爱好!

 
 
 

日志

 
 

汇编教程  

2009-09-20 06:50:13|  分类: 汇编 |  标签: |举报 |字号 订阅

  下载LOFTER 我的照片书  |

汇编语言的准备知识--给初次接触汇编者

在接触到游戏修改后发现需要很多的汇编知识,于是找汇编基础知识恶补,到网上搜索到一篇不错的文章,给各位想我一样的初学者一起学习!

教程: 汇编语言的准备知识--给初次接触汇编者(1)

  汇编语言和CPU以及内存,端口等硬件知识是连在一起的. 这也是为什么汇编语言没有通用性的原因. 下面简单讲讲基本知识(针对INTEL x86及其兼容机)

  ============================

  x86汇编语言的指令,其操作对象是CPU上的寄存器,系统内存,或者立即数. 有些指令表面上没有操作数, 或者看上去缺少操作数, 其实该指令有内定的操作对象, 比如push指令, 一定是对SS:ESP指定的内存操作, 而cdq的操作对象一定是eax / edx.

  在汇编语言中,寄存器用名字来访问. CPU 寄存器有好几类, 分别有不同的用处:

  1. 通用寄存器:

  EAX,EBX,ECX,EDX,ESI,EDI,EBP,ESP(这个虽然通用,但很少被用做除了堆栈指针外的用途)

  

  这些32位可以被用作多种用途,但每一个都有"专长". EAX 是"累加器"(accumulator), 它是很多加法乘法指令的缺省寄存器. EBX 是"基地址"(base)寄存器, 在内存寻址时存放基地址. ECX 是计数器(counter), 是重复(REP)前缀指令和LOOP指令的内定计数器. EDX是...(忘了..哈哈)但它总是被用来放整数除法产生的余数. 这4个寄存器的低16位可以被单独访问,分别用AX,BX,CX和DX. AX又可以单独访问低8位(AL)和高8位(AH), BX,CX,DX也类似. 函数的返回值经常被放在EAX中.

  

  ESI/EDI分别叫做"源/目标索引寄存器"(source/destination index),因为在很多字符串操作指令中, DS:ESI指向源串,而ES:EDI指向目标串.

  EBP是"基址指针"(BASE POINTER), 它最经常被用作高级语言函数调用的"框架指针"(frame pointer). 在破解的时候,经常可以看见一个标准的函数起始代码:

  

  push ebp ;保存当前ebp

  mov ebp,esp ;EBP设为当前堆栈指针

  sub esp, xxx ;预留xxx字节给函数临时变量.

  ...

  

  这样一来,EBP 构成了该函数的一个框架, 在EBP上方分别是原来的EBP, 返回地址和参数. EBP下方则是临时变量. 函数返回时作 mov esp,ebp/pop ebp/ret 即可.

  

  ESP 专门用作堆栈指针.

  

  2. 段寄存器:

  CS(Code Segment,代码段) 指定当前执行的代码段. EIP (Instruction pointer, 指令指针)则指向该段中一个具体的指令. CS:EIP指向哪个指令, CPU 就执行它. 一般只能用jmp, ret, jnz, call 等指令来改变程序流程,而不能直接对它们赋值.

  DS(DATA SEGMENT, 数据段) 指定一个数据段. 注意:在当前的计算机系统中, 代码和数据没有本质差别, 都是一串二进制数, 区别只在于你如何用它. 例如, CS 制定的段总是被用作代码, 一般不能通过CS指定的地址去修改该段. 然而,你可以为同一个段申请一个数据段描述符"别名"而通过DS来访问/修改. 自修改代码的程序常如此做.

  ES,FS,GS 是辅助的段寄存器, 指定附加的数据段.

  SS(STACK SEGMENT)指定当前堆栈段. ESP 则指出该段中当前的堆栈顶. 所有push/pop 系列指令都只对SS:ESP指出的地址进行操作.

  

  3. 标志寄存器(EFLAGS):

  该寄存器有32位,组合了各个系统标志. EFLAGS一般不作为整体访问, 而只对单一的标志位感兴趣. 常用的标志有:

  

  进位标志C(CARRY), 在加法产生进位或减法有借位时置1, 否则为0.

  零标志Z(ZERO), 若运算结果为0则置1, 否则为0

  符号位S(SIGN), 若运算结果的最高位置1, 则该位也置1.

  溢出标志O(OVERFLOW), 若(带符号)运算结果超出可表示范围, 则置1.

  

  JXX 系列指令就是根据这些标志来决定是否要跳转, 从而实现条件分枝. 要注意,很多JXX 指令是等价的, 对应相同的机器码. 例如, JE 和JZ 是一样的,都是当Z=1是跳转. 只有JMP 是无条件跳转. JXX 指令分为两组, 分别用于无符号操作和带符号操作. JXX 后面的"XX" 有如下字母:

  

  无符号操作: 带符号操作:

  A = "ABOVE", 表示"高于" G = "GREATER", 表示"大于"

  B = "BELOW", 表示"低于" L = "LESS", 表示"小于"

  C = "CARRY", 表示"进位"或"借位" O = "OVERFLOW", 表示"溢出"

  S = "SIGN", 表示"负"

  通用符号:

  E = "EQUAL" 表示"等于", 等价于Z (ZERO)

  N = "NOT" 表示"非", 即标志没有置位. 如JNZ "如果Z没有置位则跳转"

  Z = "ZERO", 与E同.

  

  如果仔细想一想,就会发现 JA = JNBE, JAE = JNB, JBE = JNA, JG = JNLE, JGE= JNL, JL= JNGE, ....

  

  4. 端口

  端口是直接和外部设备通讯的地方。外设接入系统后,系统就会把外设的数据接口映射到特定的端口地址空间,这样,从该端口读入数据就是从外设读入数据,而向外设写入数据就是向端口写入数据。当然这一切都必须遵循外设的工作方式。端口的地址空间与内存地址空间无关,系统总共提供对64K个8位端口的访问,编号0-65535. 相邻的8位端口可以组成成一个16位端口,相邻的16位端口可以组成一个32位端口。端口输入输出由指令IN,OUT,INS和OUTS实现,具体可参考汇编语言书籍。

 

       

教程: 汇编语言的准备知识-给初次接触汇编者2

  汇编指令的操作数可以是内存中的数据, 如何让程序从内存中正确取得所需要的数据就是对内存的寻址。

  INTEL 的CPU 可以工作在两种寻址模式:实模式和保护模式。 前者已经过时,就不讲了, WINDOWS 现在是32位保护模式的系统, PE 文件就基本是运行在一个32位线性地址空间, 所以这里就只介绍32位线性空间的寻址方式。

  其实线性地址的概念是很直观的, 就想象一系列字节排成一长队,第一个字节编号为0, 第二个编号位1, 。。。。 一直到4294967295(十六进制FFFFFFFF,这是32位二进制数所能表达的最大值了)。 这已经有4GB的容量! 足够容纳一个程序所有的代码和数据。 当然, 这并不表示你的机器有那么多内存。 物理内存的管理和分配是很复杂的内容, 初学者不必在意, 总之, 从程序本身的角度看, 就好象是在那么大的内存中。

  在INTEL系统中, 内存地址总是由"段选择符:有效地址"的方式给出。段选择符(SELECTOR)存放在某一个段寄存器中, 有效地址则可由不同的方式给出。 段选择符通过检索段描述符确定段的起始地址, 长度(又称段限制), 粒度, 存取权限, 访问性质等。 先不用深究这些, 只要知道段选择符可以确定段的性质就行了。 一旦由选择符确定了段, 有效地址相对于段的基地址开始算。 比如由选择符1A7选择的数据段, 其基地址是400000, 把1A7 装入DS中, 就确定使用该数据段。 DS:0 就指向线性地址400000。 DS:1F5278 就指向线性地址5E5278。 我们在一般情况下, 看不到也不需要看到段的起始地址, 只需要关心在该段中的有效地址就行了。 在32位系统中, 有效地址也是由32位数字表示, 就是说, 只要有一个段就足以涵盖4GB线性地址空间, 为什么还要有不同的段选择符呢? 正如前面所说的, 这是为了对数据进行不同性质的访问。 非法的访问将产生异常中断, 而这正是保护模式的核心内容, 是构造优先级和多任务系统的基础。 这里有涉及到很多深层的东西, 初学者先可不必理会。

  有效地址的计算方式是: 基址 间址*比例因子 偏移量。 这些量都是指段内的相对于段起始地址的量度, 和段的起始地址没有关系。 比如, 基址=100000, 间址=400, 比例因子=4, 偏移量=20000, 则有效地址为:

  100000 400*4 20000=100000 1000 20000=121000。 对应的线性地址是400000 121000=521000。 (注意, 都是十六进制数)。

  基址可以放在任何32位通用寄存器中, 间址也可以放在除ESP外的任何一个通用寄存器中。 比例因子可以是1, 2, 4 或8。 偏移量是立即数。 如: [EBP EDX*8 200]就是一个有效的有效地址表达式。 当然, 多数情况下用不着这么复杂, 间址,比例因子和偏移量不一定要出现。

  内存的基本单位是字节(BYTE)。 每个字节是8个二进制位, 所以每个字节能表示的最大的数是11111111, 即十进制的255。 一般来说, 用十六进制比较方便, 因为每4个二进制位刚好等于1个十六进制位, 11111111b = 0xFF。 内存中的字节是连续存放的, 两个字节构成一个字(WORD), 两个字构成一个双字(DWORD)。 在INTEL架构中, 采用small endian格式, 即在内存中,高位字节在低位字节后面。 举例说明:十六进制数803E7D0C, 每两位是一个字节, 在内存中的形式是: 0C 7D 3E 80。 在32位寄存器中则是正常形式,如在EAX就是803E7D0C。 当我们的形式地址指向这个数的时候,实际上是指向第一个字节,即0C。 我们可以指定访问长度是字节, 字或者双字。 假设DS:[EDX]指向第一个字节0C:

  mov AL, byte ptr DS:[EDX] ;把字节0C存入AL

  mov AX, word ptr DS:[EDX] ;把字7D0C存入AX

  mov EAX, dword ptr DS:[EDX] ;把双字803E7D0C存入EAX

  在段的属性中,有一个就是缺省访问宽度。如果缺省访问宽度为双字(在32位系统中经常如此),那么要进行字节或字的访问,就必须用byte/word ptr显式地指明。

  缺省段选择:如果指令中只有作为段内偏移的有效地址,而没有指明在哪一个段里的时候,有如下规则:

  如果用ebp和esp作为基址或间址,则认为是在SS确定的段中;

  其他情况,都认为是在DS确定的段中。

  如果想打破这个规则,就必须使用段超越前缀。举例如下:

  mov eax, dword ptr [edx] ;缺省使用DS,把DS:[EDX]指向的双字送入eax

  mov ebx, dword ptr ES:[EDX] ;使用ES:段超越前缀,把ES:[EDX]指向的双字送入ebx

  堆栈:

  堆栈是一种数据结构,严格地应该叫做“栈”。“堆”是另一种类似但不同的结构。SS 和 ESP 是INTEL对栈这种数据结构的硬件支持。push/pop指令是专门针对栈结构的特定操作。SS指定一个段为栈段,ESP则指出当前的栈顶。push xxx 指令作如下操作:

  把ESP的值减去4;

  把xxx存入SS:[ESP]指向的内存单元。

  这样,esp的值减小了4,并且SS:[ESP]指向新压入的xxx。 所以栈是“倒着长”的,从高地址向低地址方向扩展。pop yyy 指令做相反的操作,把SS:[ESP]指向的双字送到yyy指定的寄存器或内存单元,然后把esp的值加上4。这时,认为该值已被弹出,不再在栈上了,因为它虽然还暂时存在在原来的栈顶位置,但下一个push操作就会把它覆盖。因此,在栈段中地址低于esp的内存单元中的数据均被认为是未定义的。

  最后,有一个要注意的事实是,汇编语言是面向机器的,指令和机器码基本上是一一对应的,所以它们的实现取决于硬件。有些看似合理的指令实际上是不存在的,比如:

  mov DS:[edx], ds:[ecx] ;内存单元之间不能直接传送

  mov DS, 1A7 ;段寄存器不能直接由立即数赋值

  mov EIP, 3D4E7 ;不能对指令指针直接操作。

       

教程: 汇编语言的准备知识-给初次接触汇编者3

  “汇编语言”作为一门语言,对应于高级语言的编译器,我们需要一个“汇编器”来把汇编语言原文件汇编成机器可执行的代码。高级的汇编器如MASM, TASM等等为我们写汇编程序提供了很多类似于高级语言的特征,比如结构化、抽象等。在这样的环境中编写的汇编程序,有很大一部分是面向汇编器的伪指令,已经类同于高级语言。现在的汇编环境已经如此高级,即使全部用汇编语言来编写windows的应用程序也是可行的,但这不是汇编语言的长处。汇编语言的长处在于编写高效且需要对机器硬件精确控制的程序。而且我想这里的人学习汇编的目的多半是为了在破解时看懂反汇编代码,很少有人真的要拿汇编语言编程序吧?(汗......)

  好了,言归正传。大多数汇编语言书都是面向汇编语言编程的,我的帖是面向机器和反汇编的,希望能起到相辅相成的作用。有了前面两篇的基础,汇编语言书上对大多数指令的介绍应该能够看懂、理解了。这里再讲一讲一些常见而操作比较复杂的指令。我这里讲的都是机器的硬指令,不针对任何汇编器。

  无条件转移指令jmp:

  这种跳转指令有三种方式:短(short),近(near)和远(far)。短是指要跳至的目标地址与当前地址前后相差不超过128字节。近是指跳转的目标地址与当前地址在用一个段内,即CS的值不变,只改变EIP的值。远指跳到另一个代码段去执行,CS/EIP都要改变。短和近在编码上有所不同,在汇编指令中一般很少显式指定,只要写 jmp 目标地址,几乎任何汇编器都会根据目标地址的距离采用适当的编码。远转移在32位系统中很少见到,原因前面已经讲过,由于有足够的线性空间,一个程序很少需要两个代码段,就连用到的系统模块也被映射到同一个地址空间。

  jmp的操作数自然是目标地址,这个指令支持直接寻址和间接寻址。间接寻址又可分为寄存器间接寻址和内存间接寻址。举例如下(32位系统):

  jmp 8E347D60 ;直接寻址段内跳转

  jmp EBX ;寄存器间接寻址:只能段内跳转

  jmp dword ptr [EBX] ;内存间接寻址,段内跳转

  jmp dword ptr [00903DEC] ;同上

  jmp fward ptr [00903DF0] ;内存间接寻址,段间跳转

  解释:

  在32位系统中,完整目标地址由16位段选择子和32位偏移量组成。因为寄存器的宽度是32位,因此寄存器间接寻址只能给出32位偏移量,所以只能是段内近转移。在内存间接寻址时,指令后面是方括号内的有效地址,在这个地址上存放跳转的目标地址。比如,在[00903DEC]处有如下数据:7C 82 59 00 A7 01 85 65 9F 01

  内存字节是连续存放的,如何确定取多少作为目标地址呢?dword ptr 指明该有效地址指明的是双字,所以取

  0059827C作段内跳转。反之,fward ptr 指明后面的有效地址是指向48位完全地址,所以取19F:658501A7 做远跳转。

  注意:在保护模式下,如果段间转移涉及优先级的变化,则有一系列复杂的保护检查,现在可不加理会。将来等各位功力提升以后可以自己去学习。

  条件转移指令jxx:只能作段内转移,且只支持直接寻址。

  =========================================

  调用指令CALL:

  Call的寻址方式与jmp基本相同,但为了从子程序返回,该指令在跳转以前会把紧接着它的下一条指令的地址压进堆栈。如果是段内调用(目标地址是32位偏移量),则压入的也只是一个偏移量。如果是段间调用(目标地址是48位全地址),则也压入下一条指令的完全地址。同样,如果段间转移涉及优先级的变化,则有一系列复杂的保护检查。

  与之对应retn/retf指令则从子程序返回。它从堆栈上取得返回地址(是call指令压进去的)并跳到该地址执行。retn取32位偏移量作段内返回,retf取48位全地址作段间返回。retn/f 还可以跟一个立即数作为操作数,该数实际上是从堆栈上传给子程序的参数的个数(以字计)返回后自动把堆栈指针esp加上指定的数*2,从而丢弃堆栈中的参数。这里具体的细节留待下一篇讲述。

  虽然call和ret设计为一起工作,但它们之间没有必然的联系。就是说,如果你直接用push指令向堆栈中压入一个数,然后执行ret,他同样会把你压入的数作为返回地址,而跳到那里去执行。这种非正常的流程转移可以被用作反跟踪手段。

  ==========================================

  中断指令INT n

  在保护模式下,这个指令必定会被操作系统截获。在一般的PE程序中,这个指令已经不太见到了,而在DOS时代,中断是调用操作系统和BIOS的重要途径。现在的程序可以文质彬彬地用名字来调用windows功能,如 call user32!getwindowtexta。从程序角度看,INT指令把当前的标志寄存器先压入堆栈,然后把下一条指令的完全地址也压入堆栈,最后根据操作数n来检索“中断描述符表”,试图转移到相应的中断服务程序去执行。通常,中断服务程序都是操作系统的核心代码,必然会涉及到优先级转换和保护性检查、堆栈切换等等,细节可以看一些高级的教程。

  与之相应的中断返回指令IRET做相反的操作。它从堆栈上取得返回地址,并用来设置CS:EIP,然后从堆栈中弹出标志寄存器。注意,堆栈上的标志寄存器值可能已经被中断服务程序所改变,通常是进位标志C, 用来表示功能是否正常完成。同样的,IRET也不一定非要和INT指令对应,你可以自己在堆栈上压入标志和地址,然后执行IRET来实现流程转移。实际上,多任务操作系统常用此伎俩来实现任务转换。

  广义的中断是一个很大的话题,有兴趣可以去查阅系统设计的书籍。

  ============================================

  装入全指针指令LDS,LES,LFS,LGS,LSS

  这些指令有两个操作数。第一个是一个通用寄存器,第二个操作数是一个有效地址。指令从该地址取得48位全指针,将选择符装入相应的段寄存器,而将32位偏移量装入指定的通用寄存器。注意在内存中,指针的存放形式总是32位偏移量在前面,16位选择符在后面。装入指针以后,就可以用DS:[ESI]这样的形式来访问指针指向的数据了。

  ============================================

  字符串操作指令

  这里包括CMPS,SCAS,LODS,STOS,MOVS,INS和OUTS等。这些指令有一个共同的特点,就是没有显式的操作数,而由硬件规定使用DS:[ESI]指向源字符串,用ES:[EDI]指向目的字符串,用AL/AX/EAX做暂存。这是硬件规定的,所以在使用这些指令之前一定要设好相应的指针。

  这里每一个指令都有3种宽度形式,如CMPSB(字节比较)、CMPSW(字比较)、CMPSD(双字比较)等。

  CMPSB:比较源字符串和目标字符串的第一个字符。若相等则Z标志置1。若不等则Z标志置0。指令执行完后,ESI 和EDI都自动加1,指向源/目标串的下一个字符。如果用CMPSW,则比较一个字,ESI/EDI自动加2以指向下一个字。

  如果用CMPSD,则比较一个双字,ESI/EDI自动加4以指向下一个双字。(在这一点上这些指令都一样,不再赘述)

  SCAB/W/D 把AL/AX/EAX中的数值与目标串中的一个字符/字/双字比较。

  LODSB/W/D 把源字符串中的一个字符/字/双字送入AL/AX/EAX

  STOSB/W/D 把AL/AX/EAX中的直送入目标字符串中

  MOVSB/W/D 把源字符串中的字符/字/双字复制到目标字符串

  INSB/W/D 从指定的端口读入字符/字/双字到目标字符串中,端口号码由DX寄存器指定。

  OUTSB/W/D 把源字符串中的字符/字/双字送到指定的端口,端口号码由DX寄存器指定。

  串操作指令经常和重复前缀REP和循环指令LOOP结合使用以完成对整个字符串的操作。而REP前缀和LOOP指令都有硬件规定用ECX做循环计数器。举例:

  LDS ESI,SRC_STR_PTR

  LES EDI,DST_STR_PTR

  MOV ECX,200

  REP MOVSD

  上面的代码从SRC_STR拷贝200个双字到DST_STR. 细节是:REP前缀先检查ECX是否为0,若否则执行一次MOVSD,ECX自动减1,然后执行第二轮检查、执行......直到发现ECX=0便不再执行MOVSD,结束重复而执行下面的指令。

  LDS ESI,SRC_STR_PTR

  MOV ECX,100

  LOOP1:

  LODSW

  .... (deal with value in AX)

  LOOP LOOP1

  .....

  从SRC_STR处理100个字。同样,LOOP指令先判断ECX是否为零,来决定是否循环。每循环一轮ECX自动减1。

  REP和LOOP 都可以加上条件,变成REPZ/REPNZ 和 LOOPZ/LOOPNZ. 这是除了ECX外,还用检查零标志Z. REPZ 和LOOPZ在Z为1时继续循环,否则退出循环,即使ECX不为0。REPNZ/LOOPNZ则相反。

  ====================================================

       

教程: 汇编语言的准备知识-给初次接触汇编者4

  高级语言程序的汇编解析

  在高级语言中,如C和PASCAL等等,我们不再直接对硬件资源进行操作,而是面向于问题的解决,这主要体现在数据抽象化和程序的结构化。例如我们用变量名来存取数据,而不再关心这个数据究竟在内存的什么地方。这样,对硬件资源的使用方式完全交给了编译器去处理。不过,一些基本的规则还是存在的,而且大多数编译器都遵循一些规范,这使得我们在阅读反汇编代码的时候日子好过一点。这里主要讲讲汇编代码中一些和高级语言对应的地方。

  1. 普通变量。通常声明的变量是存放在内存中的。编译器把变量名和一个内存地址联系起来(这里要注意的是,所谓的“确定的地址”是对编译器而言在编译阶段算出的一个临时的地址。在连接成可执行文件并加载到内存中执行的时候要进行重定位等一系列调整,才生成一个实时的内存地址,不过这并不影响程序的逻辑,所以先不必太在意这些细节,只要知道所有的函数名字和变量名字都对应一个内存的地址就行了),所以变量名在汇编代码中就表现为一个有效地址,就是放在方括号中的操作数。例如,在C文件中声明:

  int my_age;

  这个整型的变量就存在一个特定的内存位置。语句 my_age= 32; 在反汇编代码中可能表现为:

  mov word ptr [007E85DA], 20

  所以在方括号中的有效地址对应的是变量名。又如:

  char my_name[11] = "lianzi2000";

  这样的说明也确定了一个地址,对应于my_name. 假设地址是007E85DC,则内存中[007E85DC]='l',[007E85DD]='i', etc. 对my_name的访问也就是对这地址处的数据访问。

  指针变量其本身也同样对应一个地址,因为它本身也是一个变量。如:

  char *your_name;

  这时也确定变量"your_name"对应一个内存地址,假设为007E85F0. 语句your_name=my_name;很可能表现为:

  mov [007E85F0], 007E85DC ;your_name的内容是my_name的地址。

  2. 寄存器变量

  在C和C 中允许说明寄存器变量。register int i; 指明i是寄存器存放的整型变量。通常,编译器都把寄存器变量放在esi和edi中。寄存器是在cpu内部的结构,对它的访问要比内存快得多,所以把频繁使用的变量放在寄存器中可以提高程序执行速度。

  3. 数组

  不管是多少维的数组,在内存中总是把所有的元素都连续存放,所以在内存中总是一维的。例如,int i_array[2][3]; 在内存确定了一个地址,从该地址开始的12个字节用来存贮该数组的元素。所以变量名i_array对应着该数组的起始地址,也即是指向数组的第一个元素。存放的顺序一般是i_array[0][0],[0][1],[0][2],[1][0],[1][1],[1][2] 即最右边的下标变化最快。当需要访问某个元素时,程序就会从多维索引值换算成一维索引,如访问i_array[1][1],换算成内存中的一维索引值就是1*3 1=4.这种换算可能在编译的时候就可以确定,也可能要到运行时才可以确定。无论如何,如果我们把i_array对应的地址装入一个通用寄存器作为基址,则对数组元素的访问就是一个计算有效地址的问题:

  ; i_array[1][1]=0x16

  lea ebx,xxxxxxxx ;i_array 对应的地址装入ebx

  mov edx,04 ;访问i_array[1][1],编译时就已经确定

  mov word ptr [ebx edx*2], 16 ;

  当然,取决于不同的编译器和程序上下文,具体实现可能不同,但这种基本的形式是确定的。从这里也可以看到比例因子的作用(还记得比例因子的取值为1,2,4或8吗?),因为在目前的系统中简单变量总是占据1,2,4或者8个字节的长度,所以比例因子的存在为在内存中的查表操作提供了极大方便。

  4. 结构和对象

  结构和对象的成员在内存中也都连续存放,但有时为了在字边界或双字边界对齐,可能有些微调整,所以要确定对象的大小应该用sizeof操作符而不应该把成员的大小相加来计算。当我们声明一个结构变量或初始化一个对象时,这个结构变量和对象的名字也对应一个内存地址。举例说明:

  struct tag_info_struct

  {

  int age;

  int sex;

  float height;

  float weight;

  } marry;

  变量marry就对应一个内存地址。在这个地址开始,有足够多的字节(sizeof(marry))容纳所有的成员。每一个成员则对应一个相对于这个地址的偏移量。这里假设此结构中所有的成员都连续存放,则age的相对地址为0,sex为2, height 为4,weight为8。

  ; marry.sex=0;

  lea ebx,xxxxxxxx ;marry 对应的内存地址

  mov word ptr [ebx 2], 0

  ......

  对象的情况基本相同。注意成员函数具体的实现在代码段中,在对象中存放的是一个指向该函数的指针。

  5. 函数调用

  一个函数在被定义时,也确定一个内存地址对应于函数名字。如:

  long comb(int m, int n)

  {

  long temp;

  .....

  return temp;

  }

  这样,函数comb就对应一个内存地址。对它的调用表现为:

  CALL xxxxxxxx ;comb对应的地址。这个函数需要两个整型参数,就通过堆栈来传递:

  ;lresult=comb(2,3);

  push 3

  push 2

  call xxxxxxxx

  mov dword ptr [yyyyyyyy], eax ;yyyyyyyy是长整型变量lresult的地址

  这里请注意两点。第一,在C语言中,参数的压栈顺序是和参数顺序相反的,即后面的参数先压栈,所以先执行push 3. 第二,在我们讨论的32位系统中,如果不指明参数类型,缺省的情况就是压入32位双字。因此,两个push指令总共压入了两个双字,即8个字节的数据。然后执行call指令。call 指令又把返回地址,即下一条指令(mov dword ptr....)的32位地址压入,然后跳转到xxxxxxxx去执行。

  在comb子程序入口处(xxxxxxxx),堆栈的状态是这样的:

  03000000 (请回忆small endian 格式)

  02000000

  yyyyyyyy <--ESP 指向返回地址

  前面讲过,子程序的标准起始代码是这样的:

  push ebp ;保存原先的ebp

  mov ebp, esp;建立框架指针

  sub esp, XXX;给临时变量预留空间

  .....

  执行push ebp之后,堆栈如下:

  03000000

  02000000

  yyyyyyyy

  old ebp <---- esp 指向原来的ebp

  执行mov ebp,esp之后,ebp 和esp 都指向原来的ebp. 然后sub esp, xxx 给临时变量留空间。这里,只有一个临时变量temp,是一个长整数,需要4个字节,所以xxx=4。这样就建立了这个子程序的框架:

  03000000

  02000000

  yyyyyyyy

  old ebp <---- 当前ebp指向这里

  temp

  所以子程序可以用[ebp 8]取得第一参数(m),用[ebp C]来取得第二参数(n),以此类推。临时变量则都在ebp下面,如这里的temp就对应于[ebp-4].

  子程序执行到最后,要返回temp的值:

  mov eax,[ebp-04]

  然后执行相反的操作以撤销框架:

  mov esp,ebp ;这时esp 和ebp都指向old ebp,临时变量已经被撤销

  pop ebp ;撤销框架指针,恢复原ebp.

  这是esp指向返回地址。紧接的retn指令返回主程序:

  retn 4

  该指令从堆栈弹出返回地址装入EIP,从而返回到主程序去执行call后面的指令。同时调整esp(esp=esp 4*2),从而撤销参数,使堆栈恢复到调用子程序以前的状态,这就是堆栈的平衡。调用子程序前后总是应该维持堆栈的平衡。从这里也可以看到,临时变量temp已经随着子程序的返回而消失,所以试图返回一个指向临时变量的指针是非法的。

  为了更好地支持高级语言,INTEL还提供了指令Enter 和Leave 来自动完成框架的建立和撤销。Enter 接受两个操作数,第一个指明给临时变量预留的字节数,第二个是子程序嵌套调用层数,一般都为0。enter xxx,0 相当于:

  push ebp

  mov ebp,esp

  sub esp,xxx

  leave 则相当于:

  mov esp,ebp

  pop ebp

  =============================================================

  好啦,我的学习心得讲完了,谢谢各位的抬举。教程是不敢当的,因为我也是个大菜鸟。如果这些东东能使你们的学习轻松一些,进步快一些,本菜鸟就很开心了。

原文由Dark Byte(CE作者)发表,Smidge204补充

大多数人认为汇编很难学,但事实上它很简单,在这个教程我将试图解释一些基本的汇编语言如何工作。

处理器以内存和寄存器来工作,寄存器类似内存但比内存快得多,寄存器有EAX,EBX,ECX,EDX,ESP,EBP,ESI,EDI,还有段寄存器,(还有一个

叫EIP,这个是指令指针,它指示下一条将要执行的指令)

一些例子:

sub ebx,eax (ebx=00000005,eax=00000002)

让我们把它分成更基本的成分:

操作码,参数1,参数2

操作码是一个指令告诉处理器做什么,在这个例子里是让储存在EBX里面的数值,减少储存在EAX中的这个数。

在这个例子中EBX=5而EAX=2,所以这个指令执行后EBX的值应该是3(5-2)

还有请注意:当你看到操作码和两个参数的时候,第一个参数是指令的目标,而第二个参数则是来源。

sub [esi+13],ebx(ebx=00000003,esi=008AB100)

在这个例子里,你可看到第一个参数有一个方括号,这意思是说用一个内存的位置来代替寄存器,内存的位置由方括号中的内容指定,在这个例子里

是esi+13(注意13是十六进制数)

因为ESI=008AB100,所以所指的地址是008AB113。

这条指令让保存在008AB113这个地址上的数值,减少保存在EBX上的数量,即3。

如果在008AB113位置上的数值是100,那么执行这个指令后,008AB113位置上的数值将会是97。

sub [esi+13],63 (esi=008AB100)

这个几乎和上一个完全一样,只不过是用直接数值取代寄存器。

记住了63实际上是99,因为指令中写的永远都是十六进制。

假设008AB113这个位置上的数值是100(用十六进制表示是64),执行这个指令后008AB113位置上的数值将会是1(100-99)。

sub ebx,[esi+13] (ebx=00000064 esi=008ab100)

这个指令让储存在EBX上的数值,减少在008AB113里面储存的数值(ESI+13=008AB100+13=008AB113,你没忘记吧)

上面直到这里都只使用SUB这个指令,但处理器能理解的指令其实很多很多。

让我们来看看MOV这个最常用的指令吧,虽然它的名字是MOVE(移动)数据,但它其实只是把数据从一个位置复制到另一个位置罢了。

MOV工作起来也和SUB完全一样,第一个参数是目标,第二个参数是来源。

举例:

MOV eax,ebx(eax=5,ebx=12)

把储存在EBX的数值复制到EAX里面

所以,如果这条指令被执行,那么EAX里面的数值会是12(并且EBX里面仍然是12)

MOV [edi+16],eax (eax=00000064, edi=008cd200)

这个指令把保存在EAX里面的数值(十六进制数64,也即十进制的100)放到EDI+16(008CD200+16=008CD216)这个位置。

所以执行这个指令之后,储存在008CD216这个位置上的数值将会是100(十六进制数64)

就象你看到的,它工作起来也和SUB指令一样。

然后,还有一些指令只有一个参数,例如INC和DEC。

举例:

inc eax :EAX中的数值加1

dec ecx :ECX中的数值减1

dec [ebp]: 将EBP所指的内存地址处的数值减1

现在我只讲32位寄存器(EAX,EBX,ECX......),但其实还有16位寄存器和8位寄存器可以使用的,16位寄存器是:AX,BX,CX,DX,SP,BP,SI,DI

;8位寄存器是:AH,AL,BH,BL,CH,CL,DH,DL。

请注意当你改变了AH或AL寄存器时你也同时改变了AX寄存器,并且如果你改变了AX寄存器你也同时改变了EAX,其他的BL+BH+BX+EBX,CH+CL+CX+ECX

,DH+DL+DX+EDX也一样。

(CCB注:以AX为例,AX是一个十六位寄存器,而AH是八位寄存器,它是指AX寄存器的高八位,而AL则是指AX的低八位。而32位的CPU增加了32位的寄

存器,即EAX是在AX的基础上再加十六位,举例说明:

如果EAX的数值是(二进制):

EAX 00000000000000001101000100100111

那么

AX                              1101000100100111

而AH,AL则分别是:

AH                              11010001

AL                                              00100111

即AX包含AH和AL,而EAX包含AX,当然也包含AH和AL,不过WINDOWS上的程序一般比较少使用8位和16位寄存器)

你可以几乎完全一样地使用这些不同的寄存器,但它们只改变1(8位寄存器)或2(16位寄存器)字节,而不是改变4(32位寄存器)字节。

举例

dec al:8位寄存器AL减1

sub [esi+12],al:将储存在[ESI+12]所指位置上的一个1字节数值,减少AL寄存器中的数值

mov al,[esi+13]:将[ESI+13]所指的位置上的1字节数值,放到AL寄存器中

请注意,将16位和8位寄存器用来指示内存地址这是完全不可能的,例如:mov [al+12],0 是错误的。

其实还有64位和128位寄存器,但我不想讨论它们,因为它们比较难于使用,并且不能用于那些可以用于32位寄存器的指令。

那么,还有JUMP(跳转),LOOP(循环)和CALL(调用)

JMP:

JMP指令简单地修改指令指针(EIP)到JMP所指的位置并且继续执行下去。

跳转里面还有条件跳转,它只在特定的条件成立时才改变指令指针。(例如根据比较指令(CMP)的结果来设置跳转)

JA=大于则跳转

JNA=不大于则跳转

JB=小于则跳转

JE=如果相等刚跳转

JC=如果进位(进位标志置位)刚跳转

还有好多其他的条件跳转

LOOP:

循环指令和跳转指令差不多都是跳转到内存的其他位置去执行,不同的是它只有在ECX寄存器非0时才跳转。

(CCB注:也就是说,ECX是个循环的计数器,比如当循环开始时,ECX里面的数值是3,那么执行一次循环后,ECX会自动减1,并且跳到前面重复循环

,第二次执行后ECX又再减1,当ECX为0的,不再跳回去执行)

当然,循环也有条件循环:

LOOPE:当ECX非0,并且“零标志”没有置位时循环

LOOPZ:和LOOPE相同

LOOPNE:当ECX非0,并且“零标志”被置位时循环

LOOPNZ:的LOOPNZ相同

(CCB注:CPU中有另一个特有的寄存器,零标志是这个特殊的寄存器中的一个“位(BIT)”,很多转向指令例如跳转,循环等都会根据这个特殊的寄存

器中的某些位来做为条件,例如这里的零标志位和上面的进位标志,一般一个标志位上是1时即被置位,而该位为0时为没有置位)

我想我还得解释一下什么是标志,它是处理器中的一些位,可以用来检查前一指令的一些条件,好象“cmp al,12”如果AL=12那么零标志位被设置为

TURE(真),否则零标志位被设置为FALSE(假)。

CALL:

调用其实和跳转一样,除了它使用堆栈来返回(即返回原处继续执行)。

解释一下堆栈:

堆栈是由ESP寄存器为指针所指的内存位置,你可以使用PUSH命令把数值压进堆栈,并且使用POP指令将数值弹出。当你使用PUSH时ESP寄存器会减少

,并且把数值放置到ESP所指的位置。当你使用POP时会把数值弹出到POP指令的参数所指的位置,并且ESP寄存器数值增加。简言之,就是最后压堆栈

的数据最先出来,倒数第二个压进去的,第二个出来。

(CCB:堆栈的特点就是“后进先出”,想象一下,一个单车道的停车场,第一辆车停到最里面,第二辆车又停进去,然后第三辆车再开进去停在最外

面,要出来的时候,是不是第三辆车要开出来之后,第二辆才能出来?要第二辆车开出来之后,第一辆开进去的才能出来?)

RET:

当CALL调用时会把(返回后要执行的)下一条指令的地址压进堆栈,RET(返回)就跳转到这个位置执行(即把指令指针设置到这个位置)。

(CALL调用时)执行到一定地方会遇到RET指令,就会跳转到储存在堆栈中的位置中去执行。(CALL把下一条指令的位置压进堆栈,而RET就把这个位置

弹出来并跳到那里执行)

这就是最基本的汇编教程,如果你有什么有关汇编的问题,请提问,我会尽量回答。

如果你想得到更详细的信息,这里有个很好的文件:

http://podgoretsky.com/ftp/Docs/Hardware/Processors/Intel/24547111.pdf

注:理解括号中的数值的用法这一点非常有用,因为以后在使用CE时要用到指针(它可以解决大多数游戏的DMA(动态内存定位)的问题,如果你能看得

懂什么汇编指令在修改你找到的数值的话)。

----------------------------------------------------------

“标志位”是保存在一个特殊寄存器中的一些BIT的集合,如果某个BIT是1,即是说这个标志被“置位”,如果是0即是说它被“清除”,正确地说,

标志位告诉你处理器中所有的内部状态并给你更多关于前一指令执行的信息。

标志位有三种:状态标志告诉你最后一条指令执行的结果,控制标志告诉你处理器将会怎样,而系统标志告诉你,你的程序执行时的内部环境。

标志寄存器有32个位:(S=状态标志,C=控制标志,X=系统标志)

代码:

0  S 进位标志

1    (保留)

2  S 奇偶标志

3    (保留)

4  S 辅助进位标志

5    (保留)

6  S 零标志

7  S 符号标志

8  X 陷阱标志

9  X 允许中断标

10  C 方向标志

11  S 溢出标志

12  X I/O特权标志(12及13位)

13  X

14  X 嵌套任务标志

15    (保留)

16  X 复原标志

17  X 虚拟8086标志

18  X 对齐检验标志

19  X 虚拟中断标志

20  X 虚拟中断未决标志

21-31 (保留)

让我们看看状态标志,因为这些比较经常用到。

溢出(进位):

当一个操作(加、减、乘等)产生的结果太大,不能存进寄存器或内存位置时,进位标志置位(否则的话,则自动清除该标志位)。例如你使用一个16位

寄存器,而你的指令产生的结果数值大于16位,则进位标志被置位。

符号:

当结果为负数时被置位,如果是正数则清除。这个是一个数值的符号位的镜像。(CCB注:就是与结果数值的最高位相同)

零标志:

如果操作结果为0则此位被置位。

辅助进位:

与进位标志相同,但它对待寄存器或内存是以3-BITS(位)而不是普通的8,16或32位,这个用于BCD(二进制编码的十进制数)方面的东西,其他地方根

本没什么用。

进位标志:

当第一个BIT超过寄存器/内存的限制时,进位标志被置位。举例来说,mov al, 0xFF,然后add al,1 将会导致进位,因为第九个BIT被设置,而且要

注意,溢出标志和零标志也会被设置,而符号标志会被清除。

  评论这张
 
阅读(670)| 评论(0)
推荐 转载

历史上的今天

评论

<#--最新日志,群博日志--> <#--推荐日志--> <#--引用记录--> <#--博主推荐--> <#--随机阅读--> <#--首页推荐--> <#--历史上的今天--> <#--被推荐日志--> <#--上一篇,下一篇--> <#-- 热度 --> <#-- 网易新闻广告 --> <#--右边模块结构--> <#--评论模块结构--> <#--引用模块结构--> <#--博主发起的投票-->
 
 
 
 
 
 
 
 
 
 
 
 
 
 

页脚

网易公司版权所有 ©1997-2017