C++博客 联系 聚合 管理  

Blog Stats

文章分类(17)

收藏夹(2)

文章档案(18)

相册

Blogs

citywanderer

今天比较搞笑,在看《Inside vc.net》的com的时候,讲到了c++中的vftable,这个虚拟表还真不是很清楚,所以就在网上查了一些关于vftable的资料,地址为:http://blog.csdn.net/gzjh1/archive/2006/05/26/756024.aspx,转贴一下:)
在面向对象的C++语言中,虚函数(virtual function)是一个非常重要的概念。因为它充分体现了面向对象思想中的继承和多态性这两大特性,在C++语言里应用极广。比如在微软的MFC类库中,你会发现很多函数都有virtual关键字,也就是说,它们都是虚函数。难怪有人甚至称虚函数是C++语言的精髓。 
        那么,什么是虚函数呢,我们先来看看微软的解释:
        虚函数是指一个类中你希望重载的成员函数,当你用一个基类指针或引用指向一个继承类对象的时候,你调用一个虚函数,实际调用的是继承类的版本。
                                                               ——摘自MSDN
        这个定义说得不是很明白。MSDN中还给出了一个例子,但是它的例子也并不能很好的说明问题。我们自己编写这样一个例子:
#include 
"stdio.h"
#include 
"conio.h"
 
class Parent
{
public:
 
char data[20];
 
void Function1();
 
virtual void Function2();  // 这里声明Function2是虚函数
}
parent;
void Parent::Function1()
{
 printf(
"This is parent,function1\n");
}

void Parent::Function2()
{
 printf(
"This is parent,function2\n");
}

 
class Child:public Parent
{
 
void Function1();
 
void Function2();
}
 child;
void Child::Function1()
{
 printf(
"This is child,function1\n");
}

void Child::Function2()
{
 printf(
"This is child,function2\n");
}

 
int main(int argc, char* argv[])
{
 Parent 
*p; // 定义一个基类指针
 if(_getch()=='c')     // 如果输入一个小写字母c
       p=&child;         // 指向继承类对象
 else
       p
=&parent;      // 否则指向基类对象
p->Function1();   // 这里在编译时会直接给出Parent::Function1()的
                                   入口地址。
 p
->Function2();    // 注意这里,执行的是哪一个Function2?
 return 0;
}

        用任意版本的Visual C
++或Borland C++编译并运行,输入一个小写字母c,得到下面的结果:
This 
is parent,function1
This 
is child,function2
      为什么会有第一行的结果呢?因为我们是用一个Parent类的指针调用函数Fuction1(),虽然实际上这个指针指向的是Child类的对象,但编译器无法知道这一事实(直到运行的时候,程序才可以根据用户的输入判断出指针指向的对象),它只能按照调用Parent类的函数来理解并编译,所以我们看到了第一行的结果。
        那么第二行的结果又是怎么回事呢?我们注意到,Function2()函数在基类中被virtual关键字修饰,也就是说,它是一个虚函数。虚函数最关键的特点是“动态联编”,它可以在运行时判断指针指向的对象,并自动调用相应的函数。如果我们在运行上面的程序时任意输入一个非c的字符,结果如下:
This 
is parent,function1
This 
is parent,function2
        请注意看第二行,它的结果出现了变化。程序中仅仅调用了一个Function2()函数,却可以根据用户的输入自动决定到底调用基类中的Function2还是继承类中的Function2,这就是虚函数的作用。我们知道,在MFC中,很多类都是需要你继承的,它们的成员函数很多都要重载,比如编写MFC应用程序最常用的CView::OnDraw(CDC
*)函数,就必须重载使用。把它定义为虚函数(实际上,在MFC中OnDraw不仅是虚函数,还是纯虚函数),可以保证时刻调用的是用户自己编写的OnDraw。虚函数的重要用途在这里可见一斑。
      在了解虚函数的基础之上,我们考虑这样的问题:一个基类指针必须知道它所指向的对象是基类还是继承类的示例,才能在调用虚函数时“自动”决定应该调用哪个版本,它是如何知道的?有些讲C
++的书上提到,这种“动态联编”的机制是通过一个“vtable”实现的,vtable是什么?微软在关于COM的文档里这样描述:
        vtable是指一张函数指针表,如同C
++中类的实现一样,vtable中的指针指向一个对象支持的接口成员函数。        
                                                               ——摘自MSDN
        很遗憾,微软这次还是没有把问题说清楚,当然,上面的文档本来就是关于COM的,与我们关心的问题不同。
        那么vtable是什么?我们先来看看下面的实验:
        在前面的示例程序中加一句printf(“
%d”,sizeof(Child));运行,然后去掉Function2()前的virtual关键字,再运行,得到这样的结果:当Function2定义成虚函数的时候,结果是24,否则结果是20。也就是说,如果Function2不是虚函数,一个Child类的示例所占空间的大小仅仅是它的成员变量data数组的大小,如果Function2是虚函数,结果多了4个字节。我们使用的是32位的Visual C++ 6.0,4个字节恰好是一个指针,或者是一个整数所占的空间。
        那么这多出来的四个字节究竟起到了什么作用?
       用Visual C
