admin 管理员组文章数量: 1184232
2024年1月26日发(作者:java随机产生数字1 5)
对初学者而言,汇编的许多命令太复杂,往往学习很长时间也写不出一个漂漂亮亮的程序,以致妨碍了我们学习汇编的兴趣,不少人就此放弃。所以我个人看法学汇编,不一定要写程序,写程序确实不是汇编的强项,大家不妨玩玩DEBUG,有时CRACK出一个小软件比完成一个程序更有成就感(就像学电脑先玩游戏一样)。某些高深的指令事实上只对有经验的汇编程序员有用,对我们而言,太过高深了。为了使学习汇编语言有个好的开始,你必须要先排除那些华丽复杂的命令,将注意力集中在最重要的几个指令上(CMP LOOP MOV JNZ……)。但是想在啰里吧嗦的教科书中完成上述目标,谈何容易,所以本人整理了这篇超浓缩(用WINZIP、WINRAR…依次压迫,嘿嘿!)教程。大言不惭的说,看通本文,你完全可以“不经意”间在前辈或是后生卖弄一下DEBUG,很有成就感的,试试看!那么――这个接下来呢?―― Here we go!(阅读时看不懂不要紧,下文必有分解)
因为汇编是通过CPU和内存跟硬件对话的,所以我们不得不先了解一下CPU和内存:(关于数的进制问题在此不提)
CPU是可以执行电脑所有算术╱逻辑运算与基本 I/O 控制功能的一块芯片。一种汇编语言只能用于特定的CPU。也就是说,不同的CPU其汇编语言的指令语法亦不相同。个人电脑由1981年推出至今,其CPU发展过程为:8086→80286→80386→80486→PENTIUM →……,还有AMD、CYRIX等旁支。后面兼容前面CPU的功能,只不过多了些指令(如多能奔腾的MMX指令集)、增大了寄存器(如386的32位EAX)、增多了寄存器(如486的FS)。为确保汇编程序可以适用于各种机型,所以推荐使用8086汇编语言,其兼容性最佳。本文所提均为8086汇编语言。寄存器(Register)是CPU内部的元件,所以在寄存器之间的数据传送非常快。用途:1.可将寄存器内的数据执行算术及逻辑运算。2.存于寄存器内的地址可用来指向内存的某个位置,即寻址。3.可以用来读写数据到电脑的周边设备。8086 有8个8位数据寄存器,这些8位寄存器可分别组成16位寄存器:AH&AL=AX:累加寄存器,常用于运算;B
H&BL=BX:基址寄存器,常用于地址索引;CH&CL=CX:计数寄存器,常用于计数;DH&DL=DX:数据寄存器,常用于数据传递。为了运用所有的内存空间,8086设定了四个段寄存器,专门用来保存段地址:CS(Code Segment):代码段寄存器;DS(Data
Segment):数据段寄存器;SS(Stack Segment):堆栈段寄存器;ES(Extra Segment):附加段寄存器。当一个程序要执行时,就要决定程序代码、数据和堆栈各要用到内存的哪些位置,通过设定段寄存器 CS,DS,SS 来指向这些起始位置。通常是将DS固定,而根据需要修改CS。所以,程序可以在可寻址空间小于64K的情况下被写成任意大小。 所以,程序和其数据组合起来的大小,限制在DS 所指的64K内,这就是COM文件不得大于64K的原因。8086以内存做为战场,用寄存器做为军事基地,以加速工作。除了前面所提的寄存器外,还有一些特殊功能的寄存器:IP(Intruction Pointer):指令指针寄存器,与CS配合使用,可跟踪程序的执行过程;SP(Stack Pointer):堆栈指针,与SS配合使用,可指向目前的堆栈位置。BP(Base Pointer):基址指针寄存器,可用作SS的一个相对基址位置;SI(Source Index):源变址寄存器可用来存放相对于DS段之源变址指针;DI(Destination Index):目的变址寄存器,可用来存放相对于 ES 段之目的变址指针。还有一个标志寄存器FR(Flag Register),有九个有意义的标志,将在下文用到时详细说明。
内存是电脑运作中的关键部分,也是电脑在工作中储存信息的地方。内存组织有许多可存放数值的储存位置,叫“地址”。8086地址总线有20位,所以CPU拥有达1M的寻址空间,这也是DOS的有效控制范围,而8086能做的运算仅限于处理16位数据,即只有0到64K,所以,必须用分段寻址才能控制整个内存地址。完整的20位地址可分成两部份:1.段基址(Segment):16位二进制数后面加上四个二进制0,即一个16进制0,变成20位二进制数,可设定1M中任何一个64K段,通常记做16位二进制数;2.偏移量(Offset):直接使用16位二进制数,指向段基址中的任何一个地址。如:2222(段基址):3333(偏移量),其实际的20位
地址值为:25553。除了上述营养要充分吸收外,你还要知道什么是DOS、BIOS功能调用,简单的说,功能调用类似于WIN95 API,相当于子程序。汇编写程序已经够要命了,如果不用MS、IBM的子程序,这日子真是没法过了(关于功能调用详见《电脑爱好者》98年11期)。
编写汇编语言有两种主要的方法:1.使用MASM或TASM等编译器;2.使用除错程序。DEBUG其实并不能算是一个编译器,它的主要用途在于除错,即修正汇编程序中的错误。不过,也可以用来写短的汇编程序,尤其对初学者而言,DEBUG 更是最佳的入门工具。因为DEBUG操作容易:只要键入DEBUG回车,A回车即可进行汇编,过程简单,而使用编译器时,必须用到文本编辑器、编译器本身、LINK以及EXE2BIN等程序,其中每一个程序都必须用到一系列相当复杂的命令才能工作,而且用编译器处理源程序,必须加入许多与指令语句无关的指示性语句,以供编译器识别,使用 DEBUG 可以避免一开始就碰到许多难以理解的程序行。DEBUG 除了能够汇编程序之外,还可用来检查和修改内存位置、载入储存和执行程序、以及检查和修改寄存器,换句话说,DEBUG是为了让我们接触硬件而设计的。(8086常用指令用法将在每个汇编程序中讲解,限于篇幅,不可能将所有指令列出)。
DEBUG的的A命令可以汇编出简单的COM文件,所以DEBUG编写的程序一定要由地址 100h(COM文件要求)开始才合法。FOLLOW
ME,SETP BY SETP(步步回车):
输入 A100 ; 从DS:100开始汇编
2.输入 MOV DL,1 ; 将数值 01h 装入 DL 寄存器
3.输入 MOV AH,2 ; 将数值 02h 装入 DL 寄存器
4.输入 INT 21 ; 调用DOS 21号中断2号功能,用来逐个显示装入DL的字符
5.输入 INT 20 ; 调用DOS 20号中断,终止程序,将控制权交回给 DEBUG
6.请按 Enter 键
7.现在已将汇编语言程序放入内存中了,输入 G(运行)
8.出现结果:输出一个符号。
ㄖ ←输出结果其实不是它,因WORD97无法显示原结果,故找一赝品将就着。
Program terminated normally
我们可以用U命令将十六进制的机器码反汇编(Unassemble)成汇编指令。你将发现每一行右边的汇编指令就是被汇编成相应的机器码,而8086实际上就是以机器码来执行程序。
1.输入 U100,106
1FED:0100 B201 MOV DL,01
1FED:0102 B402 MOV AH,02
1FED:0104 CD21 INT 21
1FED:0106 CD20 INT 20
DEBUG可以用R命令来查看、改变寄存器内容。CS:IP寄存器,保存了将执行指令地址。
1.输入R
AX=0000 BX=0000 CX=0000 DX=0000 SP=FFEE BP=0000
SI=0000 DI=0000
DS=1FED ES=1FED SS=1FED CS=1FED IP=0100 NV UP EI PL NZ
NA PO NC
1FED:0100 B201 MOV DL,01
当程序由DS:100开始执行,那么终止程序时,DEBUG会自动将IP内容重新设定为100。当你要将此程序做成一个独立的可执行文件,则可以用N命令对该程序命名。但一定要为COM文件,否则无法以DEBUG载入。
输入N ;我们得告诉DEBUG程序长度:程序从100开始到106,故占用7
;字节。我们利用BX存放长度值高位部分,而以CX存放低位部
分。
2.输入RBX ;查看 BX 寄存器的内容,本程序只有7个字节,故本步可省略
3.输入 RCX ;查看 CX 寄存器的内容
4.输入 7 ;程序的字节数
5.输入 W ;用W命令将该程序写入(Write)磁盘中
修行至此,我们便可以真正接触8086汇编指令了。 当我们写汇编语言程序的时候,通常不会直接将机器码放入内存中,而是打入一串助记符号(Mnemonic Symbols),这些符号比十六进制机器码更容易记住,此之谓汇编指令。助记符号,告诉CPU应执行何种运算。 也就是说,助忆符号所构成的汇编语言是为人设计的,而机器语言是对PC设计的。
现在,我们再来剖析一个可以将所有ASCII码显示出来的程序。
1. 输入 DEBUG
2. 输入 A100
3.输入 MOV CX,0100 ;装入循环次数
MOV DL,00 ;装入第一个ASCII码,随后每次循环装入新码
MOV AH,02
INT 21
INC DL ;INC:递增指令,每次将数据寄存器 DL 内的数值加 1
LOOP 0105 ;LOOP:循环指令,每执行一次LOOP,CX值减1,并跳
;到循环的起始地址105,直到CX为0,循环停止
INT 20
4.输入 G即可显示所有ASCII码
当我们想任意显示字符串,如:UNDERSTAND?,则可以使用DOS21H号中断9H号功能。输入下行程序,存盘并执行看看:
1.输入 A100
MOV DX,109 ;DS:DX = 字符串的起始地址
MOV AH,9 ;DOS的09h功能调用
INT 21 ;字符串输出
INT 20
DB 'UNDERSTAND?$';定义字符串
在汇编语言中,有两种不同的指令:1.正规指令:如 MOV 等,是属于CPU的指令,用来告诉CPU在程序执行时应做些什么,所以它会以运算码(OP-code)的方式存入内存中;2.伪指令:如DB等,是属于DEBUG等编译器的指令,用来告诉编译器在编译时应做些什么。DB(Define Byte)指令用来告诉DEBUG 将单引号内的所有ASCII 码放入内存中。使用 9H 功能的字符串必须以$结尾。用D命令可用来查看DB伪指令将那些内容放入内存。
6.输入 D100
1975:0100 BA 09 01 B4 09 CD 21 CD-20 75 6E 64 65 72 73 74 ......!.
underst
1975:0110 61 6E 64 24 8B 46 F8 89-45 04 8B 46 34 00 64 19
and$.F..E..F4.d.
1975:0120 89 45 02 33 C0 5E 5F C9-C3 00 C8 04 00 00 57
56 .E.3.^_.......WV
1975:0130 6B F8 0E 81 C7 FE 53 8B-DF 8B C2 E8 32 FE 0B C0
k.....S.....2...
1975:0140 74 05 33 C0 99 EB 17 8B-45 0C E8 D4 97 8B F0 89
E.......
1975:0150 56 FE 0B D0 74 EC 8B 45-08 03 C6 8B 56 FE 5E 5F
V...t..E....V.^_
1975:0160 C9 C3 C8 02 00 00 6B D8-0E 81 C3 FE 53 89 5E
FE ......k.....S.^.
1975:0170 8B C2 E8 FB FD 0B C0 75-09 8B 5E FE 8B 47 0C
E8 .......u..^..G..
现在,我们来剖析另一个程序:由键盘输入任意字符串,然后显示出来。db 20指示DEBUG保留20h个未用的内存空间供缓冲区使用。
输入A100
MOV DX,0116 ;DS:DX = 缓冲区地址,由DB伪指令确定缓冲区地址
MOV AH,0A ;0Ah 号功能调用
INT 21 ;键盘输入缓冲区
MOV DL,0A ;由于功能Ah在每个字符串最后加一个归位码(0Dh由 Enter
MOV AH,02 ;产生),使光标自动回到输入行的最前端,为了使新输出的
INT 21 ;字符串不会盖掉原来输入的字符串,所以利用功能2h加一
;个换行码(OAh),使得光标移到下一行的的最前端。
MOV DX,0118 ;装入字符串的起始位置
MOV AH,09 ;9h功能遇到$符号才会停止输出,故字符串最后必须加上
INT 21 ;$,否则9h功能会继续将内存中的无用数据胡乱显示出来
INT 20
DB 20 ;定义缓冲区
送你一句话:学汇编切忌心浮气燥。
客套话就不讲了。工欲善其事,必先利其器。与其说DEBUG 是编译器,倒不如说它是“直译器”,DEBUG的A命令只可将一行汇编指令转成机器语言,且立刻执行。真正编译器(MASM)的运作是利用文本编辑器(EDIT等)将汇编指令建成一个独立且附加名为.ASM的文本文件,称源程序。它是MASM 程序的输入部分。MASM将输入的ASM
文件,编译成.OBJ文件,称为目标程序。OBJ文件仅包含有关程序各部份要载入何处及如何与其他程序合并的信息,无法直接载入内存执行。链结程序LINK则可将OBJ文件转换成可载入内存执行(EXEcute)的EXE文件。还可以用EXE2BIN,将符合条件的EXE文件转成COM文件(COM 文件不但占用的内存最少,而且运行速度最快)。
下面我们用MASM写一个与用DEBUG写的第一个程序功能一样的程序。
用EDIT编辑一个的源程序文件。
源程序 DEBUG 程序
prognam segment
assume cs:prognam
org 100h A100
mov dl,1 mov dl,1
mov ah,2 mov ah,2
int 21h int 21
int 20h int 20
prognam ends
end
比较一下:1.因为MASM会将所有的数值假设为十进制,而DEBUG则只使用十六进制,所以在源程序中,我们必须在有关数字后加上代表进制的字母,如H代表十六进制,D代表十进制。若是以字母开头的十六进制数字,还必须在字母前加个0,以表示它是数,如0AH。2.源程序增加五行叙述:prognam segment 与 prognam ends 是成对的,用来告诉
MASM 及LINK,此程序将放在一个称为PROGNAM(PROGram NAMe)的程序段内,其中段名(PROGNAM)可以任取,但其位置必须固定。assume cs:prognam 必须在程序的开头,用来告诉编译器此程序所在段的位置放在CS寄存器中。end用来告诉MASM,程序到此结束, ORG 100H作用相当于DEBUG的A100,从偏移量100开始汇编。COM 文件的所
有源程序都必须包含这五行,且必须依相同的次序及位置出现,这点东西记下就行,千篇一律。接着,我们用MASM编译。
输入 MASM SMILE ←不用打入附加名.ASM。
Microsoft (R) Macro Assembler Version 5.10
Copyright (C) Microsoft Corp 1981, 1988. All rights reserved.
Object filename []: ←是否改动输出OBJ文件名,如不改就ENTER
Source listing []: ← 是否需要列表文件(LST),不需要就ENTER
Cross-reference []: ←是否需要对照文件(CRF),不需要则ENTER
50162 + 403867 Bytes symbol space free
0 Warning Errors ←警告错误,表示编译器对某些语句不理解,通常是输入错误。
0 Severe Errors ←严重错误,会造成程序无法执行,通常是语法结构错误。
如果没有一个错误存在,即可生成OBJ文件。OBJ中包含的是编译后的二进制结果,它还无法被 DOS载入内存中加以执行,必须加以链结(Linking)。以LINK将OBJ文件()链结成 EXE 文件()时,。
1.输入 LINK SMILE ←不用附加名OBJ
Microsoft (R) Overlay Linker Version 3.64
Copyright (C) Microsoft Corp 1981, 1988. All rights reserved.
Run File []: ← 是否改动输出EXE文件名,如不改就ENTER
List File []: ← 是否需要列表文件(MAP),不需要则ENTER
Libraries [.LIB]: ←是否需要库文件,要就键入文件名,不要则
ENTER
LINK : warning L4021: no stack segment← 由于COM文件不使用堆栈段,所以错误信息
←"no stack segment"并不影响程序正常执行
至此已经生成EXE文件,我们还须使用EXE2BIN 将EXE文件(),转换成COM文件()。输入EXE2BIN
SMILE产生 BIN 文件()。其实 BIN 文件与 COM 文件是完全相同的,但由于DOS只认COM、EXE及BAT文件,所以BIN文件无法被正确执行,改名或直接输入 EXE2BIN SMILE 即可。现在,磁盘上应该有 文件了,你只要在提示符号C:>下,直接输入文件名称 SMILE ,就可以执行这个程序了。
你是否觉得用编译器产生程序的方法,比 DEBUG 麻烦多了!以小程序而言,的确是如此,但对于较大的程序,你就会发现其优点了。我们再将ASCII程序以编译器方式再做一次,看看有无差异。首先,用建立 文件。
prognam segment ;定义段
assume cs:prognam ;把上面定义段的段基址放入 CS
mov cx,100h ; 装入循环次数
mov dl,0 ; 装入第一个ASCII码,随后每次循环装入新码
next: mov ah,2
int 21h
inc dl ;INC:递增指令,每次将数据寄存器 DL 内的数值加 1
loop next ; 循环指令,执行一次,CX减1,直到CX为0,循环停止
int 20h
prognam ends ;段终止
end ;汇编终止
在汇编语言的源程序中,每一个程序行都包含三项元素:
start: mov dl,1 ;装入第一个ASCII码,随后每次循环装入新码
标识符 表达式 注解
在原始文件中加上注解可使程序更易理解,便于以后参考。每行注解以“;”与程序行分离。编译器对注解不予理会,注解的数据不会出现在OBJ、EXE或COM文件中。由于我们在写源程序时,并不知道每一程序行的地址,所以必须以符号名称来代表相对地址,称为“标识符”。我们通常在适当行的适当位置上,键入标识符。标识符(label)最长可达31 个字节,因此我们在程序中,尽量以简洁的文字做为标识符。现在,你可以将此 文件编译成 了。
ASCII, ASCII,2BIN ASCII 。
注意:当你以编译器汇编你设计的程序时,常会发生打字错误、标识符名称拼错、十六进制数少了h、逻辑错误等。汇编老手常给新人的忠告是:最好料到自己所写的程序一定会有些错误(别人告诉我的);如果第一次执行程序后,就得到期望的结果,你最好还是在检查一遍,因为它可能是错的。原则上,只要大体的逻辑架构正确,查找程序中错误的过程,与写程序本身相比甚至更有意思。写大程序时,最好能分成许多模块,如此可使程序本身的目的较单纯,易于撰写与查错,另外也可让程序中不同部份之间的界限较清楚,节省编译的时间。如果读程序有读不懂的地方最好用纸笔记下有关寄存器、内存等内容,在纸上慢慢比划,就豁然开朗了。 下面我们将写一个能从键盘取得一个十进制的数值,并将其转换成十六进制数值而显示于屏幕上的“大程序”。前言:要让8086执行这样的功能,我们必须先将此问题分解成一连串的步骤,称为程序规划。首先,以流程图的方式,来确保整个程序在逻辑上没有问题(不用说了吧!什么语言都要有此步骤)。这种模块化的规划方式,称之为“由上而下的程序规划”。而在真正写程序时,却是从最小的单位模块(子程序)开始,当每个模块都完成之后,再合并成大程序;这种大处著眼,小处著手的方式称为“由下而上的程序设计”。
我们的第一个模块是BINIHEX,其主要用途是从8086的BX寄存器
中取出二进制数,并以十六进制方式显示在屏幕上。注意:子程序如不能独立运行,实属正常。
binihex segment
assume cs:binihex
mov ch,4 ;记录转换后的十六进制位数(四位)
rotate: mov cl,4 ;利用CL当计数器,记录寄存器数位移动次数
rol bx,cl ;循环寄存器BX的内容,以便依序处理4个十六进制数
mov al,bl ;把bx低八位bl内数据转移至al
and al,0fh ;把无用位清零
add al,30h ;把AL内数据加30H,并存入al
cmp al,3ah ;与3ah比较
jl printit ;小于3ah则转移
add al,7h ;把AL内数据加30H,并存入al
printit:mov dl,al ;把ASCII码装入DL
mov ah,2
int 21h
dec ch ;ch减一,减到零时,零标志置1
jnz rotate ;JNZ:当零标志未置1,则跳到指定地址。即:不等,则转移
int 20h ;从子程序退回主程序
binihex ends
end
利用循环左移指令ROL循环寄存器BX(BX内容将由第二个子程序提供)的内容,以便依序处理4个十六进制数:1. 利用CL当计数器,记录寄存器移位的次数。2.将BX的第一个十六进制值移到最右边。利用
AND (逻辑“与”运算:对应位都为1时,其结果为1,其余情况为零)把不要的部份清零,得到结果:先将BL值存入AL中,再利用AND以0Fh(00001111)将AL的左边四位清零。由于0到9的ASCII码为30h
到39h,而A到F之ASCII码为41h到46h,间断了7h,所以得到结果:若AL之内容小于3Ah,则AL值只加30h,否则AL再加7h。ADD指令会将两个表达式相加,其结果存于左边表达式内。标志寄存器(Flag
Register)是一个单独的十六位寄存器,有9个标志位,某些汇编指令(大部份是涉及比较、算术或逻辑运算的指令)执行时,会将相关标志位置1或清0, 常碰到的标志位有零标志(ZF)、符号标志(SF)、溢出标志(OF)和进位标志(CF)。 标志位保存了某个指令执行后对它的影响,可用其他相关指令,查出标志的状态,根据状态产生动作。CMP指令很像减法,是将两个表达式的值相减,但寄存器或内存的内容并未改变,只是相对的标志位发生改变而已:若 AL 值小于 3Ah,则正负号标志位会置0,反之则置1。 JL指令可解释为:小于就转移到指定位置,大于、等于则向下执行。CMP和JG 、JL等条件转移指令一起使用,可以形成程序的分支结构,是写汇编程序常用技巧。
第二个模块DECIBIN 用来接收键盘打入的十进制数,并将它转换成二进制数放于BX 寄存器中,供模块1 BINIHEX使用。
decibin segment
assume cs:decibin
mov bx,0 ;BX清零
newchar:mov ah,1 ;
int 21h ;读一个键盘输入符号入al,并显示
sub al,30h ;al减去30H,结果存于al中,完成ASCII码转二进制码
jl exit ;小于零则转移
cmp al,9d
jg exit ;左>右则转移
cbw ;8位al转换成16位ax
xchg ax,bx ;互换ax和bx内数据
mov cx,10d ;十进制数10入cx
mul cx ;表达式的值与ax内容相乘,并将结果存于ax
xchg ax,bx
add bx,ax
jmp newchar ;无条件转移
exit: int 20 ;回主程序
decibin ends
end
CBW 实际结果是:若AL中的值为正,则AH填入00h;反之,则AH填入FFh。XCHG常用于需要暂时保留某个寄存器中的内容时。
当然,还得一个子程序(CRLF)使后显示的十六进制数不会盖掉先输入的十进制数。
crlf segment
assume cs:crlf
mov dl,0dh ;回车的ASCII码0DH入DL
mov ah,2
int 21h
mov dl,0ah ;换行的ASSII码0AH入AH
mov ah,2
int 21h
int 20 ;回主程序
crlf ends
end
现在我们就可以将BINIHEX、DECIBIN及CRLF等模块合并成一个大程序了。首先,我们要将这三个模块子程序略加改动。然后,再写一段程序来调用每一个子程序。
crlf proc near;
mov dl,0dh
mov ah,2
int 21h
mov dl,0ah
mov ah,2
int 21h
ret
crlf endp
类似SEGMENT与ENDS的伪指令,PROC与ENDP也是成对出现,用来识别并定义一个程序。其实,PROC 真正的作用只是告诉编译器:所调用的程序是属于近程(NEAR)或远程(FAR)。 一般的程序是由
DEBUG 直接调用的,所以用 INT 20 返回,用 CALL 指令所调用的程序则改用返回指令RET,RET会把控制权转移到栈顶所指的地址,而该地址是由调用此程序的 CALL指令所放入的。
各模块都搞定了,然后我们把子程序组合起来就大功告成
decihex segment ;主程序
assume cs:decihex
org 100h
mov cx,4 ;循环次数入cx;由于子程序要用到cx,故子程序要将cx入栈
repeat: call decibin;调用十进制转二进制子程序
call crlf ;调用添加回、换行符子程序
call binihex ;调用二进制转十六进制并显示子程序
call crlf
loop repeat ;循环4次,可连续运算4次
mov ah,4ch ; 调用DOS21号中断4c号功能,退出程序,作用跟INT
20H
int 21H ; 一样,但适用面更广,INT20H退不出时,试一下它
decibin proc near push cx ;将cx压入堆栈,;
┇ exit: pop cx ;将cx还原; retdecibin endp binihex proc near push cx
┇ pop cx retbinihex endp crlf proc near
push cx
┇ pop cx retcrlf endpdecihex ends end
CALL指令用来调用子程序,并将控制权转移到子程序地址,同时将CALL的下行一指令地址定为返回地址,并压入堆栈中。CALL 可分为近程(NEAR)及远程(FAR)两种::IP的内容被压入堆栈中,用于程序与程序在同一段中。:CS 、IP寄存器的内容依次压入堆栈中,用于程序与程序在不同段中。PUSH、POP又是一对指令用于将寄存器内容压入、弹出,用来保护寄存器数据,子程序调用中运用较多。堆栈指针有个“后进先出”原则,像PUSH AX,PUSH BX…POP BX,POP AX这样才能作到保护数据丝毫不差。
汇编语言超浓缩教程到这要告一段落了,希望能奠定你独立设计的基础。而更多更好的技巧则全依赖你平时的积累了。祝你成功!
分类: 汇编学习 嵌入式开发 黑客技术
上一篇:如何学习汇编语言 下一篇:一直以为lea和mov指令差不多的,今天算是知道了,顺便测试了下C内联汇编的效率
收藏到网摘:
汇编教程(1):如何学习汇编
2008-11-01 15:43
想想我自己玩电脑也是不少时间了,也经常在各大论坛灌水,得到过很多人的帮助。
但是非常遗憾的事一直都没有为别人贡献过什么,现在我有点时间所以写了几篇关于汇编的文章,我以后还会继续更新。路线是:先写点16位汇编-32位汇编-解密知识-外挂制作。希望大家关注,同时多给点意见,在下将非常感激 ,同时希望大家多多回复不要让帖子沉下去。希望多多支持
如果大家有什么问题可以给我发Email:*************
我将尽全力帮助你解答问题!
写这篇文章(我准备把他写成一个系列),我想讲述如何从零开始学汇编语言程序设计,请千万相信汇编其实是很简单的,只要自己努力一定是可以学会的。可能由于我的技术(编程和写作)也很不到家在很多方面都写的不是很好甚至是胡说八道(但愿你不会这样认为)。如果你对此有什么意见可以给我发Email:*************给我我将竭尽全力的回复每一封邮件同时希望大家多给我提提意见,让我把文章写的更好。。!
这篇文章里不会有太多技术方面的东西,我想在这章先和大家一起探讨如何学习汇编的问题,也许有些内容让你觉得我很啰唆。那是以为我为了照顾大部分的读者所以写的比较详细。。请大家多支持我,看了之后多给我意见。如果没有意外我会坚持每个礼拜发表一篇。今天写的是第一章,只是个引子,还没具体涉及到编程。
罗云彬说的好啊,学汇编就像千军万马过独木桥,能够坚持到最后的很少。这个不禁让我想起了高考啊。汇编这东西如果你真的学精通的话可以让计算机做很多别人不能够做的事情,因此很多人都想学习汇编。
掌握这方面技术,对自身的提高确实有好处。你可以通过反汇编别人的程序跟踪别人的软件,了解别人的编程思路。通过了解别人的程序思路,使自己写出更加好的程序。而且写病毒,木马和外挂等等的一些软件~~~都必须用汇编才能够写的漂亮。研究汇编还有助于掌握一些系统底层知识,系统底层知识绝对是构造起大型软件的坚实基础。许多程序发展,都经历了这一锻炼过程的。 而大多数人可能认为汇编是一门高深的学问。很难学,我前几天在网上拿了个软件给我朋友用。 他问我用什么写的,我说用汇编。。他不信以为汇编根本就写不出强大复杂的软件,其实这都是老皇历了(现在的汇编其实我感觉和C差不多都是调用windows API来编程)。造成这种原因我觉得是以前在dos下面用汇编来写程序太麻烦,所以给人的感觉就成了很复杂,(以前在dos下汇编没有很多API可以调用)就将汇编神话了,造成初学者在心理上就承认了汇编很难学的”事实”其实不是这样现在其他编程语言可以做的比如C,用汇编做起来也不是很麻烦。再一方面由于学习资料比较匮乏学。初学者一般不知从何下手,由于没方向,花费了大量时间和精力,走了不少弯路。这里我就给想学汇编的指下学习方向(根据我自己的学习经历)。
在学汇编前,最好先掌握一门高级语言编程,这样再学汇编应容易些,如果你学校或者在其他地方学了C语言的学起来就比较轻松了,很多书上都是用C语言程序做范例来讲解的,如果你没有学也没有关系这里我给你推荐个视频: <<尚学堂科技java系列视频教程>>,这个教程可比那些什么什么大学跟这课本念PPT的好多了, 你或者会问我为什么学汇编要去看java的视频啊,这里我只是推荐你去看下他视频的前3章,也就算基础知识,这在所有的程序设计语言里面都适用, 看完了再学习汇编坡度就不会那么陡了。
看完视频我想再向大家介绍几本书都是中国人写的(我觉得国人计算机方面的著作就算汇编比较有出席拉)
一本是王爽写的<<汇编语言>>和罗云彬写的windows环境下32位汇编语言程序设计,这2本书在学习汇编的人心中地位都非常高现在都出了第2版了,而且网上都有电子版,google下就能找到。其他还有一些书比如: 温冬蝉,沈美明:清华大学出版社,《IBM PC 汇编语言程序设计教程》, 。【美】Kip R。Irvine
著,温玉杰等译:《Intel 汇编语言程序设计》(第四版),都是比较经典的,如果有机会可以看看!!
学习汇编要多动手,不要只看书和书上的例子,有条件就把例子弄到电脑上试一试,如果成功了,不要高兴,你的工作只完成了一半,把例子改一改,按你的意思,想想改了之后的结果,与上机对照一下,你会有收获的。 写汇编程序难免会出错,老一辈的人都还在讲解着dubug的使用,其实现在windows环境下的汇编调试器已经非常的完善了,我平时一般是用Rad ASM里面自带的调试器,他已经可以给我解决大部分由于编程粗心而引起的错误,还有一些是逻辑错误我一般
用ollydbg进行调试,这里我再给大家推荐个编辑器Rad ASM,这是一个非常棒的汇编IDE编程环境掌握他的使用可以让安心的写汇编程序而不用把心思在记忆复杂的一些编译指令上,我准备写篇Rad ASM的教程,但是现在还没有动笔。现在比较好的讲解Rad ASM可能是jhkdiy写的 Win32汇编开发环境介绍和RadAsm简明教程
学习汇编其实很累的,需花费大量的时间,而且经常会遇到很多的问题,资料又比较匮乏。这时你可能有点想退却,其实你不要着急,只要你认真学习,成功就在眼前。没有人是生来就什么都会的,如果你有问题,就大胆的去问你周围的人。而且现在网上也有很多的专门讨论汇编的站点比较好的应该是aogo的汇编小站和罗云彬的编程乐园,学汇编的秘诀就是勤奋+执着!记住并能做到这两点,你会变得很优秀的。
不管你学汇编是出于什么目的,如果你在学习汇编的过程中如果遇到了问题可以发邮件给我探讨,我非常的欢迎邮件是:**************。同时在学习汇编的时候如果突然觉得学的很痛苦,可以在看雪学院下点东西玩玩解密,培养培养下兴趣也未尝不可啊!多在机子上试试底层的东西,如bios调用,IO操作,对显卡,硬盘的控制。那样你会体会到学习汇编语言的快乐。
总之要记得想学会汇编,你需要自立自强,以及自学能力。现在开始吧……
转载请注明出自暗组技术论坛 /bbs/
汇编语言(2):如何学习汇编
2008-11-01 15:44
准备提笔写的时候好像不知道怎么下笔啊,毕竟以前我都没有写过技术方面的文章,怕自己技术不够让高手笑话,又怕表达不清楚自己的意思.但是心一横死就死吧!在上一篇 如何学习汇编中没有给大家扯太多技术话题,但是我觉得那也是非常重要的,学习什么事有了信心就成功了一半,所以还希望大家继续坚定要把汇
编学好的思想,要相信自己汇编是很简单的,我们通过自己的努力一定可以把汇编学好
那么让我们开始今天的话题…..
今天主要扯点计算机的工作方式,因为汇编是面向机器的语言,所以非常有必要了解下计算机的结构,CPU的工作方式.这里我假设你只有一点点最基本的电脑基础啊,最好自己去弄懂那个什么进制转换的,我觉得特麻烦,不过却也比较简单..我这里就不和大家扯了自己了解下~~那么既然假设你没有什么电脑知识就会显得有些啰唆,高手别见笑啊!
说到汇编语言啊,就扯远拉,远到哪里呢, 嗯很远, 汇编语言和机器语言是很亲密的,所以要聊下汇编语言那就不得不又扯下机器语言了,^_^.真远啊!!
机器语言
什么叫机器语言呢,机器语言就是一些机器指令的集合拉,也就是一些二进制的数字什么010010的,这就是机器指令,计算机呢可以把这些010010什么的转变为高低电平,使计算机的电子器件收到驱动,进行运算..嗯,搞的好像很复杂似的,其实简单理解就是计算机也有一门语言(机器语言)就像我们的汉语一样,我们汉语的构成呢是由字来构成的,机器语言(也就是电脑自己的语言,就是一些01001什么的来构成的.当初我理解的时候就想啊,机器语言不是人发明的么,为什么不教他学汉语啊要教他学什么01001的那么麻烦^_^.呵呵其实现在很多人都在做着这样的努力呢, 诶~~又扯远了.
给大家扯了机器语言的那就再给大家扯一下计算机吧,你有跟朋友去过电脑城装过电脑吗?或者见过别人把电脑机箱打开后里面的情况吗? 对里面有一个大风扇,大风扇下面知道是什么吗? 那是CPU就是电脑的心脏,那家伙就像我们脑袋一样是指挥部.其实我们见到的那CPU叫微处理器,是它控制着电脑的显卡啊,声卡什么的,当然我们不能够直接去控制这些东西那些玩意不听我们的,但是他们的指挥官(CPU)听我们,我们编程的任务就是指挥CPU(嗯,你可以把这想象成一场战争)去控制显卡显示图像,声卡发声.是不是很兴奋,可是我要跟你说的是他们的指挥官(CPU)是个莽夫特别讨厌学习国家的语言(这个和我倒是有些类似啊).所以我们想要和CPU沟通就要学习他的语言,可是他的语言实在是太难学了(有点向英语那么难学^_^)所以我不准备去学习他的语言,同时我也不建议你去学它的语言,那你要问我怎么办拉,嘿嘿很好办,你看过胡锦涛和布什谈话的时候是怎么谈的吗? 对拉有翻译,毛主席说人人都是平等的,胡主席可以请翻译,我们也可以~~~~我们的翻译的就是 汇编, 汇编是个好同志啊,他帮我们把指令翻译成很长很长的0101001,从此我们就离开了机器语言的怀抱,而用汇编来控制CPU拉,这是什么,这是世界的进步啊,是不是又感觉这世界又美好了一些!!
汇编语言
那么,我们现在已经理解了汇编语言和机器语言的相同和区别了,要说相同的话他们都好像是4个汉字,区别就是前2个字不同,倒下一片~~~~~~“, 其实汇编和机器语言在我理解是没有什么区别的,汇编只是把机器语言的010010对应的翻译了一下,就像我学英语一样,老是喜欢做硬式翻译,所以此时此刻我不得不再打击你一下,因为汇编只是硬式翻译了下机器语言,所以造成了汇编的指令过多,和过于复杂了.当然后来为了解决这个问题又出了C语言对吧,你不会想当叛徒去投靠C吧,晕,张三丰说过,千万不能够当叛徒,这不,现在的32位汇编就有宏汇编,搞的和C都差不多了,所以你也别想着要跑C那里去,现在我们只要学习下DOS下的汇编理解了那些指令的用处,到时候我们在windows下面写程序的时候都有很多宏可以调用了..
汇编语言的组成
那么我们现在把机器语言扯完了,(其实我还想再扯一会的).但是怕你不满意!!下面就是汇编闪亮登场了,你也许会问我汇编长什么样子呢?? 这不好说啊,但是我可以肯定的是他绝对没有我帅^_^…其实汇编不能够说长什么样子,应该说汇编由什么东西构成呢, 那么我告诉你汇编是由 一些指令 比如你看着头晕的mov啊什么的, 还有就是伪指令了,伪指令就是些段标记啊什么,现在不知道没有关系,到时候我再跟你扯..
存储器(内存)
现在我假设你已经把汇编指令学完了,那么你肯定是命令计算机执行几条指令对吧,理论上说的过去,但是现在还有个小问题就是..你在什么地方给计算机发布命令呢? 计算机可不是很随便的人啊, 计算机很死板死板到什么程度呢,它只能够在内存(存储器)中接受我们的指令.其他地方…sorry不行.所以我们必须把我们的指令放到内存里面让CPU来读取和执行,所以知道内存的作用了吧,很重要吧,那么我们当然要了解下CPU是如何来读取我执行我们的指令的对吧..所以我们的学习环节里面又多了个,了解CPU是如何在内存中
读取指令和写入指令..
那么到底CPU是如何去读取指令呢,是这样的,CPU把内存划分成一个一个的单元,单元里边的编号是从0开始的,比如有128个存储单元,那么第一个就是0 最后一个就是127,你现在肯定想问拉,那一个存储单元可以存多少信息呢?? 计算机把一个2进制位称为一个比特(bit)8个就是一个字节了,(Byte)实际上现在的电脑啊它的内存是大的不得了的,我们只有了解这个概念就成,比如我现在写文章这台的电脑就有512M内存,那么换算下来是多少呢?
1KB = 1024 B(Byte) 1MB = 1024 KB 1GB = 1024 MB 1TB = 1024 GB
你可能要问了,我这是不是在讲硬盘啊,硬盘上面才用多少个G啊这样标示啊,其实内存和硬盘是一样的,他们的计量单位都是按照上面那些公式来的.
存储单元
刚才给大家扯到,内存被分为多个存储单元,存储单元是从0开始编号.你可能有疑问为什么要编号呢,这里就可以给你回答了,这就向一条街啊,你找一户人家肯定不好找对吧, 但是给他们的房子都弄个编号那就好找了..其实CPU读数据就向找人一样啊,根据门牌号(存储单元编号)这样找就非常快拉.
地址总线
既然说到了编号啊,就给大家聊点编号的问题吧,CPU本是有限啊,只能够指定特定多的存储单元,为什么啊,这个就是由于CPU的地址总线的限制了,比如啊我们的身份证是18位的,当我们国家的人口啊超过18为数字能够容纳的范围的话是无法标示的对吧(不过估计目前是无法达到这个数字的,这都是计划生育搞的好啊),那么现在是CPU有多少地址总线的话就只能够对特定多的内存进行寻址,如果再多出来的内存是没有用的,我们假设CPU有10根地址总线那么我们来看下他的寻址情况,在电子计算机中,一根导线可以传送的稳定状态只有2种,0和1
那么10根地址总线可以标示的范围为, 2的10次方个,最小就是0,那么最大就是1023了.所以可以得出结论,一个CPU有N跟地址线,则可以寻址的范围为2的N次方个内存单元.
数据总线
既然聊了关于存储器(内存)的那么多,不妨再聊点比较有趣的事情,聊什么呢,聊点关于CPU读取指令的问题,首先我想向大家提个问题,比如我现在在内存中有1000条指令啊, CPU来读取这些指令的时候可以一次全部读完吗? 这个问题啊,据我所知啊我们现在用的CPU是没有办法一次全部读完的,那么它怎么办呢,你也想到了,就是分多次来读取对吧.那么问题又来了CPU一次可以读取多少条指令呢? 这要看CPU的数据总线的宽度了,8086的CPU数据总线宽度为16位,是16位2进制的数据比如现在传送 89D9H 8086CPU就可以一次传送完,如果是89D898那么这个数字16位就标示不完了,所以要分两次来传送.
你看到这里我不得不佩服你的勇气,你是一个精神可嘉的人,虽然我写着很累,但是相信你看着也不轻松啊,诶..主要是因为我文采不好写的很烂,让人不大容易明白,其实这些东西都是概念性的东西,我这人最讨厌概念性的东西了,所以我学汇编的时候也是囫囵吞枣就过去了,所以现在给大家讲起来也底气不足..不过没有关系这些概念性的东西没有弄懂也没有关系,了解下就成了,等你到时候把汇编学个入门后再倒回来看就会获益匪浅的.
回忆下我们吹了什么东西, 机器语言, 汇编语言明白了吗? 存储器(内存)的作用明白了吗?存储单元和地址总线还有数据总线这些明白了吗? 如果明白了就过关了,如果没有诶~~~~不是你有问题就是我有问题了..
这段终于给大家吹完了,我也松了一口气,以后的扯淡应该会更加轻松些.所以别紧张.下一篇可能要过几天了,这几天比较忙啊!下一篇就给大家真刀实枪的干了,我们就要接触汇编指令了,期待吧! 请多多支持我的blog,你们的支持是我最大的动力..有动力马才跑的快啊!
汇编教程(3):寄存器CPU工作原理①
2008-11-01 15:54
你真的弄懂了汇编语言基础?? 真的吗? 如果是那么就继续今天的话题啊!!在对CPU读取指令方面有了一个感性的了解后,我们就应该学学寄存器了,寄存器可以说是汇编代码里面必然存在的东西,不然这汇编程序就没法写了,那寄存器是什么呢?CPU到底是怎么样操作寄存器的呢??…
现在的孩子真幸福啊,今天我一个18岁的朋友来象我请教如何配置电脑的问题,呵呵,想象一下啊我18岁都在做些什么呢? 不过非常不幸的是,我那朋友想买电脑来打完美世界.这不得不让我吃惊啊,现在的孩子虽然得到的东西多了,也不知道他们是否知道珍惜啊!!
好了不跟大家扯这些家常了,我本来文采就不怎么好,写这个教程权当自娱自乐啊,希望高手要求不要放太高,但是有错误是一定要提的! 上次写的那篇 汇编基础 收到了不少朋友的反馈,他们说我取的名字不好,不应该叫汇编精通啊(前两篇这个教程叫汇编精通),和我这样的写作风格不相配,想想也是哦,所以改成了现在这个名字.还有不少朋友给我提了很宝贵的意见,其中有位朋友说我写的跑题了..其实我写这个教程不打算教会你汇编,我也没有那个本事,我只是想大家对汇编有个感性的认识.. 或者是你在阅读其他书籍的时候有什么概念上的东西觉得干涩的,我给大家做个感性的介绍而已,我的想法是: 如果你想学习汇编,你手里起码得有几本书吧,比如我在如何学习汇编 那篇文章中给大家介绍那本 王爽写的 汇编语言 就非常不错,我就是靠这本书带入门的,所以希望阁下学习汇编还是以书本为主啊, 我本来技术就有限.甚至连一些概念上的东西弄错了都不知道, 惭愧惭愧啊..
上节课给大家介绍了机器语言和汇编语言,并且对CPU读取指令有了一个感性的认识,如果你没有读懂的话请给我发送邮件 或者参看王爽写的 汇编语言..其实那篇文章说的东西你不懂也没有关系,我觉得和写汇编程序关系不大,特别是写win32汇编程序.. 既然如此就让我们了解今天的话题 CPU的工作原理..
CPU的工作原理
其实CPU从不亲自去作显卡显示图像,声卡发出声音这些事情,而是CPU在自己的逻辑存储器(这是假想中的,这个操作对我们是透明的)地址空间里通过控制总线进行操作,那些外部器件自己在逻辑存储器里面找块地方让CPU分个地址..这个说起来好像比较拗口啊,没有关系如果你不懂也关系不大,如果觉得我讲的不太明白可以参考 王爽汇编
对于一个汇编程序员来说不必要深入的了解这些东西,我们在写程序的时候主要是和寄存器打交道我们通过操作寄存器来达到控制CPU的目的.所以不了解以上讲的问题不是很大的..
寄存器:
既然我们用汇编写程序主要打交道的是寄存器,那么我们有必要深入的了解下寄存器了,寄存器是什么呢?
它其实和内存是一样的东西,只是更加快而已,所以别被他吓倒,那么有多少个寄存器呢? 我可以非常难过的告诉你,寄存器的个数非常多,诶当初我就是看到这个才放弃学习汇编的,不过后来有幸看到了王爽的汇编语言后,才发觉原来寄存器也不过如此啊,我知道你现在想深入的了解寄存器,但是请别急慢慢来,,我们先了解4个寄存器 AX BX CX DX..这4个我们用的最多了,我知道你现在又非常的想了解寄存器到底有什么用,但是请听我说,你还需要具备一点东西,来伙计,我再给你介绍2条常见的汇编指令,非常容易理解:
mov指令,add指令..它们的用法是这样的:
mov ax,19 将19送入AX AX = 19
add ax,18 将寄存器AX的值+18 AX = AX + 18
简单吧,学过高级语言的优势就出来咯,如果你还不懂高级语言赶紧去看下 尚学堂 那个视频啊! 那来考虑下我们在高级语言里面这样一段代码
int a = 10;
int b = 20;
int c;
c = a + b;
我们用汇编该怎么来表示呢?? 我想应该是这样的
mov ax, 10
mov bx, 20
add cx, ax
add cx, bx
明白了吧,寄存器是什么玩意,现在不嫌弃寄存器的个数多了吧,也不觉得寄存器多有什么问题了吧, 毛主席说:代码就能够说明一切问题啊,这应该是有道理的..那么到底有多少寄存器呢,总共只有4个AX, BX CX
DX..但是对我们的编程来说一点问题也没有有了4个就够了~~~..
在8086以前的CPU都是8位的..8086是16位的机器上面的那些寄存器都是16位的(明白什么叫16位了吧),那以前8086以前的CPU呢怎么办呢?intel向了个折中的办法,就是把这些16位的寄存器再进行分家,就分成了高8位和低8位比如
AX 就分为 AH AL
BX 就分为 BH BL
CX 就分为 CH CL
DX 就分为 DH DL
这里H 和L的意思分别表示High和Low好理解了吧,其实它们的用法也是一样的,我们就挑AX来讲讲, 比如现在有一个数字43E4储存在AX中那么AH的值为43,AL为E4为什么呢?intel的CPU是把数据按照高位到低位排列的所以AH(高8位)就存储了43,AL(低8位)就存储了E4,就那么简单,由上我们也可以看出CPU可以处理的数据只有2种,一种是字(word占16位)一种是字节(byte 8位),一个字存放在16位寄存器中,这个字的高位字节和低位字节自然就存储在这个寄存器的高8位和低8位了..比如
AX = 3454H
那么自然的 AH = 34H AL = 54H
同时我们又可以把AH和AL当作单独的两个寄存器来使用它们都互不影响的!如:
AH = 42H
那么 AL还是为54H 但是AX却改变了 AX = 4254H,那么寄存器的内容就这些了,只是有些细节要注意下,请看代码:
mov ax, 8F35H
add ax, 4E34H
add ax, 9F45H
那么现在AX的值为多少呢? 你可能会认为是17CAF,但是我告诉你AX=7CAF,为什么呢如果一个值超过了它所能表示的范围,那么它将把进位丢弃(不是真的丢弃,但是我们现在可以这样认为)同样的事情发生在8位寄存器上,请看:
mov al, 8FH
add al, 4EH
add al, 45H
那么AL的值是22,它同样丢失别以为他会进位到AH或者其他地方,这里我们如果进行的8位运算CPU就只认识8位寄存器,其他的不管,所以以下这些指令都是错误的:
mov ax, bl ‘把16位和8一起用是错的
mov bh, ax ‘把8位和16为一起用也是错的
mov ax, 20000000 ‘把一个大于寄存器的值赋给寄存器也是错的
add al, 10000 ‘将一个大于8位的数据加到8位寄存器也是错的
嗯,终于完了,这是一场攻坚战啊,学汇编和学C不同如果上来就Hello World的话估计大部分人都的晕,所以我们还是要先打下基础啊,虽然现在还是纸上谈兵,但是我们毕竟已经接触到了汇编指令了,再过两结课我们就可以上机写写指令了,但是现在还不行,先好好体会下..接下来我们还要讲下CPU是怎么给出物理地址..好好体会,如果觉得我讲的哪里不对和不明白一定要告诉我!! 我建议你学习的时候结合下书本,效果肯定要好很多的.
汇编教程4:寄存器CPU工作原理②
2008-11-01 15:55
最近在网上看了长篇小说<<疯狂的程序员>>。。嗯不错不错,看来这做程序员的人还大多有些相似的,不过每个人却还是有些不同的,就象我。。诶到现在还是一无所有,人财两空啊~~~~~~命苦不能怪政府。。
上一篇 寄存器CPU工作原理1,给大家介绍了几个寄存器,那几个寄存器因为是最常用的所以也叫通用寄存器,相信大家对这寄存器还是有了一个感性的认识了,但是CPU不能够只访问寄存器的,它要做更多的事情,比如读取内存。这应该说是CPU的基本功能之一吧,所以我们不了解下CPU访问内存的方法确实有点那个啊。。。 既然这样我们还是有必要了解下的。。。。。
CPU要访问内存它必须给出内存单元的地址,同时这些单元都是一维线性的。每一个内存单元都有一个唯一的地址。。这在前面的章节有所提及,那么CPU是怎么样给出内存单元的地址呢??想要了解这个问题,我们还得看看CPU的结构。。
我们通常说的8086CPU是16位的,那么这个16位代表了什么概念呢? 这个嘛。。 概括起来的讲啊主要是这几点。。
寄存器一次最多只能处理16位的数据。
寄存器的最大宽度为16位
寄存器和运算器之间的通路为16位
但是问题就来了,8086有20位地址总线(20位地址总线的寻址能力就有1M了),而CPU内部只能够处理16位的数据那改怎么办呢??这个问题嘛,当然不可以浪费了地址总线对吧。。~~如果是地址总线也改成16位的那么我们的CPU寻址能力就只有64K了,那可是天壤之别啊。。当然的想个办法对不。。别人说intel里面的人都是天才,从这方面说的话还是有些道理的。。他们想到了在CPU内部做一个加法器来进行合成一个20位的物理地址。。那么合成过程是怎么样的呢??嗯,,,好的请听我细细道来。
首先CPU中的相关部件(这个部件我们马上要提到)提供2个16位的地址,然后送入一个叫地址加法器的部件,然后地址加法器再把两个地址合成为一个20位物理地址。。那么现在问题又来了对吧。。。他是怎么样合成物理地址的呢??没有关系这里有个公式。。。套公式总是比想问题简单些的。。。额。合成地址的规则是这样的:
物理地址 = 段地址 * 16 + 偏移地址。。
毛主席说过,实践才是硬道理啊,给大家套个公式就明白了,比如现在CPU要访问内存地址单元124C8H的内存单元,那么我们就可以让段地址为1240H,然后让偏移地址为00C8H,套用公式就比较简单了:
124C8H = 1240H * 16 + 00C8H
注意拉,这里别弄错了,这里我们讨论的数据都是16进制的,所以*16只是把小数点移动了一位而已,嗯。。是不是已经感觉到了16进制的好处了。。当你在感叹16进制是多么好之余有没有想过。。我刚才写的那公式里面讲了什么。段。。段地址。。什么的!这个段地址又是什么概念呢?? 千万别以为是一段一段的地址啊!,其实内存里面是绝对没有分段的,分段只是来自CPU自己。其实分段以后是有好处的,这样我们就可以把一些连续的内存当作一个段来使用,套用上面的公式,段地址不变。。只是改动偏移地址。。哈哈这样就方便多了。我们知道偏移地址也是16位的,那么一个段最长呢也只能有64KB而已,明白了吗? 以为偏移地址只能够表示64KB的内存而已。。。
前面我们讲到了”CPU的相关部件提供两个地址”,这么一句话。。那到底是什么东西在提供段地址呢??又是什么东西提供偏移地址呢??,这又不得不让我想到了寄存器,回忆我们前面学习的(应该还不至于忘记吧)4个通用寄存器,那么是不是CPU内部只有那么多寄存器呢?不是的。。呐。我这里就再给你说一个寄存器。这个寄存器就是专门用来放段地址的,它就是CS寄存器,不过好像叫寄存器就俗了些,不可以体现出它和其他那些寄存器的区别来,因为那些寄存器都是放一般性数据,,这CS寄存器可就不同咯。。所以又给它改个名字,就叫段寄存器吧。。这样理解应该比较好理解吧!!
现在还有个小问题,我们有了一个叫CS的段寄存器,,那还得有一个放偏移地址的寄存器啊,这次倒霉的就是CPU中一个叫IP的寄存器(嗯,别和IP地址的IP弄混了)。。啊,原来是这样啊。现在是万事俱备只欠东风啊。。就让我们来套用这个公式吧。。
物理地址 = CS * 16 + IP
就这样,给出了物理地址,事实上当你为CS为什么不叫EX或者其他的名字的时候,我得告诉你一个天大的秘密:
在8086的CPU上,任意时刻,设CS中的内容为M,IP的内容为N,那么8086将从内存:
M * 16 + N 单元开始执行。。
你可能不以为然呐,这有什么了不起啊,我跟你说这就错了,有了这玩意可以玩的事情可就多了。。你想啊我们想让CPU执行一条指令我们要怎么办呢? 想到了吧,嘿嘿。。让我值得高兴的是,我比你先想到~~~~我们只要设 M * 16 + N 的值为我们想要的内存单元地址那不就得了吗? 对的,确实是这样。。
在任意时刻 CPU将CS : IP指向的内容当作指令执行
为了让你有个更感性的认识我非常有必要举个例子,这是毛主席说的,实践才是硬道理啊,比如我在 内存20000 -20003处有条指令,那么我们想让这条指令执行一下该这么办呢?我想应该是这样的
1。 让CS : IP的值指向 20000处
2。 读取并执行指令,同时IP值要加3
我说的第一点可能比较好理解了,可为什么要加3呢,其实是这样的,你想想啊。。。CPU从20000处开始读取指令,读取到指令后就会返回,然后呢执行,CPU在执行完了这条指令后当然知道这条指令是占几个字节,这里是3个字节。。所以CPU就会把IP的值加3。。。嗯,明白了吧,原来是这样啊。
知识这东西啊,确实是好东西。当然你得先把它学会对吧。。现在让我们回想一下,那个电脑为什么在一按电源就会呼啦啦自动执行指令。。那么我们由此可以判断出,一定是有什么东西在修改着CS : IP的值对吧。。那么事实上是这样的。。
8086在加点启动后(或者重启了电脑) CS : IP的值被分别设置为CS=F000H, IP=FFFFH也就是说CPU在启动的时候从FFFF0H单元开始执行第一条指令,有趣吧。。
所以我们可以得出结论:
想要让指令执行必须让CS:IP指向它,反过来,一条指令如果被执行了,那么CS:IP一定指向过它。。
是不是已经按耐不住了,忍不住想写几条指令给CPU执行了??事实上我是一个不喜欢调人胃口的人,但是此时此刻我不得不提醒你,你还记得mov, add。指令吗?不会那么快就把这两条指令忘了吧,如果忘记了。那么你应该再回头看看 寄存器(CPU工作原理1), 如果你还记得那么就比较简单了,例如我们还是在内存20000-20003处有条指令,那么我们可以这样写代码:
mov cs, 2000H
mov ip, 0H
如果你这样写的话,是要出错的,因为intel公司不给你这样干,这样写的指令是错误的。。你可能想问下为什么不行,那么我可以给你电话,你打电话去intel问下就清楚了,那么既然intel不给我们这样干,他总的提供让我们怎么修改CS和IP的值的方法吧,那么我可以明确的告诉你,方法是有的,就是用另外一条指令,它就是 jmp,使用方法:
同时修改CS 和IP的值: jmp 段地址 : 偏移地址
jmp 1000:2
只修改IP的值: jmp 某一合法寄存器
jmp ax ‘这条指令执行的效果和 mov ip, 0H的效果差不多
如果你看到了这里,并且前面的教程你都看了,并且明白了,那么我不得不佩服你的勇气,与毅力,好好努力,相信自己一定可以把汇编学好的。相信你看到这里也开始心里甜了吧,很多奥秘开始向你解开,我当初就是学到这里的时候想,我一定要把汇编学下去,并且要学好。。相信你也是一样,下一篇我就要给大家将debug的使用了,虽然那东西简陋但是还是有必要了解下的。。因为它简单,学起来方便。。OK今天就到这里吧,晚上2点了!
汇编教程5:debug的使用
2008-11-01 15:56
嗯,这次距离上次文章发布快两个礼拜了,今天我终于痛下决心了。要再写一篇。写到一半,好友 “冰横”,来向我介绍黑莓手机,,黑莓??什么是黑莓手机。诶!看来啊,落后拉。。后来查下资料才发现,这东西确实好啊,又便宜,功能又多。。有时间挑一个。然后在写这篇文章的时候,要找一个屏幕截图的软件,我用的是SangIt这个软件我可是用了好几年了6.0就开始用了,现在都9.0了。非常不错。在WP里面上传的时候弄了好几次都没有成功,不过最后还是成功拉!废话少说,开始今天的话题:debug的使用
上次我们讲了CPU给出物理地址的方法、和介绍了2个段寄存器 CS 和IP。虽然我已经写了好几篇了,但是还没有涉及到任何的编程知识,这在学习高级语言里面是不敢想象的,学高级语言就是好啊,比如C。哈哈上来就写 “hello world”,汇编可不行,,写完这篇,我们还不能写出 “Hello World”,还要下几篇才可以,不过别泄气。通用寄存器那章都挺过来了,这两篇不难。
本来我是想直接和大家讲 “Rad ASM”想想还是作罢,还是讲讲 “debug”吧,让大家了解下计算机内部的原理。实际上你以后在编程工作中根本就用不到“debug”,至少我是很少用到,我都是用 “Rad Asm”里面集成的调试器,直观,方便。所以这章也不用死记,只要了解原理就成。
“debug”是什么呢?debug是一个软件,是一个调试其他软件的软件,既然是软件,当然要启动拉,在windows 2000/xp下启动可以这样,[开始]-[运行]-[cmd]。打开命令行窗口,然后输入命令 [debug],这样就启动了程序
debug是古老的命令行方式运作,命令繁多,一下介绍怕消化不完,那么我们只学习几个常用的。
-R
查看,改变寄存器的值。
-D
查看内存中的内容
-E
改写内存中的内容
-U
将内存中的机器指令翻译为汇编指令
-T
执行一条汇编指令
-A
以汇编的指令的格式在内存中写入一条汇编指令
这就是我们最常用的debug指令了。这样罗列出来给大家看,非常的不直观。。我们就来做几个实例虽然我个人非常不喜欢用图片来讲解计算机的原理性的东西,但是不可否认,这样更容易让人理解:
-R: 查看和修改寄存器的值:我们已经学过6个寄存器了,4个通用的,2个段寄存器。。
上面是在命令行里面显示的信息,,非常简单、你在自己电脑上试试就可以了,告诉你一个小密码,这个命令是系统自带的,所以别问我哪里下载(这个软件在MS-DOS的时候就是系统自带了,所以现在的系统里面都有这个软件。如果你看不明白请在自己的电脑中试验下,实在不明白请看 王爽《汇编语言》,首先最上面是输入了-R命令,然后显示出了很多寄存器的值,有几个我们是认识的,有几个我们还不认识,不过没有关系,我们会在以后的时间介绍,现在就先不管了,反正记得这是显示寄存器的值就是拉,左下角这里显示的 CA12:0100你仔细看看就明白了,刚好对应CS:IP的值对吧。旁边的ADD DH[ DI +48 ] 这就是当前CS:IP指向地址的指令。同样的右下角还有个DS: 0048 = 00这个也不用深究,现在不用了解。。
命令就是这样,很简单,自己试验试验就直观了,如果要改变一个寄存器的值可以这样:
输入命令-R接着要修改的寄存器,然后回车,就输入想要修改寄存器的值就成,然后你要记得举一反三,修改其他寄存器的值方法也是一样的,包括CS:IP寄存器。所以说是非常简单的。。
-R命令:查看内存中的内容:我们可以用 d 段地址:偏移地址 的方式查看,使用方法如下:
输入命令后,默认是显示从你输入的物理地址开始128个内存单元的内容,右边全是点…的这里是该内存单元内容所对应的ASCII码。如果你嫌弃它给你显示的内存单元少了或者多了, 比如现在我们要显示1000:0-1000:23内存单元的内容该怎么做呢? 我们可以试试这样: d:1000:0 23 。这样就把1000:0-1000:23内存单元的内容全部显示出来了,一个不多,一个不少。这里就不截图了,自己试试。
-E命令:改写内存单元的内容:这个比较简单,截图一看你就明白了:
非常简单吧,我们先用d命令查看了1000:0处的内容,然后用e命令修改内存单元的内容,总共修改了11个从43开始到32结束。当然你也可以写入ASCII码值,道理和写入数字是一样的,不过记得字符串要加冒号”"。是英文的冒号。
-U命令:查看内存中该地址对应的汇编指令:
可以看出来,这条命令是非常简单的,这条命令和-D很像,-D是查看内存中的ASCII码,-U是查看对应的汇编指令。非常简单。所以看到这里你就应该明白,在内存中执行指令和数据是没有区别的,关键是CPU如何解释了,如果你让CS:IP指向它,那么这个地方就是执行指令,如果你让~~~,呵呵那个还没有讲呢,下次再讲。
-A命令:以汇编的形式在内存中写入指令:使用方法也是非常简单:
我们首先用-A在2000:0处写入了3条指令,然后用-U查看了2000:0-2000:10的汇编指令,结果当然是显示我们刚才写入的指令了,所以说非常简单。看也看了,写也写了,追究是为了执行,所以我们来看看如何执行汇编指令。
-T命令:执行一条指令,如果没有给出CS:IP,那么就执行默认的CS:IP处的指令:
T命令也是非常简单的,上图我们是先用-R命令查看了寄存器的值,然后用-R修改了CS和IP的值,然后用-A在2000:0处写入了一条指令,最后用-T执行了一下,注意看AX的值,是不是加1了,非常简单吧。。
到这里debug最简单的使用方法就讲完了,这篇我写的特别累,主要是截图,我喜欢安静的写,这样可以让自己的思绪有条理,所以我喜欢汇编,不喜欢用VB,或者delphi这样的可视化工作界面,用汇编你想做什么就做什么,只要你能够想到,限制你的只有你的知识和你的想象空间。debug就已经讲完了,是不是意见有了一些成绩了?你可能不以为然,说这debug能够做什么呢??其实可以做的事情多了,如果你是在没有任何开发工具的情况下,又需要写个小程序做点事情(好像只有黑客这么干),那么debug是非常不错的选择,在网吧或者其他别人的电脑里面想做点破坏你就可以写点汇编指令。非常不错、什么开发工具都不用。当然现在你还没有那个能力,等你再深入的学习下你就可以做到了。
这一篇又写完了,下面我们主要讲下栈,等讲完了栈就可以开始用记事本写自己的“hello World”了,在如何学习汇编 就已经讲过,学习汇编是很吃苦的事情,但是,如果你越过了这道槛,你就会获得很大的收获。有听过 “浴火重生”的故事吗?传说中,凤凰是人世间幸福的使者,每五百年,它就要背负着积累于人世间的所有不快和仇恨恩怨,投身于熊熊烈火中自焚,以生命和美丽的终结换取人世的祥和和幸福。同样在肉体经受了巨大的痛苦和轮回后它们才能得以更美好的躯体得以重生.
脱壳的艺术!(1)
2008-11-01 20:27
概述:脱壳是门艺术——脱壳既是一种心理挑战,同时也是逆向领域最为激动人心的智力游戏之一。为了甄别或解决非常难的反逆向技巧,逆向分析人员有时不得不了解操作系统的一些底层知识,聪明和耐心也是成功脱壳的关键。这个挑战既牵涉到壳的创建者,也牵涉到那些决心躲过这些保护的脱壳者。
本文主要目的是介绍壳常用的反逆向技术,同时也探讨了可以用来躲过或禁用这些保护的技术及公开可用的工具。这些信息将使研究人员特别是恶意代码分析人员在分析加壳的恶意代码时能识别出这些技术,当这些反逆向技术阻碍其成功分析时能决定下一步的动作。第二个目的,这里介绍的信息也会被那些计划在软件中添加一些保护措施用来减缓逆向分析人员分析其受保护代码的速度的研究人员用到。当然没有什么能使一个熟练的、消息灵通的、坚定的逆向分析人员止步的。
关键词:逆向工程、壳、保护、反调试、反逆向
1简介
在逆向工程领域,壳是最有趣的谜题之一。在解谜的过程中,逆向分析人员会获得许多关于系统底层、逆向技巧等知识。
壳(这个术语在本文中既指压缩壳也包括加密壳)是用来防止程序被分析的。它们被商业软件合法地用于防止信息披露、篡改及盗版。可惜恶意软件也基于同样的理由在使用壳,只不过动机不良。
由于大量恶意软件存在加壳现象,研究人员和恶意代码分析人员为了分析代码,开始学习脱壳的技巧。但是随着时间的推移,为防止逆向分析人员分析受保护的程序并成功脱壳,新的反逆向技术也被不断地添加到壳中。并且战斗还在继续,新的反逆向技术被开发的同时逆向分析人员也在针锋相对地发掘技巧、研究技术并开发工具来对付它们。
本文主要关注于介绍壳所使用的反逆向技术,同时也探讨了躲过/禁用这些保护措施的工具及技术。可能有些壳通过抓取进程映像(dump)能够轻易被搞定,这时处理反逆向技术似乎没有必要,但是有些情况下加密壳的代码需要加以跟踪和分析,例如:
需要躲过部分加密壳代码以便抓取进程映像、让输入表重建工具正确地工作。
深入分析加密壳代码以便在一个反病毒产品中整合进脱壳支持。
此外,当反逆向技术被恶意程序直接应用,以防止跟踪并分析其恶意行为时,熟悉反逆向技术也是很有价值的。
本文绝不是一个完整的反逆向技术的清单,因为它只涵盖了壳中常用的、有趣的一些技术。建议读者参阅最后一节的链接和图书资料,以了解更多其他逆向及反逆向的技术。
笔者希望您觉得这些材料有用,并能应用其中的技术。脱壳快乐!
2 调试器检测技术
本节列出了壳用来确定进程是否被调试或者系统内是否有调试器正在运行的技术。这些调试器检测技术既有非常简单(明显)的检查,也有涉及到native APIs和内核对象的。
2.1 ebugged Flag : IsDebuggerPresent()
最基本的调试器检测技术就是检测进程环境块(PEB)1中的BeingDebugged标志。kernel32!IsDebuggerPresent() API检查这个标志以确定进程是否正在被用户模式的调试器调试。
下面显示了IsDebuggerPresent() API的实现代码。首先访问线程环境块(TEB)2得到PEB的地址,然后检查PEB偏移0x02位置的BeingDebugged标志。
mov eax, large fs: 18h
mov eax, [eax+30h]
movzx eax, byte ptr [eax+2]
retn
除了直接调用IsDebuggerPresent(),有些壳会手工检查PEB中的BeingDebugged标志以防逆向分析人员在这个API上设置断点或打补丁。
示例
下面是调用IsDebuggerPresent() API和使用ebugged标志确定调试器是否存在的示例代码。
;call kernel32!IsDebuggerPresent()
call [IsDebuggerPresent]
test eax,eax
jnz .debugger_found
;check ebugged directly
Mov eax,dword [fs:0x30] ;EAX
= sEnvironmentBlock
movzx eax,byte [eax+0x02] ;AL = ebugged
test eax,eax
jnz .debugger_found
由于这些检查很明显,壳一般都会用后面章节将会讨论的垃圾代码或者反—反编译技术进行混淆。
对策
人工将ebugged标志置0可轻易躲过这个检测。在数据窗口中Ctrl+G(前往表达式)输入fs:[30],可以在OllyDbg中查看PEB数据。
另外Ollyscript命令"dbh"可以补丁这个标志。
dbh
最后,Olly Advanced3 插件有置BeingDebugged标志为0的选项。
2.2 alFlag , ags, lags
alFlag PEB另一个成员被称作NtGlobalFlag(偏移0x68),壳也通过它来检测程序是否用调试器加载。通常程序没有被调试时,NtGlobalFlag成员值为0,如果进程被调试这个成员通常值为0x70(代表下述标志被设置):
FLG_HEAP_ENABLE_TAIL_CHECK(0X10)
FLG_HEAP_ENABLE_FREE_CHECK(0X20)
FLG_HEAP_VALIDATE_PARAMETERS(0X40)
这些标志是在ntdll!LdrpInitializeExecutionOptions()里设置的。请注意alFlag的默认值可以通过工具或者在注册表以下位置创建条目来修改:
HKLMSoftwareMicrosoftWindows NtCurrentVersionImage File Execution Options
Heap Flags 由于NtGlobalFlag标志的设置,堆也会打开几个标志,这个变化可以在ntdll!RtlCreateHeap()里观测到。通常情况下为进程创建的第一个堆会将其Flags和ForceFlags4分别设为0x02(HEAP_GROWABLE)和0 。然而当进程被调试时,这两个标志通常被设为0x50000062(取决于NtGlobalFlag)和0x40000060(等于Flags AND 0x6001007D)。默认情况下当一个被调试的进程创建堆时下列附加的堆标志将被设置:
HEAP_TAIL_CHECKING_ENABLED(0X20)
HEAP_FREE_CHECKING_ENABLED(0X40)
示例
下面的示例代码检查alFlag是否等于0,为进程创建的第一个堆是否设置了附加标志(sHeap):
;ebx = PEB
Mov ebx,[fs:0x30]
;Check if alFlag != 0
Cmp dword [ebx+0x68],0
jne .debugger_found
;eax = sHeap
Mov eax,[ebx+0x18]
;Check
Cmp dword [eax+0x0c],2
jne .debugger_found
;Check lags
Cmp dword [eax+0x10],0
jne .debugger_found
对策
可以将 alFlag和ocess标志补丁为进程未被调试时的相应值。下面是一个补丁上述标志的ollyscript示例:
Var peb
var patch_addr
var process_heap
//retrieve PEB via a hardcoded TEB address( first thread: 0x7ffde000)
Mov peb,[7ffde000+30]
//patch alFlag
Lea patch_addr,[peb+68]
mov [patch_addr],0
//patch /ForceFlags
Mov process_heap,[peb+18]
lea patch_addr,[process_heap+0c]
mov [patch_addr],2
lea patch_addr,[process_heap+10]
mov [patch_addr],0
同样地Olly Advanced插件有设置alFlag和sHeap的选项。
2.3 DebugPort: CheckRemoteDebuggerPresent()/NtQueryInformationProcess()
Kernel32!CheckRemoteDebuggerPresent()是另一个可以用于确定是否有调试器被附加到进程的API。这个API内部调用了ntdll!NtQueryInformationProcess(),调用时ProcessInformationclass参数为ProcessDebugPort(7)。而NtQueryInformationProcess()检索内核结构EPROCESS5的DebugPort成员。非0的DebugPort成员意味着进程正在被用户模式的调试器调试。如果是这样的话,ProcessInformation 将被置为0xFFFFFFFF ,否则ProcessInformation 将被置为0。
Kernel32!CheckRemoteDebuggerPresent()接受2个参数,第1个参数是进程句柄,第2个参数是一个指向boolean变量的指针,如果进程被调试,该变量将包含TRUE返回值。
BOOL CheckRemoteDebuggerPresent(
HANDLE hProcess,
PBOOL pbDebuggerPresent
)
ntdll!NtQueryInformationProcess()有5个参数。为了检测调试器的存在,需要将ProcessInformationclass参数设为ProcessDebugPort(7):
NTSTATUS NTAPI NtQueryInformationProcess(
HANDLE ProcessHandle,
PROCESSINFOCLASS ProcessInformationClass,
PVOID ProcessInformation,
ULONG ProcessInformationLength,
PULONG ReturnLength
)
示例
下面的例子显示了如何调用CheckRemoteDebuggerPresent()和NtQueryInformationProcess()来检测当前进程是否被调试:
; using Kernel32!CheckRemoteDebuggerPresent()
lea eax,[.bDebuggerPresent]
push eax ;pbDebuggerPresent
push 0xffffffff ;hProcess
call [CheckRemoteDebuggerPresent]
cmp dword [.bDebuggerPresent],0
jne .debugger_found
; using ntdll!NtQueryInformationProcess(ProcessDebugPort)
lea eax,[.dwReturnLen]
push eax ;ReturnLength
push 4 ;ProcessInformationLength
lea eax,[.dwDebugPort]
push eax ;ProcessInformation
push ProcessDebugPort ;ProcessInformationClass(7)
push 0xffffffff ;ProcessHandle
call [NtQueryInformationProcess]
cmp dword [.dwDebugPort],0
jne .debugger_found
对策
一种方法是在NtQueryInformationProcess()返回的地方设置断点,当这个断点被断下来后,将ProcessInformation 补丁为0。 下面是自动执行这个方法的ollyscript示例:
var bp_NtQueryInformationProcess
// set a breakpoint handler
eob bp_handler_NtQueryInformationProcess
// set a breakpoint where NtQueryInformationProcess returns
gpa "NtQueryInformationProcess",""
find $RESULT,#C21400# //retn 14
mov bp_NtQueryInformationProcess,$RESULT
bphws bp_NtQueryInformationProcess,"X"
run
bp_handler_NtQueryInformationProcess:
//ProcessInformationClass == ProcessDebugPort?
cmp [esp+8],7
jne bp_handler_NtQueryInformationProcess_continue
//patch ProcessInformation to 0
mov patch_addr,[esp+c]
mov [patch_addr],0
// clear breakpoint
bphwc bp_NtQueryInformationProcess
bp_handler_NtQueryInformationProcess_continue:
run
Olly Advanced插件有一个patch NtQueryInformationProcess()的选项,这个补丁涉及注入一段代码来操纵NtQueryInformationProcess()的返回值。
2.4 Debugger Interrupts
在调试器中步过INT3和INT1指令的时候,由于调试器通常会处理这些调试中断,所以异常处理例程默认情况下将不会被调用,Debugger Interrupts就利用了这个事实。这样壳可以在异常处理例程中设置标志,通过INT指令后如果这些标志没有被设置则意味着进程正在被调试。另外,kernel32!DebugBreak()内部是调用了INT3来实现的,有些壳也会使用这个API。
示例
这个例子在异常处理例程中设置EAX的值为0xFFFFFFFF(通过CONTEXT6记录)以此来判断异常处理例程是否被调用:
; set exception handler
push .exeception_handler
push dword [fs:0]
mov [fs:0],esp
;reset flag(EAX) invoke int3
xor eax,eax
int3
;restore exception handler
pop dword [fs:0]
add esp,4
; check if the flag had been set
test eax,eax
je .debugger_found
:::
.exeception_handler:
;EAX = ContextRecord
mov eax,[esp+0xc]
;set flag ()
mov dword [eax+0xb0],0xffffffff
;set
inc dword [eax+0xb8]
xor eax,eax
retn
对策
由于调试中断而导致执行停止时,在OllyDbg中识别出异常处理例程(通过视图->SEH链)并下断点,然后Shift+F9将调试中断/异常传递给异常处理例程,最终异常处理例程中的断点会断下来,这时就可以跟踪了。
另一个方法是允许调试中断自动地传递给异常处理例程。在OllyDbg中可以通过 选项-> 调试选项 -> 异常 -> 忽略下列异常 选项卡中钩选"INT3中断"和"单步中断"复选框来完成设置。
2.5 Timing Checks
当进程被调试时,调试器事件处理代码、步过指令等将占用CPU循环。如果相邻指令之间所花费的时间如果大大超出常规,就意味着进程很可能是在被调试,而壳正好利用了这一点。
示例
下面是一个简单的时间检查的例子。在某一段指令的前后用RDTSC指令(Read Time-Stamp Counter)并计算相应的增量。增量值0x200取决于两个RDTSC指令之间的代码执行量。
rdtsc
mov ecx,eax
mov ebx,edx
;...more instructions
nop
push eax
pop eax
nop
;...more instructions
;compute delta between RDTSC instructions
rdtsc
;Check high order bits
cmp edx,ebx
ja .debugger_found
;Check low order bits
sub eax,ecx
cmp eax,0x200
ja .debugger_found
其它的时间检查手段包括使用kernel32!GetTickCount() API, 或者手工检查位于0x7FFE0000地址的
SharedUserData7数据结构的TickCountLow 及TickCountMultiplier 成员。
使用垃圾代码或者其它混淆技术进行隐藏以后,这些时间检查手段尤其是使用RDTSC将会变得难于识别。
对策
一种方法就是找出时间检查代码的确切位置,避免步过这些代码。逆向分析人员可以在增量比较代码之前下断然后用 运行 代替 步过 直到断点断下来。另外也可以下GetTickCount()断点以确定这个API在什么地方被调用或者用来修改其返回值。
Olly Advanced采用另一种方法——它安装了一个内核模式驱动程序做以下工作:
1 设置控制寄存器CR48中的时间戳禁止位(TSD),当这个位被设置后如果RDTSC指令在非Ring0下执行将会触发一个通用保护异常(GP)。
2 中断描述表(IDT)被设置以挂钩GP异常并且RTDSC的执行被过滤。如果是由于RDTSC指令引发的GP,那么仅仅将前次调用返回的时间戳加1。
值得注意的是上面讨论的驱动可能会导致系统不稳定,应该始终在非生产机器或虚拟机中进行尝试。
2.6 SeDebugPrivilege
默认情况下进程是没有SeDebugPrivilege权限的。然而进程通过OllyDbg和WinDbg之类的调试器载入的时候,SeDebugPrivilege权限被启用了。这种情况是由于调试器本身会调整并启用SeDebugPrivilege权限,当被调试进程加载时SeDebugPrivilege权限也被继承了。
一些壳通过打开进程间接地使用SeDebugPrivilege确定进程是否被调试。如果能够打开意味着进程启用了SeDebugPrivilege权限,由此可以推断进程正在被调试。这个检查能起作用是因为进程安全描述符只允许SYSTEM访问,但是一旦进程拥有了SeDebugPrivilege权限,就可以忽视安全描述符9而访问其它进程。注意默认情况下这一权限仅仅授予了Administrators组的成员。
示例
下面是SeDebugPrivilege检查的例子:
;query for the PID of
call [CsrGetProcessId]
;try to open the process
push eax
push FALSE
push PROCESS_QUERY_INFORMATION
call [OpenProcess]
;if OpenProcess() was successful,
;process is probably being debugged
test eax,eax
jnz .debugger_found
这里使用了ntdll!CsrGetProcessId() API获取的PID,但是壳也可能通过手工枚举进程来得到的PID。如果OpenProcess()成功则意味着SeDebugPrivilege权限被启用,这也意味着进程很可能被调试。
对策
一种方法是在ntdll!NtOpenProcess()返回的地方设断点,一旦断下来后,如果传入的是的PID则修改EAX值为0xC0000022(STATUS_ACCESS_DENIED)。
2.7 Parent Process(检测父进程)
通常进程的父进程是(双击执行的情况下),父进程不是说明程序是由另一个不同的应用程序打开的,这很可能就是程序被调试了。
下面是实现这种检查的一种方法:
1 通过TEB(Id)或者使用GetCurrentProcessId()来检索当前进程的PID
2 用Process32First/Next()得到所有进程的列表,注意的PID(通过ile)和通过32ParentProcessID获得的当前进程的父进程PID
3 如果父进程的PID不是的PID,则目标进程很可能被调试
但是请注意当通过命令行提示符或默认外壳非的情况下启动可执行程序时,这个调试器检查会引起误报。
对策
Olly Advanced提供的方法是让Process32Next()总是返回fail,这样壳的进程枚举代码将会失效,由于进程枚举失效PID检查将会被跳过。这些是通过补丁 kernel32!Process32NextW()的入口代码(将EAX值设为0然后直接返回)实现的。
77E8D1C2 > 33C0 xor eax, eax
77E8D1C4 C3 retn
77E8D1C5 83EC 0C sub esp, 0C
2.8 DebugObject: NtQueryObject()
除了识别进程是否被调试之外,其他的调试器检测技术牵涉到检查系统当中是否有调试器正在运行。
逆向论坛中讨论的一个有趣的方法就是检查DebugObject10类型内核对象的数量。这种方法之所以有效是因为每当一个应用程序被调试的时候,将会为调试对话在内核中创建一个DebugObject类型的对象。
DebugObject的数量可以通过ntdll!NtQueryObject()检索所有对象类型的信息而获得。NtQueryObject接受5个参数,为了查询所有的对象类型,ObjectHandle参数被设为NULL,ObjectInformationClass参数设为ObjectAllTypeInformation(3):
NTSTATUS NTAPI NtQueryObject(
HANDLE ObjectHandle,
OBJECT_INFORMATION_CLASS ObjectInformationClass,
PVOID ObjectInformation,
ULONG Length,
PULONG ResultLength
)
这个API返回一个OBJECT_ALL_INFORMATION结构,其中NumberOfObjectsTypes成员为所有的对象类型在ObjectTypeInformation数组中的计数:
typedef struct _OBJECT_ALL_INFORMATION{
ULONG NumberOfObjectsTypes;
OBJECT_TYPE_INFORMATION ObjectTypeInformation[1];
}
检测例程将遍历拥有如下结构的ObjectTypeInformation数组:
typedef struct _OBJECT_TYPE_INFORMATION{
[00] UNICODE_STRING TypeName;
[08] ULONG TotalNumberofHandles;
[0C] ULONG TotalNumberofObjects;
...
}
TypeName成员与UNICODE字符串"DebugObject"比较,然后检查TotalNumberofObjects 或
TotalNumberofHandles 是否为非0值。
对策
与NtQueryInformationProcess()解决方法类似,在NtQueryObject()返回处设断点,然后补丁 返回的OBJECT_ALL_INFORMATION结构,另外NumberOfObjectsTypes成员可以置为0以防止壳遍历ObjectTypeInformation数组。可以通过创建一个类似于NtQueryInformationProcess()解决方法的ollyscript脚本来执行这个操作。
类似地,Olly Advanced插件向NtQueryObject() API中注入代码,如果检索的是ObjectAllTypeInformation类型则用0清空整个返回的缓冲区。
2.9 Debugger Window
调试器窗口的存在标志着有调试器正在系统内运行。由于调试器创建的窗口拥有特定类名(OllyDbg的是OLLYDBG,WinDbg的是WinDbgFrameClass),使用user32!FindWindow()或者user32!FindWindowEx()能很容易地识别这些调试器窗口。
示例
下面的示例代码使用FindWindow()查找OllyDbg或WinDbg创建的窗口来识别他们是否正在系统中运行。
push NULL
push .szWindowClassOllyDbg
call [FindWindowA]
test eax,eax
jnz .debugger_found
push NULL
push .szWindowClassWinDbg
call [FindWindowA]
test eax,eax
jnz .debugger_found
.szWindowClassOllyDbg db “OLLYDBG”,0
.szWindowClassWinDbg db “WinDbgFrameClass”,0
对策
一种方法是在FindWindow()/FindWindowEx()的入口处设断点,断下来后,改变lpClassName参数的内容,这样API将会返回fail,另一种方法就是直接将返回值设为NULL。
2.10 Debugger Process
另外一种识别系统内是否有调试器正在运行的方法是列出所有的进程,检查进程名是否与调试器(如
,等)的相符。实现很直接,利用Process32First/Next()然后检查映像名称是否与调试器相符就行了。
有些壳也会利用kernel32!ReadProcessMemory()读取进程的内存,然后寻找调试器相关的字符串(如”OLLYDBG”)以防止逆向分析人员修改调试器的可执行文件名。一旦发现调试器的存在,壳要么显示一条错误信息,要么默默地退出或者终止调试器进程。
对策
和父进程检查类似,可以通过补丁 kernel32!Process32NextW() 使其总是返回fail值来防止壳枚举进程。
2.11 Device Drivers
检测内核模式的调试器是否活跃于系统中的典型技术是访问他们的设备驱动程序。该技术相当简单,仅涉及调用kernel32!CreateFile()检测内核模式调试器(如SoftICE)使用的那些众所周知的设备名称。
示例
一个简单的检查如下:
push NULL
push 0
push OPEN_EXISTING
push NULL
push FILE_SHARE_READ
push GENERIC_READ
push .szDeviceNameNtice
call [CreateFileA]
cmp eax,INVALID_HANDLE_VALUE
jne .debugger_found
.szDeviceNameNtice db ".NTICE",0
某些版本的SoftICE会在设备名称后附加数字导致这种检查失败,逆向论坛中相关的描述是穷举附加的数字直到发现正确的设备名称。新版壳也用设备驱动检测技术检测诸如Regmon和Filemon之类的系统监视程序的存在。
对策
一种简单的方法就是在kernel32!CreateFileW()内设置断点,断下来后,要么操纵FileName参数要么改变其返回值为INVALID_HANDLE_VALUE(0xFFFFFFFF)。
2.12 OllyDbg:Guard Pages
这个检查是针对OllyDbg的,因为它和OllyDbg的内存访问/写入断点特性相关。
除了硬件断点和软件断点外,OllyDbg允许设置一个内存访问/写入断点,这种类型的断点是通过页面保护11来实现的。简单地说,页面保护提供了当应用程序的某块内存被访问时获得通知这样一个途径。
页面保护是通过PAGE_GUARD页面保护修改符来设置的,如果访问的内存地址是受保护页面的一部分,将会产生一个STATUS_GUARD_PAGE_VIOLATION(0x80000001)异常。如果进程被OllyDbg调试并且受保护的页面被访问,将不会抛出异常,访问将会被当作内存断点来处理,而壳正好利用了这一点。
示例
下面的示例代码中,将会分配一段内存,并将待执行的代码保存在分配的内存中,然后启用页面的PAGE_GUARD属性。接着初始化标设符EAX为0,然后通过执行内存中的代码来引发STATUS_GUARD_PAGE_VIOLATION异常。如果代码在OllyDbg中被调试,因为异常处理例程不会被调用所以标设符将不会改变。
;set up exception handler
push .exception_handle
push dword [fs:0]
mov [fs:0],esp
;allocate memory
push PAGE_READWRITE
push MEM_COMMIT
push 0x1000
push NULL
call [VirtualAlloc]
test eax,eax
jz .failed
mov [.pAllocatedMem],eax
;store a RETN on the allocated memory
mov byte [eax],0xC3
;then set the PAGE_GUARD attribute of the allocated memory
lea eax,[.dwOldProtect]
push eax
push PAGE_EXECUTE_READ | PAGE_GUARD
push 0x1000
push dword [.pAllocatedMem]
call [VirtualProtect]
;set marker (EAX) as 0
xor eax,eax
;trigger a STATUS_GUARD_PAGE_VIOLATION exception
call [.pAllocatedMem]
;check if marker had not been changed (exception handler not called)
test eax,eax
je .debugger_found
.exception_handler
;EAX = CONTEXT record
mov eax,[esp+0xC]
;set marker () to 0xFFFFFFFF
;to signal that the exception handler was called
mov dword [eax+0xb0],0xFFFFFFFF
xor eax,eax
retn
对策
由于页面保护引发一个异常,逆向分析人员可以故意引发一个异常,这样异常处理例程将会被调用。在示例中,逆向分析人员可以用INT3指令替换掉RETN指令,一旦INT3指令被执行,Shift+F9强制调试器执行异常处理代码。这样当异常处理例程调用后,EAX将被设为正确的值,然后RETN指令将会被执行。
如果异常处理例程里检查异常是否真地是STATUS_GUARD_PAGE_VIOLATION,逆向分析人员可以在异常处理例程中下断点然后修改传入的ExceptionRecord参数,具体来说就是ExceptionCode, 手工将ExceptionCode设为STATUS_GUARD_PAGE_VIOLATION即可。
3 断点和补丁检测技术
本节列举了壳最常用的识别软件断点、硬件断点和补丁的方法。
3.1 Software Breakpoint Detection
软件断点是通过修改目标地址代码为0xCC(INT3/Breakpoint Interrupt)来设置的断点。壳通过在受保护的代码段和(或)API函数中扫描字节0xCC来识别软件断点。
示例
检测可能和下面一样简单:
cld
mov edi,Protected_Code_Start
mov ecx,Protected_Code_End - Protected_Code_Start
mov al,0xcc
repne scasb
jz .breakpoint_found
有些壳对比较的字节值作了些运算使得检测变得不明显,例如:
if ( byte XOR 0x55 == 0x99 ) then breakpoint found
Where: 0x99 == 0xCC XOR 0x55
对策
如果软件断点被发现了逆向分析人员可以使用硬件断点来代替。如果需要在API内部下断,但是壳又检测API内部的断点,逆向分析人员可以在最终被ANSI版API调用的UNICODE版的API下断(如:用LoadLibraryExW代替LoadLibraryA),或者用相应的native API来代替。
3.2 Hardware Breakpoint Detection
另一种断点称之为硬件断点,硬件断点是通过设置名为Dr0到Dr7的调试寄存器12来实现的。Dr0-Dr3包含至多4个断点的地址,Dr6是个标志,它指示哪个断点被触发了,Dr7包含了控制4个硬件断点诸如启用/禁用或者中断于读/写的标志。
由于调试寄存器无法在Ring3下访问,硬件断点的检测需要执行一小段代码。壳利用了含有调试寄存器值的CONTEXT结构,CONTEXT结构可以通过传递给异常处理例程的ContextRecord参数来访问。
示例
这是一段查询调试寄存器的示例代码:
; set up exception handler
push .exception_handler
push dword [fs:0]
mov [fs:0],esp
;eax will be 0xFFFFFFFF if hardware breakpoints are identified
xor eax,eax
;throw an exception
mov dword [eax],0
;restore exception handler
pop dword [fs:0]
add esp,4
;test if EAX was updated (breakpoint identified)
test eax,eax
jnz .breakpoint_found
:::
.exception_handler
;EAX = CONTEXT record
mov eax,[esp+0xc]
;check if Debug Registers 0-Dr3 is not zero
cmp dword [eax+0x04],0
jne .hardware_bp_found
cmp dword [eax+0x08],0
jne .hardware_bp_found
cmp dword [eax+0x0c],0
jne .hardware_bp_found
cmp dword [eax+0x10],0
jne .hardware_bp_found
jmp .exception_ret
.hardware_bp_found
;set to signal breakpoint found
mov dword [eax+0xb0],0xFFFFFFFF
.exception_ret
;set upon return
add dword [eax+0xb8],6
xor eax,eax
retn
有些壳也利用调试寄存器的值作为解密密钥的一部分。这些调试寄存器要么初始化为一个特定值要么为0。因此,如果这些调试寄存器被修改,解密将会失败。当解密的代码是受保护的程序或者脱壳代码的一部分的时候,将导致无效指令并造成程序一些意想不到的终止。
对策
如果壳没检测软件断点,逆向分析人员可以尝试使用软件断点,同样OllyDbg的内存读/写断点也可以使用。当逆向分析人员需要设置API断点的时候在native或者是UNICODE版的API内部设软件断点也是可行的。
3.3 Patching Detection via Code Checksum Calculation
补丁检测技术能识别壳的代码是否被修改(代码被修改则意味着反调试例程已经被禁用了),其次也能识别是否设置了软件断点。补丁检测是通过代码校验来实现的,校验计算包括从简单到复杂的校验和/哈希算法。
示例
下面是一个比较简单的校验和计算的例子:
mov esi,Protected_Code_Start
mov ecx,Protected_Code_End - Protected_Code_Start
xor eax,eax
.checksum_loop
movzx ebx,byte [esi]
add eax,ebx
rol eax,1
inc esi
loop .checksum_loop
cmp eax,dword [.dwCorrectChecksum]
jne .patch_found
对策
如果代码校验例程识别出了软件断点,可以用硬件断点来代替。如果校验例程识别出了代码补丁,逆向分析人员可以通过在补丁地址设置内存访问断点来定位校验例程所在,一旦发现了校验例程,可以修改校验和为预期的值或者在比较失败后修改适当的标志。
/hackbruce/blog/category/%BB%E3%B1%E0%D3%EF%D1%D4
版权声明:本文标题:汇编语言零基础学习 内容由网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:http://roclinux.cn/p/1706238180a504791.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论