• QQ
  • nahooten@sina.com
  • 常州市九洲新世界花苑15-2

黑客安全

汇编语言和硬件知识

原创内容,转载请注明原文网址:http://homeqin.cn/a/wenzhangboke/jishutiandi/heikeanquan/2019/1003/670.html

汇编语言和CPU以及内存,端口等硬件常识是连在一起的. 这也是为何汇编语言没有通用性的缘故. 底下手机App外包简单讲讲根基常识(针对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 系列指令就是凭据这些标记来决意是否要跳转, 从而App开发培训实现条件分枝. 要留意,许多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实现,详细可参考汇编语言册本。
 
汇编指令的操纵数能够是内存中的数据, 若何让程序从内存中正确获得所需求的数据就是对内存的寻址. 
 
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 ;不行对指令指针干脆操纵.
 
“汇编语言”作为一门语言,对应于高档语言的编译器,咱们常州网站开发培训需求一个“汇编器”来把汇编语言原文件汇编成机械可实行的代码。高档的汇编器如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则相反。
 
高档语言程序的汇编分析 
 
在高档语言中,如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 
 
============================================================= 
好啦,我常州平台运营的学习心得讲完了,感谢各位的提拔。教程是不敢当的,由于我也是个大菜鸟。要是这些东东能使你们的学习轻松少许,前进快少许,本菜鸟就很高兴了。
 

上篇:上一篇:软件破解常用API
下篇:下一篇:没有了