++打开前面的示例程序,在main函数中p->Function1(); 一句前面按F9设断点,按F5开始调试,输入一个小写c,程序停到了我们设的断点上。找到Debug工具条,按Disassembly按钮,如图所示:
(图呢?CSDN上的这个应该是作者的原创了吧?可是图竟然不见了
>_<!!
算了,不管图了,反正也不是很重要,go on
        我们看到了反汇编后的代码。由上图可见,对Function1和Function2的调用反汇编后生成的代码截然不同。Function1不是虚函数,因此对它的调用仅仅被编译成为一条call指令,转向Parent::Function1子程序;而Function2是虚函数,它的代码要复杂一些,我们来仔细分析:
45:       p->Function2();    
004012CA   mov      eax,dword ptr [ebp
-4]
// eax就是我们的p指针
004012CD   mov      edx,dword ptr [eax]
// edx取child对象头部四个字节
004012CF   mov       esi,esp
004012D1   mov       ecx,dword ptr [ebp
-4]
// 可能要检查栈,不管它
004012D4   call        dword ptr [edx]
// 注意这里,调用了child对象头部的一个函数指针
004012D6   cmp       esi,esp
004012D8   call         __chkesp (004013b0)
这里最关键的一句是call dword ptr[edx],edx是child对象头部,前面我们分析过了,child对象共有24字节,其中成员变量占用20字节,还有4个字节作用未知。现在从这段汇编代码上看,那4个字节很可能就是child对象开头的这个函数指针,因为编译器并不知道我们的成员变量data是做什么用的,更不可能把data的任何一部分当成一个函数指针来处理。
那么这个函数指针会跳转到那里去呢?我们按F10单步运行到这个call指令,然后按F11跟进去:
00401032   jmp         Parent::Function2 (0040bfe0)
00401037   jmp         Parent::Parent (004010d0)
    → 0040103C   jmp         Child::Function2 (
00401250)
00401041   jmp         Child::Child (004011c0)
光标停在了第三行,40103C的地方,执行这里的jmp指令后,又跳转到Child::Function2的位置,从而得到我们上面所看到的结果。
这并不是最终的结论,我们看看40103C周围的几行代码,连续几行全都是jmp指令,这是什么程序结构?有汇编语言编程经验的朋友可能会想起来了,这是一张入口表,分别存放着到几个重要函数的跳转指令!我们再回去看看微软对于vtable的描述:vtable是指一张函数指针表,(如同C
++中类的实现一样,)vtable中的指针指向(一个对象支持的接口)成员函数。打括号的字不要看,这句话的主干就是:vtable是一张函数指针表,指向成员函数。种种事实证明,上面的四行代码就是我们要找的这个vtable!
现在我们应该对虚函数的原理有一个认识了。每个虚函数都在vtable中占了一个表项,保存着一条跳转到它的入口地址的指令(实际上就是保存了它的入口地址)。当一个包含虚函数的对象(注意,不是对象的指针)被创建的时候,它在头部附加一个指针,指向vtable中相应位置。调用虚函数的时候,不管你是用什么指针调用的,它先根据vtable找到入口地址再执行,从而实现了“动态联编”。而不像普通函数那样简单地跳转到一个固定地址。
以上结论仅仅是针对Visual C
++ 6.0编译器而言的,对于其他编译器,具体实现并不完全相同,但都大同小异。著名的“绿色兵团”杂志上撰文介绍,Linux平台上的GNU C++编译器就把指向vtable的指针放在对象尾部而不是头部,而且vtable中仅仅存放虚函数的入口地址,而不是跳转到虚函数的指令。具体的一些细节,篇幅所限,我们这里不再讨论,希望有兴趣的朋友能继续研究。
PS:综上所述,只有对指针和引用,虚函数的魔力才有作用!
这篇文章又讲到了汇编的知识,以前看过一些汇编的资料,但是一直没有弄明白,主要还是因为没有自己去写代码,把上面的代码在vc.net2005中运行了一下,打开Disassemble、Register窗口,里面的东西还真是稀里糊涂的。狂人日记中讲的Assemble还不错,http://krrj.blogbus.com/s42550/(主要是讲注册机的内容,截取其中Assemble的部分)转贴如下,也可以看看http://www.xker.com/Html/bcyy/hbyy/20051118784.htm的《简明x86汇编语言教程
稍微有点儿计算机知识的朋友一定知道,计算机是只识别0和1的,最初那会儿,要写程序,就要用0和1来写,呵呵,Cool吧!所以曾经有过的对程序员的崇拜,可能就源自那个时候吧  后来,人们发现用0和1来写程序,太不爽了,不但写起来不上手,而且回过头来看的话,应该很难再看明白了,总之出于这些原因,就有了汇编语言。 


汇编语言用一些助记符来代替0和1的多种组合,也就是各个指令,这样的话,从一定程度上来说,方便了许多(一头老牛:方便太多了)(一只菜鸟:一点儿也不方便,完全看不懂)。但是,汇编也同样不方便,同样写起来不爽,而且后期维护同样不方便,再加上人们慢慢地需要写一些更大的程序,在这样的情况下,高级语言就被人发明了出来,就是我们今天用的Basic、pascal、C、C
++等等等等,这些语言的出现,一下了使程序的开发难度大大减低了(一头老牛:减低太多了,我膝盖就能写程序了)(一只菜鸟:还不是一样难),以前用汇编要很长时间才能开发出来的程序,现在只需要很短的时间且很轻松的就可以搞定了,特别是最近几年,可视化编程的大肆普及,使程序员的神秘感一下子摔了下来,Coder这样的词现在都满天飞了。最惨的就是汇编,一夜之间变成了低级语言、下流的语言、吃完大蒜不刷牙的民工、开车加完油不给钱的地痞、在公共汽车上吐口水的冰岛人等等等等  


(汇编:呜呜呜…我不活了)。 


但是汇编还是有它先天的优势的,因为其与CPU内部的指令一一对应,所以在一些特殊的场合,必须由汇编来实现,比如访问硬件的端口、写病毒…. 


而且生成的可执行文件效率巨高,且生成的可执行文件贼小,写小程序是很爽的,呵呵,而且用汇编写注册机,是件很轻松的事,你不用再为怎样还原为你所熟悉的语言而为难。说了这么多,还是切入主题吧(昏倒观众若干): 


既然计算机只识别0和1,那么,所有存储在计算机上的文件,也都是以二进制的形式存放的,当然也包括可执行文件了。 


所以,你只要找一个十六进制编辑器比如Ultra Edit什么的,就可直接打开并查看可执行文件了,呵呵,如果你能看懂的话  你会发现,此时看到的,全是些十六进制数值(每4位二进制数可转换为一位十六进制数),这就是可执行文件的具体内容,当然,其中就包括可执行文件的代码了。(一头老牛:好亲切啊)(一只菜鸟:笨牛,你给我闭嘴,我眼都花了)。 


呵呵,此时,你是不是觉得看这些东西,有些那个? 


这些东西看起来就像有字天书,没人能
*这玩意儿来进行分析,于是乎。就有了相应的软件,可以将这些十六进制数值转换为相应的汇编代码,这样的话,我们就可以对别人的软件进行分析了。这就是所谓的逆向分析了。 


呵呵,聪明的你现在一定在想,如果找到软件计算注册码的部分,并对其进行分析,弄懂它的计算方法,那么你不就不用通过¥的方式来进行软件注册了吗?当然,你也可以将此计算过程还原为任意一个你所熟悉的编程语言,那么,编译后的这个程序,就叫做注册机,它的功能就是计算某一特定软件的注册码。(呵呵,是不是经常在软件中看到此类说明?
"禁止制作和提供该软件的注册机及破解程序;禁止对本软件进行反向工程,如反汇编、反编译等") 


作者这样做,心情我们是可以理解的,毕竟人家花了那么多心思在自己的软件上,所以,我不希望你仅仅是因为交不起注册费的原因来学习破解。 


总的说来,上边儿的介绍有点儿太理想化了,上面提到的分析方法,就是所谓的静态分析,此类分析常用的工具有W32DASM、IDA和HIEW等。静态分析,顾名思义,就是只通过查看软件的反汇编代码来对软件进行分析。一般如果只是想暴破软件,只进行静态分析就够了。但要想真正的弄清注册算法,一般还是要进行动态分析的,即能过调试器来一边执行程序一边进行分析。具体内容,我会在《破解原理》和《调试器入门》中详细说明,呵呵,毕竟现在都以经有点儿跑题了。 


我废话说了这么多,其实就是想告诉你汇编的重要性,我不要求你精通,但最少你也得能看懂吧,要不,还谈什么分析?虽然有哥们儿一点儿汇编都不懂就上路了,甚至还破掉了几个软件,但是,这样是不是惨了点儿?难不成你想暴破软件暴破一辈子? 


其实你完全不用惧怕汇编的,看上去怪吓人的,其实跟你平时背那些控件的属性方法差不多,MFC那么多你都搞的定,汇编命令才有多少?而且,汇编不光只是在Crack软件时有用,在好多地方也都有用,且用处巨大,所以我觉得,把汇编拿下,是件义不容辞的事: 


你只要相信它并不难就好了。 


(以下为第二次修改时加入) 


先给你讲一下CPU的组成吧: 


CPU的任务就是执行存放在存储器里的指令序列。为此,除要完成算术逻辑操作外,还需要担负CPU和存储器以及I
/O之间的数据传送任务。早期的CPU芯片只包括运算器和控制器两大部分。到了近几年,为了使存储器速度能更好地与运算器的速度相匹配,又在芯片中引入了高速缓冲存储器(知道为什么P4比P4赛扬贵那么多吗?)。(当!一个硬物飞了过来,话外音:你讲这些做什么,我们又不要设计CPU) 


你急什么嘛,由于汇编比较“低级” ;;所以它是直接操作硬件的,你以为这是用VB呢,想什么时候用变量随手就可以拿来用,你不掌握好CPU内部的一些工作分配情况,到时怎么来看汇编代码啊。(当!又一声,重要还不快点儿说) 


除了高速缓冲存储器之外的组成,大体上可以分为3个部分: 


1.算术逻辑部件ALU(arithmetic logic unit)用来进行算术和逻辑运算。这部分与我们的关系不太大,我们没必要管它。 


2.控制逻辑。同样与我们的关系不大。 


3.这个才是最最重要的。工作寄存器,它在计算机中起着重要的作用,每一个寄存器相当于运算器中的一个存储单元,但它的存取速度却贼快贼快,比存储器要快很多了。它用来存放计算过程中所需要的或所得到的各种信息,包括操作数地址、操作数及运算的中间结果等。下面我们专门的介绍这些寄存器。 


在介绍之前,有必要说点儿基础性的知识。知道什么是32位吧,就是说寄存器是32位的,晕
~~等于没说。在CPU中,一个二进制位被看作是一位,八位就是一个字节,在内存中,就是以字节为单位来在存储信息的,每一个字节单元给以一唯一的存储器地址,称为物理地址,到时候访问相应的内存,就是通过这个地址。八个二进制位都能表达些什么呢?可以表达所有的ASCII码,也就是说一个内存单元可以存储一个英文字符或数字什么的,而中文要用Unicode码来表示,也就是说两个内存单元,才能装一个汉字。十六位就是两个字节这不难理解吧,当然啦,那有了十六位,就肯定有三十二位六十四位什么的,三十二位叫做双字,六十四位就叫做四字。今天我们所使的CPU,相信全是32位的了,除非你用的是286或更早的话。自然而然,CPU中的寄存器,也就是32位的了,也就是说一个寄存器,可以装下32个0或1(这其中不包括段寄存器)。 


大体上来说,你需要掌握的寄存器,有十六个,我一个一个给介绍给你: 


首先,介绍小翠儿(当
!,我自己打我自己一下得了,最近看周星驰看多了),重说,首先,介绍通用寄存器。 


一共八个,分别是EAX、EBX、ECX、EDX、ESP、EBP、EDI、ESI。 


其中,EAX—EDX这四个寄存器又可称为数据寄存器,你除了直接访问外,还可分别对其高十六位和低十六位(还计的我说它们是32位的吗?)进行访问。它们的低十六位就是把它们前边儿的E去掉,即EAX的低十六位就是AX。而且它们的低十六位又可以分别进行八位访问,也就是说,AX还可以再进行分解,即AX还可分为AH(高八位)AL(低八位)。其它三个寄存器请自行推断。这样的话,你就可以应付各种情况,如果你想操作的是一个八位数据,那么可以用 MOV AL (八位数据)或MOV AH (八位数据),如果你要操作的是一个十六位数据,可以用MOV AX (十六位数据)三十二位的话,就用MOV EAX (三十二位数据)也许我这样说,你还是会不明白,没关系,慢慢来,我给你大概画张图吧,虽然不怎么漂亮: 


─────────────────────── 


│                    │          │          │ 


│                    │          │          │ 


│     高十六位      EAX    AH  AX    AL      │ 


│                    │          │          │ 


│                    │          │          │ 


─────────────────────── 


(我倒啊这个图为啥老是不能正常显示?我都重画三遍了)  


明白了吗?不明白没有关系,你就按你自己的理解能力,能理解多少,就理解多少。 


这四个寄存器,主要就是用来暂时存放计算过程中所用的操作数、结果或其它信息。 


而ESP、EBP、EDI、ESI这四个呢,就只能用字来访问,它们的主要用途就是在存储器寻址时,提供偏移地址。因此,它们可以称为指针或变址寄存器。话说回来,从386以后,所有的寄存器都可以用来存储内存地址。(这里给你讲一个小知识,你在破解的时候是不是看到过[EBX]这样的形式呢?这就是说此时EBX中装的是一个内存地址,而真正要访问的,就是那那个内存单元中所存储的值)。 


在这几个寄存器中,ESP称为堆栈指针寄存。堆栈是一个很重要的概念,它是以“后进先出”方式工作的一个存储区,它必须存在于堆栈段中,因而其段地址存放于SS寄存器中。它只有一个出入口,所以只有一个堆栈指针寄存器。ESP的内容在任何时候都指向当前的栈顶。我这样说你可能会觉的还是不明白,那我举个例子吧,知道民工盖房吧,假设有两个民工,一个民工(以下简称民工A)要向地上铺砖,另一个民工(以下简称民工B)给民工A递砖,民工A趴在地上,手边是民工B从远处搬来的板砖,他拿起来就用,民工B从远处搬来后,就还放在那一堆砖上,这样,民工A拿着用后,民工B随既就又补了上去,这就是后进先出。你在脑子里想象一下这个这程。有没有想明白,民工A永远是从最上边开始拿砖。堆栈就是这样,它的基址开始于一个高地址,然后每当有数据入栈,它就向低地址的方向进行存储。相应的入栈指令是PUSH。每当有数据入栈,ESP就跟着改变,总之,它永远指向最后一个压入栈的数据。之后,如果要用压入堆栈的数据,就用出栈指令将其取出。相应的指令是POP,POP指令执行后,ESP会加上相应的数据位数。 


特别是现在到了Win32系统下面,堆栈的作用更是不可忽视,API所用的数据,均是
*堆栈来传送的,即先将要传送的数据压入堆栈,然后CALL至API函数,API函数会在函数体内用出栈指令将相应的数据出栈。然后进行操作。以后你就会知道这点的重要性了。许多明码比较的软件,一般都是在关键CALL前,将真假两个注册码压入栈。然后在CALL内出栈后进行比较。所以,只要找到个关键CALL,就能在压栈指令处,下d命令来查看真正的注册码。具体内容会在后面详细介绍,本章暂不予讨论。 


另外还有EBP,它称为基址指针寄存器,它们都可以与堆栈段寄存器SS联用来确定堆栈中的某一存储单元的地址,ESP用来指示段顶的偏移地址,而EBP可作为堆栈区中的一个基地址以便访问堆栈中的信息。ESI(源变址寄存器)和EDI(目的变址寄存器)一般与数据段寄存器DS联用,用来确定数据段中某一存储单元的地址。这两个变址寄存器有自动增量和自动减量的功能,可以很方便地用于变址。在串处理指令中,ESI和EDI作为隐含的源变址和目的变址寄存器时,ESI和DS联用,EDI和附加段ES联用,分别达到在数据段和附加段中寻址的目的。目前暂时不明白不要紧。 


接下来,再介绍如花(当当当,我再打自己三下算了)接下来,介绍一下专用寄存器,呵呵,有没有被这个名字吓倒?看起来怪专业的。 


所谓的专用寄存器,有两个,一个是EIP,一个是FLAGS。 


我们先来说这个EIP,可以说,EIP算是所有寄存器中最重要的一个了。它的意思就是指令指针寄存器,它用来存放代码段中的偏移地址。在程序运行的过程中,它始终指向下一条指令的首地址。它与段寄存器CS联用确定下一条指令的物理地址。当这一地址送到存储器后,控制器可以取得下一条要执行的指令,而控制器一旦取得这条指令就马上修改EIP的内容,使它始终指向下一条指令的首地址。可见,计算机就是用EIP寄存器来控制指令序列的执行流程的。 


那些跳转指令,就是通过修改EIP的值来达到相应的目的的。 


再接着我们说一下这个FLAGS,标志寄存器,又称PSW(program status word),即程序状态寄存器。这一个是存放条件标志码、控制标志和系统标志的寄存器。 


其实我们根本不需要太多的去了解它,你目前只需知道它的工作原理就成了,我举个例子吧: 


Cmp EAX,EBX  ;用EAX与EBX相减 


JNZ 
00470395   ;不相等的话,就跳到这里; 


这两条指令很简单,就是用EAX寄存器装的数减去EBX寄存器中装的数。来比较这两个数是不是相等,当Cmp指令执行过后,就会在FLAGS的ZF(zero flag)零标志位上置相应值,如果结果为0,也就是他们两个相等的话,ZF置1,否则置0。其它还有OF(溢出标志)SF(符号标志)CF(进位标志)AF(辅助进位标志)PF(奇偶标志)等。 


这些你目前没必要了解那么清楚,会用相应的转移指令就行了。 


最后要介绍的就是段寄存器了(刚才是谁说的樱红?反正不是我) 


这部分寄存器一共六个,分别是CS代码段,DS数据段,ES附加段,SS堆栈段,FS以及GS这两个还是附加段。 


其实现在到了Win32环境下,段寄存器以经不如DOS时代那样重要了。 


所以,我们知道就行了。 


啰嗦了这么多,相信你对CPU以经有了个大概的了解了吧。什么?还是什么也不明白?呵呵,那也不要灰心,请相信这是我的错,是我没有讲清楚而已,你可以去参考一些书籍。我始终觉的,你案头有一本讲汇编的书是非常非常有必要的,我这边儿是清华版的《80x86汇编语言程序设计》沈美明主编,46元。 


我们接下来就再讲一讲一些常用的汇编指令吧。(由于考虑到目前以经有了相应的帖子,所以,我只是从汇编指令中,挑出一些最常用,需要掌握的,更多内容,还请参见书本。) 


CMP A,B 比较A与B其中A与B可以是寄存器或内存地址,也可同时是两个寄存器,但不能同都是内存地址。这个指令太长见了,许多明码比较的软件,就用这个指令。 


MOV A,B 把B的值送给A其中,A与B可是寄存器或内存地址,也可同时是两个寄存器,但不能同都是内存地址。 


Xor a,a异或操作,主要是用来将a清空 


LEA装入地址,例如LEA DX,
string 将字符的地址装入DX寄存器 


PUSH 压栈 


POP 出栈 


ADD 加法指令 格式:ADD DST,SRC 执行的操作:(DST)
<-(SRC)+(DST) 


SUB 减法指令 格式:SUB DST,SRC 执行的操作:(DST)
<-(DST)-(SRC) 


MUL 无符号乘法指令 格式: MUL SRC  执行的操作:字节操作(AX)
<-(AL)*(SRC);字操作(DX,AX)<-(AX)*(SRC);双字操作:(EDX,EAX)<-(EAX)*(SRC) 


DIV 无符号除法指令 格式:DIV SRC  执行的操作:字节操作:16们被除数在AX中,8位除数为源操作数,结果的8位商在AL中,8位余数在AH中。表示为: 


(AL)
<-(AX)/(SRC)的商,(AH)<-(AX)/(SRC)的余数。字操作:32位被除数在DX,AX中。其中DX为高位字,16位除数为源操作数,结果的16位商在AX中,16位余数在DX中。表示为:(AX)<-(DX,AX)/(SRC)的商,(DX)<-(DX,AX)/(SRC)的余数。 


双字操作:64位的被除数在EDX,EAX中。其中EDX为高位双字;32位除数为源操作数,结果的32位商在EAX中,32位余数在EDX中。表示为: 


(EAX)
<-(EDX,EAX)/(SRC)的商,(EDX)<-(EDX,EAX)/(SRC)的余数。 


NOP 无作用,可以用来抹去相应的语句,这样的话,嘿嘿嘿… 


CALL调用子程序,你可以把它当作高级语言中的过程来理解。 


控制转移指令: 


JE 或JZ 若相等则跳 


JNE或JNZ 若不相等则跳 


JMP 无条件跳 


JB 若小于则跳 


JA 若大于则跳 


JG 若大于则跳 


JGE 若大于等于则跳 


JL 若小于则跳 


JLE 若小于等于则跳 


总的来说,以上几个,都是比较常见的,需要掌握,但需要掌握的绝不止这几个,其它的指令希望你能在私下里再了解一下,可以找相应的教程来看。 


刚才忘了,现在再把数制转换也给贴上: 


首先说二进制转换为十进制的问题: 


各位二进制数码乘以与其对应的权之和即为该二进制相对应的十进制数。例如: 


10100=2的4次方+2的2次方,也就是十进制数20。 


11000=2的4次方+2的3次方,也就是十进制数24。 


接着说一下十进制数转换为二进制数的方法: 


这样的方法到底有多少,我也不清楚,我只讲最简单的一个
-除法: 


把要转换的十进制数的整数部分不断除以2,并记下余数,直到商为0为止。 


例:N
=34D(说明一下,你可能在某些数字的后边看到过加有一个字母,这个字母便是用来表示数制的,十进制数用D,二进制数用B,八进制数用O,十六进制数用H) 


  
34/2=17     (a0=0


  
17/2=8      (a1=1


  
8/2=4       (a2=0


  
4/2=2       (a3=0


  
2/2=1       (a4=0


  
1/2=0       (a5=1


所以N
=34D=100010B。 


对于被转换的十进制数的小数部分则应不断乘以2,并记下其整数部分,直到结果的小数部分为0为止。 


十六进制数与二进制数、十进制数之间的转换: 


总的来说,十六进制数与二进数之间的转换,应该算是很简单的了,你只需把与之相对应的数值进行转换就成了。 


十六进制数的基数是16,共有16个数码,它们是0,
1,2,3,4,5,6,7,8,9,A,B,C,D,E,F。其中A表示十进制中的10,其余类推。它们与二进制和十进制数的关系如下: 


0H
=0D=0000B,1H=1D=0001B,2H=2D=0010B,3H=3D=0011B,4H=4D=0100B,5H=5D=0101B,6H=6D=0110B,7H=7D=0111B,8H=8D=1000B,9H=9D=1001B,AH=10D=1010B,BH=11D=1011B,CH=12D=1100B,DH=13D=1101B,EH=14D=1110B,FH=15D=1111B 


所以,二进制与十六进制之间要进行转换的话,只要把它们由低到高每四位组成一级,直接用十六进制来表示就可以了: 


例:   
1000      1010      0011       0101 


       
8         A         3          5 


十六进制转二进制则用只需将每一位用四位二进制数来表示就成了: 


例:     A         B         
1          0 


     
1010      1011       0001       0000 


最后是十六进制数与十进制数之间的互相转换 


十六进制数转十进制数 


各位十六进制数与其对应权值的乘积之和即为与此十六进制数相对应的十进制数。 


例:N
=BF3CH 


   
=11*16的3次方+15*16的2次方+3*16的1次方+12*16的0次方 


   
=11*4096+15*256+3*16+12*1 


   
=48956D 


十进制转十六进制 


我还是只讲最简单的除法: 


把要转换的十进制数的整数值部分不断除以16,并记下余数,直到商为0为止。 


例N
=48956D 


   
48956/16=3059       (a0=12


   
3059/16=191         (a1=3


   
191/16=11           (a2=15


   
11/16=0             (a3=11


所以N
=48956D=BF3CH。 


通过以上的介绍,我不知道你到底看懂没有,如果有的话,请你去看一下书本,把我没讲到的地方和讲过了的地方都仔细地看几遍。如果你根本就没有看懂,那么你就更需要去看书了,不要因为这就丧失掉学习的信心。你认真地把前边儿的CPU介绍看完,弄清楚寄存器的概念,再把后边汇编指令拿下,就可以上路了。你认真学,认真背的话,会发现其实并没你想像中的那么难。一星期的时间,就可大概掌握了,但只是掌握而已,最起码可以看懂汇编代码了。要真想学好的话,就连后边儿的也一同看了吧,再写一些小程序来练练手。当然想精通汇编,那可不是一天两天一月两月的事,但你只要有恒心,有什么搞不定的?CPU也是人做的,指令只是其中的一部分而已,人家能做出CPU,你还怕连使用都学不会? 



课后FAQ 


Q:我以前学过8086
/8088,并且也在DOS下写过程序,能行吗? 


A:绝对能行,相对8086
/8088,现在的CPU在基本指令方面,也没有添加多少新的指令。你只需了解一下各寄存器的变化以及补充一下Windows程序的知识就成了。而且,既然你用汇编在DOS下写过程序,那么对Debug等调试器,肯定已经很上手了,所以你有先天的优势。 


Q:汇编对我来说不成问题,可我为什么总是不上手呢? 


A:呵呵,这样的老鸟倒还有不少,他们把汇编用的相当熟练,但是,只是因为经验的原因,所以才觉的不上手的,许多人当初不也都这样吗?最起码我就是,见了CALL就跟进,呵呵,倒跟了不少API,所以对于这部分高手,你只需多练练手以及掌握一些分析的技巧就成了。 


Q:我没学过编程,能学汇编吗? 


A:总的来说,也行。不过希望汇编的学习,不会使你丢掉学习其它高级语言的信心。:) 



答网友问 


Q:寄存器可以随便用么,有没有什么限制?写个程序的时候那些变量什么的可以放在任意的寄存器么? 


A:呵呵,我现在就来回答楼上朋友的问题。 


寄存器有它的使用机制,及各个寄存器都有着明确的分工。 


如小翠儿  如数据寄存器(EAX
-EDX),它们都是通用寄存器,及在软件中,任何数据都可存放于此。但是除此之外,它们又可以都可以用于各自的专用目的。 


例如: 


EAX可以作为累加器来使用,所以它是算术运算的主要寄存器。在乘除法等指令中指定用来存放操作数。比如在乘法中,你可以用AL或AX或EAX来装被乘数,而AX或DX:AX或EAX或EDX:EAX则用来装最后的积。 


EBX一般在计算存储器地址时,它经常用作基址寄存器。 


ECX则常用来保存计数值,如在移位指令它用来装位移量、循环和串处理指令中作隐含的计数器。 


最后就剩下四大天王中的黎明了,近一段时间来,他总是比较低调(你别打我了,我去撞墙好了)最后就剩下EDX了,一般在作双字长运算时把DX和AX组在一起存放一个双字长数(你还记的什么是双字长吧,举个例子,比如说有一个数二进制数据01101000110101000100100111010001,你要把它寄存起来,就可以把0110100011010100(即高十六位)放在DX中,把0100100111010001(即低十六位)放在AX中,这个数表示为DX:AX)当然完全可以用一个EDX就把这个数给装下。所以,还可以用EDX:EAX来装一个64位数据,这个你会推断出来吧。 


而ESP、EBP、EDI、ESI,我上边儿以经大概介绍的差不多了,所以这里不说它们了。 


当然还有其它的一些限制,因为我们只是要看程序的汇编代码(人家写好了的,肯定不会犯错误吧),而不是要去写,所以可以不必掌握。有性趣的话,去看相关书籍。 


另外再说一下你的最后一个问题“写个程序的时候那些变量什么的可以放在任意的寄存器么? ”这句话我不明白你要问的是什么。我想你可能是把一些关点给搞错了,变量这词通常都是出现在高级语言中的,而你用高级语言写程序的话,完全不用理解那些寄存器什么的,这些都跟高级语言没什么关系。但是最终,高级语言也还是把你写的程序转换为对寄存器、内部存储器的操作。 


<本章完>


 1int fun()
 2
 3    int a=0
 4    register int i; 
 5    for (i=0; i<1000; i++)
 6    {
 7        a+=i; 
 8    }

 9    return a; 
10}

11
12int main(int argc, char* argv[])
13{
14    int a;
15    a = fun();
16    printf("%d\n",a);
17
18    return 0;
19}
上面这段程序的Assembly为:
//ESP为堆栈指针,每push一个register,ESP减4始终指向栈顶
//每一个语句的前部分那些数字表示EIP

004125F0  push        ebp        
//ebp 0012FFC0
004125F1  mov         ebp,esp    //esp 0012FF70->EBP = 0012FF70
004125F3  sub         esp,44h    //ESP = 0012FF2C
004125F6  push        ebx        //ESP = 0012FF28
004125F7  push        esi        //ESP = 0012FF24
004125F8  push        edi        //ESP = 0012FF20
    int a;
    a 
= fun();
004125F9  call        fun (4116D6h)
/*
004116D6  jmp         fun (4125A0h)    //ESP = 0012FF1C

int fun()
{                     //ESP = 0012FF1C
004125A0  push        ebp          //EBP = 0012FF70    ESP = 0012FF18
004125A1  mov         ebp,esp         //EBP = 0012FF18在fun函数中未变
004125A3  sub         esp,48h         //ESP = 0012FED0
004125A6  push        ebx          //ESP = 0012FECC
004125A7  push        esi          //ESP = 0012FEC8
004125A8  push        edi          //ESP = 0012FEC4一直未变直到pop栈
    int a=0; 
//a的值:0x6bc9b010(随即数)           //0012FF14 = 6BC9B010(Register窗口中) //表示0012FF14处的值为6BC9B010
//dword表示双字节,ptr表示a为内存中的变量?[a]表示a的值
004125A9  mov         dword ptr [a],0      //0012FF14(a的地址?)之后连续四个字节的值为0
//i的值:0x0041b4e6            //0012FF10 = 0041B4E6
    register int i; 
    for (i=0; i<1000; i++)
004125B0  mov         dword ptr [i],0     //0012FF10之后连续四个字节的值为0
//fun函数的起始地址为0x004125a0
004125B7  jmp         fun+22h (4125C2h) //0x004125a0+22h==0x4125C2h

004125B9  mov         eax,dword ptr [i] 
004125BC  add         eax,1 
//0012FF10 = 00000001 
//第一次运行到这里可以在Memory窗口中看到0x0012FF10处的值为01 00 00 00要反过来看值
004125BF  mov         dword ptr [i],eax 
//0012FF10 = 00000000
004125C2  cmp         dword ptr [i],3E8h 
//如果i地址中的之[i]与3E8h(1000)相等,跳到EIP=0x4125D6处即跳出循环
004125C9  jge         fun+36h (4125D6h) 
    {
        a+=i; 
//将a的值放到寄存器Eax中
004125CB  mov         eax,dword ptr [a]    //EAX = 00000000 a、i都为0
004125CE  add         eax,dword ptr [i] //EAX = 00000000
004125D1  mov         dword ptr [a],eax 
    }
//无条件跳到4125B9h处即i++
004125D4  jmp         fun+19h (4125B9h) 
    return a; 
004125D6  mov         eax,dword ptr [a] 
}
//之前ESP = 0012FEC4 EBP = 0012FF18
004125D9  pop         edi  //ESP = 0012FEC8
004125DA  pop         esi  //ESP = 0012FECC
004125DB  pop         ebx  //ESP = 0012FED0
004125DC  mov         esp,ebp //ESP = 0012FF18
004125DE  pop         ebp  //ESP = 0012FF1C EBP = 0012FF70与main函数调用fun时的值相等
004125DF  ret    
*/

//ESP = 0012FF20 EBP = 0012FF70
004125FE  mov         dword ptr [a],eax 
    printf(
"%d\n",a);
00412601  mov         eax,dword ptr [a] 
00412604  push        eax  
00412605  push        offset string "%d\n" (42DB5Ch) 
0041260A  call        @ILT
+2495(_printf) (4119C4h) 
0041260F  add         esp,
8 

    
return 0;
//将Eax中的值置为0
00412612  xor         eax,eax 
}
其中那些进栈出栈的作用还不明白
posted on 2006-07-02 16:46 citywanderer 阅读(419) 评论(0)  编辑 收藏 引用 所属分类: C++

只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理