找回密码
 注册成为果猿

QQ登录

只需一步,快速开始

微信登录

微信扫一扫,快速登录

搜索
  • 果壳院刊

果壳疯狂编程之机械姬系列:汇编之美

果壳王子 2020-12-13 22:40:22 显示全部楼层 阅读模式

果壳式疯狂编程(机械姬系列):汇编之美

果壳式疯狂编程(机械姬系列):汇编之美


果壳疯狂编程(机械姬系列):汇编之美

    原创 果壳王子 果壳学院
          即便把我关在果壳中,我仍然是无限空间之王!
创作声明:本书内容包含虚构创作
书中的情节存在虚构加工,如有雷同,那就雷同



    1第一章:汇编基础知识

    第1章:汇编基础知识



    学习CE(游戏修改工具)不得不先了解相关汇编知识。如同爱必经历纯洁的革命友谊升华到初恋,才更懂珍爱。
    汇编——就是我们的初恋!

    汇编语言是和具体的微处理器相联系的,每一种微处理器的汇编语言都不一样,因此我们只能通过一种常用的结构简洁的微处理器汇编语言来进行学习。

    本书采用8086CPU为中央处理器的计算机进行讲解。8086CPU结构简洁,便于疯狂式汇编教学。

    最疯狂且优美的,应是最简洁有力的!

    —— 果壳学院疯狂编程 机械姬系列:汇编之美
    2(1.1) 机器语言

    1.1 机器语言

    说到汇编语言的产生,首先要讲一下机器语言。机器语言是机器指令的集合。什么是机器指令?我们在使用CE时,常常见到。请看下图:

    果壳疯狂编程:汇编语言从入门到神通

    果壳疯狂编程:汇编语言从入门到神通


    图中所示的就是机器指令(或称机器码),这是十六进制的,如果要让计算机识别,则必须是二进制的。

    例如机器指令 89 5C 24 38 转为二进制是10001001010111000010010000111000


    你看得懂上面这条机器指令的具体含义是什么吗?不懂是吧,我也不懂。可见,机器指令是如此晦涩难懂,于是汇编语言便孕育而生了。

    汇编语言是程序猿中的普罗米修斯为猿类盗取的编程圣火(注意该描述和下图描述不是同一个神话体系)

    果壳疯狂编程:汇编语言从入门到神通

    果壳疯狂编程:汇编语言从入门到神通

    上帝说,要有光,于是便有了光……

    ──《圣经 ● 创世纪》

    3(1.2) 汇编语言

    1.2 汇编语言

    汇编语言的主体是汇编指令,汇编指令我们在使用CE时,也是常常见到。见下图:

    果壳疯狂编程:汇编语言从入门到神通

    果壳疯狂编程:汇编语言从入门到神通


    汇编指令和机器指令是一一对应的。比如:机器指令 89D8和汇编指令mov ax,bx的含义是相同的,然而我们很难懂89D8的含义是什么,mov ax,bx的含义一看就懂了,其含义是:把寄存器bx中的数据传送给ax。


    汇编指令的写法与人类语言接近,便于阅读和记忆。既然有了如此简明易懂的汇编指令,为什么不把机器指令抛弃掉?因为计算机指令最终是由CPU来执行的,但是CPU只认识机器指令,不认识汇编指令。

    简言之:汇编指令是给人看的,机器指令是给狗看的──不对,是给CPU看的(写得太入戏,有点儿鸡动)。

    有一个东西可以把汇编指令翻译成机器指令,这个东西就是汇编语言编译器。

    程序员用汇编语言写出源程序,再用编译器将其编译为机器码,由计算机最终执行,下图描述了这个工作过程:

    果壳疯狂编程:汇编语言从入门到神通

    果壳疯狂编程:汇编语言从入门到神通

    4(1.3) 指令和数据

    1.3 指令和数据

    CPU是计算机的核心部件,它控制整个计算机的运作并进行运算,要想让一个CPU工作,就必须向它提供指令和数据,指令和数据在存储器中存放。存储器也就是我们平常说的内存。

    在内存中,指令和数据没有任何本质上的区别,都是二进制信息,只不过CPU在工作的时候,把有的信息看作指令,把有的信息看作数据,即为同样的信息赋予了不同的意义!


    就好比同样叫“黑呀呀”,有的同学理解为可爱的黑鸭鸭,有的同学理解为冷艳执鞭黑衣女教师——是不是很带画面感?

    又如围棋的棋子,在棋盒里的时候没有任何区别,在对弈的时候就有了不同的意义。

    同样,内存中的二进制信息 1000100111011000,计算机既可以把它看作 89D8H 数据本身来处理,也可以把它看作指令 mov ax,bx 来处理。

    1000100111011000 → 89D8H(数据)

    1000100111011000 → mov ax,bx(指令)


    那么,CPU在什么时候把它看作数据?在什么时候把它看作指令呢?这个在后面会讲到。
    5(1.4) 存储单元

    1.4 存储单元


    存储器被划分成若干个存储单元,每个存储单元从0开始顺序编号,例如一个存储器有128个存储单元,编号从0~127,见下图:

    果壳疯狂编程:汇编语言从入门到神通

    果壳疯狂编程:汇编语言从入门到神通


    那么一个存储单元能存储多少信息呢?我们知道计算机的最小信息单位是bit,也就是一个二进制位,8个bit组成一个Byte,也就是通常讲的一个字节,微机存储器的一个存储单元可以存储一个Byte,即8个bit(8个二进制位),一个存储器有128个存储单元,它可以存储128个Byte。

    微机存储器的容量是以字节为最小单位来计算的,对于拥有128个存储单元的存储器,我们可以说它的容量是128个字节。

    对于大容量的存储器,一般还用以下单位来计量容量(以下用B代表Byte)。

    1KB=1024B   1MB=1024KB   1GB=1024MB   1TB=1024GB
    6(1.5) CPU对存储器的读写

    1.5 CPU对存储器的读写

    以上讲到,存储器被划分成多个存储单元,存储单元从0开始顺序编号,这些编号就是存储单元的内存地址,见下图:

    果壳疯狂编程:汇编语言从入门到神通

    果壳疯狂编程:汇编语言从入门到神通


    CPU要从内存中读取或写入数据,首先要指定内存地址,并指明要对哪一个器件进行操作,是读取数据还是写入数据,因而,CPU要想进行数据的读写,必须和外部器件的芯片进行下面3类信息的交互:

    1:内存地址(地址信息)
    2:器件的选择,读或写命令(控制信息)
    3:读或写的数据(数据信息)


    那么CPU是通过什么将地址、控制信息和数据传到内存中呢?是导线,是连接CPU和其他芯片的导线,通常称之为总线。

    根据传送信息的不同,总线从逻辑上分为3类:地址总线、控制总线、数据总线。

    CPU从内存3读取数据的过程如下图:

    果壳疯狂编程:汇编语言从入门到神通

    果壳疯狂编程:汇编语言从入门到神通


    1:CPU通过地址线将地址信息3发出。
    2:CPU通过控制线发出内存读命令,选中存储器芯片,并通知它,将要从中读取数据。
    3:存储器将内存地址3中的数据8通过数据线送入CPU。


    写入数据的过程和读取数据的过程相似。例如向内存地址3写入数据26,操作步骤如下。

    1:CPU通过地址线将地址信息3发出。
    2:CPU通过控制线发出内存写命令,选中存储器芯片,并通知它,将要从中写入数据。
    3:CPU通过数据线将数据26送入内存地址3的存储单元中。


    从上面我们知道了CPU是如何进行数据读写的工作原理,可是,如何发出具体指令命令计算机进行数据读写呢?这就需要依靠能够驱动它进行工作的简洁而有力的机器指令了!

    对于8086CPU,下面的机器指令就能够完成从内存地址3的存储单元中读取数据的操作。

    机器指令:101000000000001100000000

    汇编指令:mov ax,[3]

    含义:从内存地址3的存储单元读取数据送入寄存器ax中。


    7第二章:寄存器(CPU工作原理)

    第2章:寄存器(CPU工作原理)



    一个典型的CPU,由运算器、控制器、寄存器等器件组成,对于游戏修改者来说,重点学习寄存器,其它不必管。

    不同的CPU,寄存器的个数、结构是不相同的,8086CPU有14个寄存器,每个寄存器有一个名称,我们对它进行分类:

    1. 通用寄存器:AX、BX、CX、DX
    2. 段寄存器:CS、SS、DS、ES
    3. 指针寄存器:SP、BP
    4. 变址寄存器:SI、DI
    5. 指令指针寄存器:IP
    6. 标志寄存器:FR


    以上寄存器都是16位的,更古老的CPU的寄存器是8位的,这个就不讲了。现在的CPU的寄存器是32位的,这个将在后续章节中与16位寄存器一起讲解。
    8(2.1) 通用寄存器

    2.1 通用寄存器

    AX、BX、CX、DX这4个寄存器通常用来存放一般性的数据,被称为通用寄存器。

    以AX为例,寄存器的逻辑结构图如下:

    果壳疯狂编程:汇编语言从入门到神通

    果壳疯狂编程:汇编语言从入门到神通


    一个16位寄存器可以存储一个16位的数据,数据在寄存器中的存放情况如下图所示:

    果壳疯狂编程:汇编语言从入门到神通

    果壳疯狂编程:汇编语言从入门到神通


    黑呀呀提问:一个16位寄存器所能存储的数据的最大值是多少?

    王子答曰:16位即16个bit,16个bit等于2个字节,范围从0~65535,最大值是65536B,因为1KB=1024B,65536B÷1024B=64KB,所以最大值是64KB。

    8086CPU的AX、BX、CX、DX这4个寄存器为了兼容8位寄存器,每个寄存器都可以分为两个可独立使用的8位寄存器。

    AX可分为AH和AL,BX可分为BH和BL,CX可分为CH和CL,DX可分为DH和DL。

    8086CPU的16位寄存器分为2个8位寄存器的情况如下图所示:

    果壳疯狂编程:汇编语言从入门到神通

    果壳疯狂编程:汇编语言从入门到神通


    AX的低8位(0位~7位)构成了AL寄存器,高8位(8~15位)构成了AH寄存器。对于AH和AL的名字可以这样理解:H=High(高的),L=Low(低的)。


    AH和AL寄存器都是可以独立使用的8位寄存器,下图展示了16位寄存器及它所分成的2个8位寄存器的数据存储的情况:

    果壳疯狂编程:汇编语言从入门到神通

    果壳疯狂编程:汇编语言从入门到神通


    小洁儿提问:一个8位寄存器所能存储的数据的最大值为多少?

    王子答曰:8位即8个bit,8个bit等于1个字节,范围从0~255,所以最大值是256B。
    9(2.2) 字在寄存器中的存储

    2.2 字在寄存器中的存储

    字节,即为byte,一个字节由8个bit组成。字,即为word,一个字由两个字节组成,这两个字节分别称为这个字的高位字节和低位字节,见下图:

    果壳疯狂编程:汇编语言从入门到神通

    果壳疯狂编程:汇编语言从入门到神通


    一个字可以存在一个16位寄存器中,这个字的高位字节和低位字节分别存在这个寄存器的高8位寄存器和低8位寄存器中。

    妲己大师娘卡特黑兰:“说了半天,这都是干嘛用的,这和咱们要学的CE有几毛钱关系鸭?”

    王子曰:“切莫性急,和鸭没关系,和鸡有关,温火方能煲好靓鸡汤,提前热身,方能持久作战——为日后而战,鸡动起来吧,骚年!”

    果壳疯狂编程:汇编语言从入门到神通

    果壳疯狂编程:汇编语言从入门到神通
    “燃烧我的荷尔蒙!”

    10(2.3) 物理地址

    2.3 物理地址

    在1.4节我们学习了存储单元,存储单元又叫内存单元,以后我们多数用内存单元这一名称。

    所有的内存单元构成的存储空间是一个一维的线性空间,每一个内存单元在这个空间中都有唯一的地址,我们将这个唯一的地址称为物理地址。

    CPU通过地址总线送入内存的,必须是一个内存单元的物理地址,在CPU向地址总线上发出物理地址之前,必须要在内部先形成这个物理地址,不同的CPU可以有不同的形成物理地址的方式,我们现在讨论8086CPU是如何在内部形成内存单元的物理地址的。

    在进入下一个小节深入讲解8086CPU如何给出物理地址的方法之前,我们先分享一则圣经小故事,放松一下肌肉和心情。

    上帝使他沉睡,他就睡了,于是上帝取下他一条肋骨,又把肉合起来(旁白:“哇靠这都行!”)。

    耶和华(又名“爷火化”)就用那人身上所取的肋骨,造出一个女人,领她到那人跟前(那会儿的福利可真好)。

    那人便是亚当,当他醒来看到上帝这一件新作品时,便说道:“这是我的骨中骨、肉中肉,可以称她为女人,因她是从男人身上取出来的。”

    于是,美丽的夏娃诞生了!她和亚当幸福快乐地生活在伊甸园……据说后来,夏娃在苹果树下偷吃了一个苹果,并把那苹果带回来分给亚当吃……自此以后,他们就更觉"幸福"了……阿门。



    果壳疯狂编程:汇编语言从入门到神通

    果壳疯狂编程:汇编语言从入门到神通
    (别太入戏)


    11(2.4) 8086CPU给出物理地址的方法

    2.4 8086CPU给出物理地址的方法

    8086CPU有20位地址总线,最多可以传送20位地址,然而8086CPU内部结构是16位的,一次性只能传送16位的地址,那么怎么解决20位地址(总线)与16位地址(内部结构)不一致的问题呢?


    8086CPU采用一种在内部用2个16位地址合成的方法来形成一个20位的物理地址。

    当8086CPU要读写内存时,怎样在CPU内部形成物理地址的呢?

    1:CPU中的相关部件提供2个16位的地址,一个称为段地址,另一个称为偏移地址。
    2:段地址和偏移地址通过内部总线送入一个称为地址加法器的部件。
    3:地址加法器将这两个16位地址合成为1个20位的物理地址。


    地址加法器采用“段地址×16+偏移地址”的方法合成物理地址。即:段地址×16+偏移地址=物理地址

    “段地址×16”可以理解为段地址的16倍(段地址的十进制数*16)。以下这个说法更好理解。

    我们把16转化为十六进制10,然后计算一下。假设段地址=2A7,2A7×10=2A70,计算结果2A70相对于段地址2A7左移了一位,所以,段地址×16可以理解为:段地址左移一位。

    请问同学们:“如果段地址=A100,偏移地址=42B,那么,物理地址=?”

    聪明如你,一定知道这个答案:“段地址×16=段地址左移一位,即A100左移一位=A1000,A1000+42B=A142B,所以,物理地址是:A142B”

    8086CPU要访问地址为123C8H的内存单元,此时,地址加法器的工作过程如下图所示(图中数据皆为十六进制表示):

    果壳疯狂编程:汇编语言从入门到神通

    果壳疯狂编程:汇编语言从入门到神通


    段地址×16又叫基础地址,所以,物理地址=基础地址+偏移地址。(旁白:有没有感觉到,CE中的基址和偏移的概念慢慢浮出了水面?)

    观察下面的地址,你有什么发现?

    果壳疯狂编程:汇编语言从入门到神通

    果壳疯狂编程:汇编语言从入门到神通


    结论:CPU可以用不同的段地址和偏移地址形成同一个物理地址。
    12(2.5) CS和IP

    2.5 CS和IP

    8086CPU在访问内存时,要由相关部件提供内存单元的段地址和偏移地址,然后送入地址加法器合成物理地址,那么,是什么部件提供段地址呢?是段寄存器提供段地址!8086CPU有4个段寄存器:CS、DS、SS、ES,本章先讲解CS。

    CS和IP是8086CPU中2个最为关键的寄存器,它们指示了CPU当前要读取指令的地址,我们看一下CE,可以看出在游戏中,什么是CPU要读取的指令地址,见下图:

    果壳疯狂编程:汇编语言从入门到神通

    果壳疯狂编程:汇编语言从入门到神通


    图中左边所示的代码地址就是游戏中CPU要读取指令的地址。CS叫代码段寄存器,IP叫指令指针寄存器。

    在8086CPU机中,任意时刻,设CS的值为M,设IP的值为N,8086CPU将从内存“M×16+N”单元开始,向下读取每一条指令并执行。


    也可以这样描述:在8086CPU机中,任意时刻,CPU将CS和IP(CS:IP)配合指向的内存单元里的信息当作指令执行。

    下列一组图展示了8086CPU读取、执行指令的工作原理(图中数字皆为十六进制):

    果壳疯狂编程:汇编语言从入门到神通

    果壳疯狂编程:汇编语言从入门到神通

    上图为读取、执行指令前的初始状态,CS=2000H,IP=0000H。

    果壳疯狂编程:汇编语言从入门到神通

    果壳疯狂编程:汇编语言从入门到神通

    上图CS、IP中的数值送入地址加法器,地址加法器完成:物理地址=段地址×16+偏移地址。

    果壳疯狂编程:汇编语言从入门到神通

    果壳疯狂编程:汇编语言从入门到神通

    上图地址加法器将物理地址送入“输入输出控制电路”。

    果壳疯狂编程:汇编语言从入门到神通

    果壳疯狂编程:汇编语言从入门到神通

    上图输入输出控制电路将物理地址20000H送上地址总线。

    果壳疯狂编程:汇编语言从入门到神通

    果壳疯狂编程:汇编语言从入门到神通

    上图从内存20000H单元开始,将存放其中(3个字节)的机器指令 B8 23 01 通过数据总线送入CPU。

    果壳疯狂编程:汇编语言从入门到神通

    果壳疯狂编程:汇编语言从入门到神通

    上图输入输出控制电路将机器指令 B8 23 01 送入指令缓冲器。

    果壳疯狂编程:汇编语言从入门到神通

    果壳疯狂编程:汇编语言从入门到神通

    上图读取一条指令后,IP中的值自动增加,以使CPU可以读取下一条指令。因当前读入的指令 B8 23 01长度为3个字节,

    所以IP中的值加3,此时,CS:IP指向内存单元2000:0003。

    果壳疯狂编程:汇编语言从入门到神通

    果壳疯狂编程:汇编语言从入门到神通

    上图执行控制器执行指令 B8 23 01(mov ax,0123h)。

    果壳疯狂编程:汇编语言从入门到神通

    果壳疯狂编程:汇编语言从入门到神通

    上图指令执行后,AX中的数值为0123H。那么,接下来就是读取、执行下一条指令 BB 03 00(mov bx,0003h)了。

    CS和IP的重要性在于它们的数值提供了CPU要执行指令的地址。

    在1.3节中,我们说过,在内存中指令和数据没有任何区别,都是二进制信息,CPU在工作的时候,把有的信息看作指令,把有的信息看作数据,那么,CPU在什么时候把它看作指令?在什么时候把它看作数据呢?现在我们可以更有深度的回答这个问题了。

    只要内存单元(二进制信息)被CS:IP指向,这些内存单元就会被CPU看作指令执行(如mov ax,bx),否则它依然代表着数据本身(如89D8H)。
    13(2.6) 修改CS和IP的指令

    2.6 修改CS和IP的指令

    Mov被称为传送指令,可以修改大部分寄存器的值。如:mov ax,123H,将ax中的值设为123H。同样地,我们也可以执行 mov bx,2a4H、mov cx,5f0H、mov dx,b29H 等等。


    但是,mov不能修改CS和IP这两个寄存器的值,因为8086CPU没有提供这样的功能。

    要修改CS和IP的值,可以用jmp指令,事实上,还有一些指令是可以修改CS和IP的,这些指令被统称为转移指令,这个在后面的章节会讲到,现在先学习这个最简单的转移指令:jmp。

    若想同时修改CS和IP的值,可用形如“jmp 段地址:偏移地址”的指令完成。如:jmp 2ae3:9,执行后:CS=2ae3H,IP=9H,CPU将从2ae39H(2ae3*10+9H)处读取指令。

    “jmp 段地址:偏移地址”指令的功能为:用指令中给出的第一个参数“段地址”修改CS,用第二个参数“偏移地址”修改IP。

    若想仅修改IP的值,可用形如“jmp 某一合法寄存器”的指令完成。如:jmp ax,执行前ax=437aH,CS=17f0H,IP=423cH,执行后,CS不变,IP=437aH。即将原IP值 "423cH"修改为ax里的值"437aH"。

    “jmp 某一合法寄存器”指令的功能为:用现有寄存器中的值来修改IP。为什么叫“某一合法寄存器”呢?因为并不是所有寄存器都可以修改IP!
    14(2.7) 代码段

    2.7 代码段

    在编程时,可以根据需要,将一组内存单元定义为一个段。段分3种类型:代码段、数据段、栈段。

    对于8086PC机,我们可以将长度为N(N≤64KB)的一组代码(机器指令和汇编指令),存放在一组地址连续、起始地址为16的倍数的内存单元中,我们将这一组内存单元定义为代码段。

    果壳疯狂编程:汇编语言从入门到神通

    果壳疯狂编程:汇编语言从入门到神通


    这段长度为10字节的指令,存放于123B0H~123B9H的一组内存单元中,我们就可以认为这是一个代码段。若要让CPU执行这些指令,必须要将CS:IP指向代码段中第一条指令的首地址(123B0H)。


    15第三章:寄存器(内存访问)

    第3章:寄存器(内存访问)

    引言

    在电影《骇客帝国》中,男主角发现,看似正常的现实世界实际上似乎被某种程序的力量控制着,人类原来的现实世界已不复存在,眼前的一切不过是人工智能精细设计出来的为获得人类生物电能,从而欺骗人类大脑的虚拟网格世界(请参考“缸中之脑”的思想实验)。在影片中有这样一个桥段:男主角等人置身于一个由无数个小房间组成的矩阵中,他们在各房间走道中不断穿梭,希望能寻找到一把神秘钥匙,打开一扇指定的门。这一行为在计算机内存中,叫做内存访问,即通过地址+偏移的方式来访问某一个内存单元,并实现把数据压入栈空间或者从栈中取出(入栈、出栈的操作)。而男主角等人要寻找的这把特殊的key, 在汇编术语中称之为:“SS:SP”。让我们带着这些抽象的概念和电影动人的画面感,进入第3章的旅程!

    果壳疯狂编程:汇编语言从入门到神通

    果壳疯狂编程:汇编语言从入门到神通

    电影《骇客帝国》

    16(3.1) 内存中字的存储

    3.1 内存中字的存储

    CPU中,一个字在16位寄存器存储时,高位字节存放在高8位寄存器中,低位字节存放在低8位寄存器中。  字在内存中存储时,由于内存单元是字节单元(一个单元存放一个字节),则一个字要用两个地址连续的内存单元来存放,这个字的低位字节存放在低地址单元中,高位字节存放在高地址单元中。如:字4E20H在内存中的存放情况如下图所示:

    果壳疯狂编程:汇编语言从入门到神通

    果壳疯狂编程:汇编语言从入门到神通


    字节单元:一个内存单元存放一个字节(8位)。

    字单元:存放一个字型数据(16位)的内存单元,由两个地址连续的内存单元组成。
    17(3.2) DS和[address]

    3.2 DS和[address]

    CPU要读写一个内存单元的时候,必须先给出这个内存单元的地址,在8086CPU中,内存地址由段地址和偏移地址组成,其中,段地址存放在段寄存器DS中,DS称作数据段寄存器(是否还记得8086CPU有4个段寄存器:CS、DS、SS、ES,上一章已讲过CS,本章讲DS)。

    如果我们要将内存地址为14A70的内存单元中的数据送入AL中,可以用如下的程序段进行:

    果壳疯狂编程:汇编语言从入门到神通

    果壳疯狂编程:汇编语言从入门到神通

    段地址(1000)*10+偏移地址(4A70)=内存单元(14A70)

    [address]表示一个内存单元,上图中的内存单元[address]中存放着偏移地址 [4A70]。  内存单元[address],我们可以知道它的地址是多少,但它的数值是多少,我们无从知晓。
    18(3.3) Mov指令

    3.3 Mov指令

    传送指令mov可以完成以下4种传送:

    1:将数据直接送入寄存器。如:mov ax,1230H

    2:将一个寄存器中的数据送入另一个寄存器。如:mov ax,bx

    3:将一个内存单元中的数据送入一个寄存器。如:mov ax,[27b0]

    4:将一个寄存器中的数据送入内存单元中。如:mov [607c],bx

    注意:不能直接用数据送给段寄存器,如:mov ds,1230H 是错误的,必须要用一个寄存器来进行中转。比如:

    Mov bx,1230H Mov ds,bx

    这样就行了。至于为什么8086CPU不支持将数据直接送入段寄存器呢?这属于8086CPU硬件设计的问题,我们只需知道这一点即可。
    19(3.4) Add和Sub指令

    3.4 Add和Sub指令

    Mov指令有两个操作对象,Add和Sub同样有两个操作对象。  


    ADD含意:加法指令。
    ADD格式:Add 操作对象1,操作对象2。

    ADD功能:两数相加,并把结果保存到操作对象1中。  

    Add 具有以下几种形式:

    Add 寄存器,数据   如:add ax, 8
    Add 寄存器,寄存器   如:add ax, bx
    Add 寄存器,内存单元   如:add ax, [27a0]
    Add 内存单元,寄存器   如:add [46e9], bx   

    SUB含意:减法指令。
    SUB格式:Sub 操作对象1,操作对象2。
    SUB功能:两数相减,即从操作对象1减去操作对象2,并将其结果保存到操作对象1中。  

    Sub 具有以下几种形式:

    Sub 寄存器,数据   如:sub ax, 9
    Sub 寄存器,寄存器   如:sub ax, bx
    Sub 寄存器,内存单元   如:sub ax, [b027]
    Sub 内存单元,寄存器   如:sub [8601], bx   
    20(3.5) 数据段

    3.5 数据段

    前面讲过(参见2.7节),对于8086PC机,在编程时,可以根据需要,将一组内存单元定义为一个段。我们可以将一组长度为N(N≤64KB)、地址连续、起始地址为16的倍数的内存单元当作专门存储数据的内存空间,从而定义了一个数据段。

    比如:用123B0H~123B9H这段内存空间来存放数据,我们就可以认为123B0H~123B9H这段内存是一个数据段,长度为10个字节。  如何访问数据段中的数据呢?首先用DS存放数据段的段地址,然后用相关的指令访问数据段中的内存单元。  


    又比如,将123B0H~123B9H的内存单元定义为数据段,现在要累加这个数据段中的前3个单元 中的数据,代码如下:

    Mov ax, 123BH Mov ds, ax  将123BH送入ds中,作为数据段的段地址。
    Mov al, 0  用al存放累加结果,先把al中的数据清零。

    Add al, [0] 将ds数据段第一个单元(偏移地址为0)中的数值加到al中。

    Add al, [1] 将ds数据段第二个单元(偏移地址为1)中的数据加到al中。

    Add al, [2] 将ds数据段第三个单元(偏移地址为2)中的数据加到al中。  

    在1.5节中,我们说过,在内存中指令和数据没有任何区别,都是二进制信息,CPU在工作的时候,把有的信息看作指令,把有的信息看作数据,那么CPU在什么时候把它看作指令?在什么时候把它看作数据呢?在2.5节中我们回答了第一个问题“什么时候把它看作指令"。

    现在可以回答第二个问题了:什么时候把它看作数据?  答:只要把一段内存单元的段地址放到DS中,并用Mov、Add、Sub等访问内存单元时,CPU就会将这些内存单元看作数据来访问。   
    21(3.6) 修改游戏的两种方式

    3.6 修改游戏的两种方式(改数据和改指令)


    指令和数据虽然从表面上看其内存结构并无区别,见下图:

    果壳疯狂编程:汇编语言从入门到神通

    果壳疯狂编程:汇编语言从入门到神通

    就像西施和貂蝉一样都是古典大美女——想什么呐!


    但实际上,它们的差别可大着呢,见下图:

    果壳疯狂编程:汇编语言从入门到神通

    果壳疯狂编程:汇编语言从入门到神通

    然而她们的手法、口活、体力活以及各种生活技能也许大不相同哦(危险)

    以上是两者在实际意义上的区别(数据表示属性,指令表示行为)。

    然而当我们从哲学的角度或用辩证法思维来思考问题时却发现:它们既是有区别的,又是密切相关的。数据和指令(或者说属性和行为),此二者是一个矛盾体:既非我族类,互不相同,又唇齿相依,相互合作。举个例子来说明这种思想:

    某个游戏,初始有500份木材,建造一座房子消耗木材50份,如何实现木材数量(属性数据)的读写操作呢?

    需要如下指令(行为方法):

    Mov ax, [bx+270]
    Sub [bx+16a], ax

    内存单元[bx+270]为建造房子时消耗的木材数量,每次消耗50份木材。


    内存单元[bx+16a]为当前木材数量。

    假设木材初始数量为500,当我们建造一座房子(首套房)时,则CPU会从内存单元[bx+270]中读取数据50送入ax,然后内存单元[bx+16a]的数值减去ax,并把计算结果450(由500-50而得)写入该内存单元[bx+16a]中,则当前木材数量变为450

    这两条指令(MovSub)实现了木材数量(属性数据)的读写操作,致使游戏中的伐木造房功能得以实现。

    从上面这个简单的例子,可以看出数据与指令是怎样的关系。


    只有数据没有指令,那么数据就没法读写,只有指令没有数据,那么只能是空指令,没有实质意义——巧妇难为无米之炊。

    修改数据和指令就是修改游戏的两种方式。
    以前我们用《金山游侠》、《gamemaster》等修改器修改的都是数据,现在有了CE,CE除了可以修改数据,还可以修改指令,棒到家——可以修改西施和貂蝉的行为了!

    赶紧对她们下达“主人的指令”吧!

    —— 妲己:主人的命令是绝对的。

    果壳疯狂编程:汇编语言从入门到神通

    果壳疯狂编程:汇编语言从入门到神通
    妲己大师娘的非法制绘

    22(3.7) 栈

    3.7 栈

    栈是一种具有特殊的访问方式的存储空间,它的特殊性就在于:最后进入这个空间的数据最先出去。可以用一个盒子和3本书来描述栈的这种操作方式。

    一个开口的盒子就可以看成一个栈空间,现有3本名著:《金瓶梅》、《肉蒲团》、《剪灯新话》,把它们放到盒子中,操作过程如下图所示(注意轻拿轻放,都是中国古典名著):

    果壳疯狂编程:汇编语言从入门到神通

    果壳疯狂编程:汇编语言从入门到神通


    上图把书放进盒子,这种操作可以称之为“入栈”。


    现在的问题是:一次只允许取一本,我们如何将3本书从盒子中取出来呢?

    显然,必须从盒子的最上边取,这样取出的顺序就是:《剪灯新话》、《肉蒲团》、《金瓶梅》,和放入的顺序相反,见下图:

    果壳疯狂编程:汇编语言从入门到神通

    果壳疯狂编程:汇编语言从入门到神通


    上图把书从盒子中取出,这种操作可以称为“出栈”。

    如果说,上例中的盒子就是一个栈,那么对于栈有两个基本操作:入栈和出栈。入栈就是将一个新的元素放到栈顶,出栈就是从栈顶取出一个元素。栈顶的元素总是最后入栈,需要出栈时又最先被从栈中取出,栈的这种操作规则被称为 LIFO(last in first out:后进先出)。   

    果壳疯狂编程:汇编语言从入门到神通

    果壳疯狂编程:汇编语言从入门到神通
    入栈出栈,后进先出之遐想:商女不知亡国恨,隔江尤唱后庭花。

    小知识点:想必大家对《金瓶梅》和《肉蒲团》并不陌生,对《剪灯新话》却缺乏相当的了解,那么今天果壳大湿就给大家来讲讲《剪灯新话》这本文学著作。《剪灯新话》是明代瞿佑撰写的文言短篇小说,中国十大禁书之一,此书为中国历史上第一部禁毁小说。书中除描摹普罗男女的畸变离奇隐秘之事外,其人鬼相恋,“交合之事,一如人间”,亦成为扣脚大汉和金刚芭比津津乐道,奉之为宅男宅女圣经宝典之根由,并不断从中汲取文学养分。中国文化博大精深——外国人和外星人是看不懂的。
    23(3.8) CPU提供的栈机制

    3.8 CPU提供的栈机制

    现如今的CPU中都会有栈的设计。8086CPU提供的入栈和出栈指令,最基本的两个是:push(入栈)和pop(出栈)。

    Push ax 表示将寄存器ax中的数据送入栈中,pop ax 表示从栈顶取出数据送入ax


    8086CPU的入栈和出栈操作都是以字为单位进行的。

    下面两张图描述了push和pop指令的执行过程。

    果壳疯狂编程:汇编语言从入门到神通

    果壳疯狂编程:汇编语言从入门到神通



    果壳疯狂编程:汇编语言从入门到神通

    果壳疯狂编程:汇编语言从入门到神通
    pop ax时,将数据1122H从1000BH-1000AH地址处取出;pop bx时,将数据2266H从1000DH-1000CH地址处取出;pop cx时,将数据0123H从1000FH-1000EH地址处取出。


    上面两张图指令的执行过程,写成代码如下:

    Mov ax, 123H
    Push ax
    Mov bx, 2266H
    Push bx
    Mov cx, 1122H
    Push cx
    Pop ax
    Pop bx
    Pop cx


    注意:字型数据用两个内存单元存放,高地址单元存放高8位,低地址单元存放低8位。

    看了上面两张图后,现在提出两个问题。

    问题一(栈空间):我们将10000H~1000FH这段内存当作栈来使用,CPU是如何知道这段空间是栈?关于这个问题将在3.10节中进行解答(现在先把问题抛出来供大家思考)。

    问题二(栈顶单元):push ax等入栈指令执行时,要将寄存器中的数据放入当前栈顶单元的上方,成为新的栈顶元素;pop ax等指令执行时,要从栈顶单元取出数据送入寄存器中。


    显然,push、pop在执行的时候,CPU必须要知道哪个单元是栈顶单元,可是,如何知道?

    王子解答:8086CPU中,有这么两个寄存器——堆栈段寄存器SS和堆栈指针寄存器SP。栈顶的段地址存放在SS中,栈顶的偏移地址则存放在SP中。任意时刻,SS:SP将会指向栈顶元素。push和pop指令在执行时,CPU会自动从SS和SP中得到栈顶单元的地址。

    这也就是本章开篇引言中讲述的关于《骇客帝国》男主角等人在房间矩阵中苦苦寻找的key:SS:SP。有了key就能定位到栈顶单元。

    现在,我们可以完整地描述push和pop指令的功能实现机制了,例如push ax的执行,由以下两步骤完成:

    1. SP=SP-2,SS:SP指向当前栈顶前面的单元,以当前栈顶前面的单元为新的栈顶。

    2. 将ax中的数据送入SS:SP指向的内存单元处,SS:SP此时指向新栈顶。

    下图描述了push ax的执行过程。

    果壳疯狂编程:汇编语言从入门到神通

    果壳疯狂编程:汇编语言从入门到神通


    小马提问:如果10000H~1000FH这段空间为栈,初始状态栈是空的,此时,SS=1000H,SP=?

    王子答曰:此时SP需要SP=SP+2,则SP=0010H。具体分析见下图:

    果壳疯狂编程:汇编语言从入门到神通

    果壳疯狂编程:汇编语言从入门到神通


    将10000H~1000FH这段空间当作栈段,SS=1000H,栈空间大小为16字节,栈最底部的字单元地址为1000:000E,任意时刻,SS:SP指向栈顶,当栈中只有一个元素时,SS=1000H,SP=000EH


    若栈为空,相当于栈中唯一的元素出栈后,SP=SP+2,原来为000EH,加2后SP=0010H,所以,当栈为空的时候,SS=1000H,SP=0010H

    换一个角度看,任意时刻,SS:SP指向栈顶元素,当栈为空的时候栈中没有元素,也就不存在栈顶元素,所以SS:SP只能指向栈的最底部单元下面的单元,该单元的偏移地址为栈最底部的字单元的偏移地址+2(即SP+SP+2),栈最底部字单元的地址为1000:000E,所以,栈空时,SP=0010H

    接下来,我们描述pop指令的功能实现机制,例如:pop ax。Pop ax的执行过程和push ax刚好相反,由以下两步完成:
    1. 将SS:SP指向的内存单元处的数据送入ax中。
    2. SP=SP+2,SS:SP指向当前栈顶下面的单元,以当前栈顶下面的单元为新的栈顶。


    下图描述了pop ax的执行过程。

    果壳疯狂编程:汇编语言从入门到神通

    果壳疯狂编程:汇编语言从入门到神通


    注意:上图中,出栈后,SS:SP指向新的栈顶1000EH,pop操作前的栈顶元素,1000CH处的数据2266H依然存在,但是,它已不在栈中(不在栈中在哪里,以后再讲),当再次执行push等入栈指令后,SS:SP移至1000CH,并在里面写入新的数据,将它覆盖。

    小知识点:根据上述出入栈原理,我们可以得知,为什么当我们删除硬盘中的文件后,若尚未存入新文件,则仍有可能实现“硬盘数据恢复”,但一旦在原处覆盖存入新文件则旧文件必将永远丢失,好多种子万劫不复!出栈入栈与硬盘数据丢失,两者同理。此时此刻,想起那些被误删的种子和那段无法被遗忘的岁月,王子"湿"性大发,"性"甚至哉,不得不淫它曹操一“手":《观沧海》。  


    东临碣石以观沧海。

    水何澹澹,山岛竦峙

    树木丛生,百草丰茂。

    秋风萧瑟,洪波涌起。

    日月之行,若出其中。

    星汉灿烂,若出其里。

    幸甚至哉,歌以咏志!

    果壳疯狂编程:汇编语言从入门到神通

    果壳疯狂编程:汇编语言从入门到神通
    出栈入栈与硬盘数据(种子)丢失两者同理之遐思:日月之行若出其中,星汉灿烂若出其里

    24(3.9) push和pop指令的两种运用形式

    3.9 push和pop指令的两种运用形式

    PUSH和POP指令的格式有如下两种形式。  

    第一种形式(push 寄存器、POP 寄存器):
    "push 寄存器",入栈,将一个寄存器中的数据压入栈中。
    "POP 寄存器",出栈,用一个寄存器接收出栈的数据。  

    这一种形式,它们可以在栈和寄存器之间传送数据。 注意:上面的寄存器可以是段寄存器,比如,可以是:push ds、pop ds

    第二种形式(push 内存单元 、POP 内存单元):

    "push 内存单元",入栈,将一个内存单元中的字型数据入栈(注意,栈操作都是以字为单位)。
    "POP 内存单元",出栈,用一个内存字单元接收出栈的数据。

    示例:

    果壳疯狂编程:汇编语言从入门到神通

    果壳疯狂编程:汇编语言从入门到神通

    这一种形式,它们可以在栈和内存单元之间传送数据。  



    指令执行时,CPU要知道内存单元的地址,可以在push、pop指令中给出内存单元的偏移地址。段地址在指令执行时,CPU会自动从DS中取得段地址。   

    小结: Push和pop实质上是一种内存传送指令,与mov指令不同的是,push和pop指令访问的栈空间的地址不是在指令中给出的,而是由SS:SP指出的。同时,push和pop指令还需要改变SP中的数值。Mov指令只需一步操作,就是传送,而执行push、pop指令需要两步操作,执行push时,先改变SP,后向SS:SP处传送数据入栈;执行pop时,先读取SS:SP处的数据并将其送出栈,而后改变SP。
    25(3.10) 栈段

    3.10 栈段

    前面讲过(参见2.7节),对于8086PC机,在编程时,可以根据需要,将一组内存单元定义为一个段。我们可以将长度为N(N≤64KB)的一组地址连续、起始地址为16的倍数的内存单元,当作栈空间来用,从而定义了一个栈段。

    比如,我们将10010H~1001FH这段内存空间当作栈来用,以栈的方式进行访问,这段空间就可以认为是一个栈段,大小为16个字节。

    如何使得如push、pop等栈操作指令访问我们定义的栈段呢?那就是要将SS:SP指向我们定义的栈段。现在我们来回答3.8节中的第一个问题:CPU是如何知道这段空间是栈?

    王子潇洒答曰:只要这段内存单元被SS:SP指向,那么,CPU就会把这段空间当作栈来使用!


    26第四章:寻址方式

    第4章:寻址方式

    引言

    寻寻觅觅,冷冷清清,凄凄惨惨戚戚。乍暖还寒时候,最难将息。三杯两盏淡酒,怎敌他、晚来风急!雁过也,正伤心,却是旧时相识。

    满地黄花堆积,憔悴损,如今有谁堪摘?守着窗儿,独自怎生得黑!梧桐更兼细雨,到黄昏、点点滴滴。这次第,怎一个愁字了得!


    本章精彩内容介绍——

    1.李清照寻夫:Mov ax, [bx]

    2.其夫未归,如何用另一种更灵活的方式来玩转她:Mov ax, [bx+idata]

    3.那么清照妹妹身上的物理地址究竟在哪里?物理地址=基础地址+偏移地址。

    4.上下求索,尝试不同玩法,从[idata]一直到[bx+si+idata],无一死角,遍体酥麻……

    5.到黄昏、点点滴滴……这次第,怎一个""字了得!

    果壳疯狂编程:汇编语言从入门到神通

    果壳疯狂编程:汇编语言从入门到神通
    清照妹妹

    27(4.1) 内存单元和[bx]的描述

    4.1 内存单元和[bx]的描述

    要完整地描述一个内存单元,需要两种信息:

    1.内存单元的物理地址(物理地址=基础地址+偏移地址)
    2.内存单元的长度(字单元、字节单元)


    如:mov ax, [2a78] 表示将一个内存单元中的数据送入ax(16位寄存器)中,这个内存单元的长度为2字节(
    字单元),存放一个字,偏移地址为2a78H,段地址在DS中。

    如:mov al, [10f3] 表示将一个内存单元中的数据送入al(8位寄存器)中,这个内存单元的长度为1字节(字节单元),存放一个字节,偏移地址为10f3H,段地址在DS中。  


    一个内存单元,段地址默认在DS中,偏移地址可以直接给出,单元的长度可以由具体指令中的其它操作对象(比如上述的ax寄存器、al寄存器)指出。  

    上面两条指令中,内存单元[2a78]和[10f3],它们的偏移地址分别是2a78H和10f3H,第一条指令中,ax为16位寄存器,这意味着[2a78]是字单元;第二条指令,al为低8位寄存器,这意味着[10f3]是字节单元(8位相当于单字节,16位相当于双字节)。  

    再看这两条指令:
    Mov ax, [bx]  
    Mov al, [bx]  [bx]   

    同样也表示一个内存单元,它的偏移地址就是bx的数值。   
    28(4.2) [bx+idata]

    4.2 [bx+idata]

    请看下面三组指令:

    Mov ax, [127a]
    Mov ax, [30e0]
    Mov ax, [c950]


    指令中的127a、30e0、c950皆为常量,我们可以用idata来表示常量:mov ax, [idata]

    同理:Mov bx, 123aH、Mov bx, 8762H、Mov bx, 4c0eH 即可表示为:mov bx, idata

    假设我们用[idata]和[bx]的方式来指明一个内存单元: Mov bx, idataMov ax, [bx]

    则我们还可以用另一种更灵活的方式来玩转它:Mov ax, [bx+idata]


    偏移地址为bx中的数值加上idata

    我们看一下指令mov ax, [bx+200]的含义:将内存单元[bx+200]中的数据送入ax,长度为2个字节(字单元),偏移地址为bx中的数值加上200,段地址在DS中。  

    该指令也可以写成如下常用格式:

    Mov ax, [200+bx]mov ax, 200[bx]mov ax, [bx].200
    29(4.3) SI和DI

    4.3 SI和DI

    SI是源变址寄存器,DI是目的变址寄存器,它们的功能与BX相近,但它们不能分成两个8位寄存器。  


    以下指令把内存单元[13b2]中的数据送入ax

    Mov bx, 13b2H
    Mov ax, [bx]   

    同样地,可以: Mov si, 13b2HMov ax, [si]   

    也可以: Mov di, 13b2HMov ax, [di]   

    以下指令把内存单元[2a7c+123]中的数据送入ax

    mov bx, 2a7cH
    Mov ax, [bx+123]   

    同样地,可以: Mov si, 2a7cHMov ax, [si+123]   

    也可以: Mov di, 2a7cHMov ax, [di+123]

    由此可见,我们用[bx]的方式来指明一个内存单元,同样地我们也可以用[si]或[di]的方式来指明一个内存单元,它们的含义基本相同。


    而当我们用[bx+idata]的灵活方式来指明一个内存单元时,同样地我们亦可以用[si+idata]或[di+idata]的方式来指明一个内存单元,含义近似。
    30(4.4) [bx+si]和[bx+di]

    4.4 [bx+si]和[bx+di]

    本节我们学习比前面几节讲解的更为灵活的指明一个内存单元的方式:[bx+si]和[bx+di]  


    [bx+si]和[bx+di]的含义相似,我们以[bx+si]为例进行讲解。

    [bx+si]表示一个内存单元,它的偏移地址为bx的数值加上si的数值。  


    指令mov ax, [bx+si]的含义如下:

    将一个内存单元[bx+si]中的数据送入ax,这个内存单元的长度为2字节(字单元),存放一个字,偏移地址为bx的数值加上si的数值,段地址在DS中。  

    该指令也可以写成如下常用格式:mov ax, [bx][si]
    31(4.5) 寻址方式的含义

    4.5 寻址方式的含义

    前面我们学习了几种定位内存地址的方法,现在总结一下:  

    1.[idata] 用一个常量来表示地址,可用于直接定位一个内存单元。


    2.[bx] 用一个变量来表示内存地址,可在一个起始地址的基础上间接定位一个内存单元。

    3.[bx+idata] 用一个变量和常量表示地址,可在一个起始地址的基础上用变量+常量的方式间接定位一个内存单元。


    4.[bx+si] 用两个变量表示地址(间接定位)。


    5.[bx+si+idata] 用两个变量和一个常量表示地址(间接定位)。  


    可以看到,从[idata]一直到[bx+si+idata],我们可以用更加灵活的方式来定位一个内存单元的地址。以上这几种定位内存地址的方法就称作寻址方式。

    下一章我们将对寻址方式的问题进行深入浅出的探讨,敬请关注! 关注果壳不迷路,果壳带你上高速!

    果壳疯狂编程:汇编语言从入门到神通

    果壳疯狂编程:汇编语言从入门到神通
    我的机器女友,今晚一起上高速


    32第五章:数据处理的两个基本问题

    第5章:数据处理的两个基本问题

    引言  


    我们知道,计算机是进行数据处理、运算的机器,那么有两个基本的问题就包含在其中:  

    1. 处理的数据在什么地方?

    2. 要处理的数据有多长?  

    这两个问题,在机器指令中,必须给以明确或隐含的说明,否则计算机就无法工作。

    本章中,我们就要针对8086CPU对这两个基本问题进行讨论。
    33(5.1) bx、si、di和bp

    5.1 bx、si、di和bp

    Bx、si和di这3个寄存器我们已经学过了,现在总结以下两个要点,并学学bp。  

    1:在8086CPU中,只有这4个寄存器可以用在[…]中来进行内存单元的寻址。

    比如,下面的指令都是正确的:
    Mov ax, [bx]
    Mov ax, [bx+si]
    Mov ax, [bx+di]
    Mov ax, [bp]
    Mov ax, [bp+si]
    Mov ax, [bp+di]  

    而下面的指令则是错误的:

    Mov ax, [ax] Mov ax, [cx] Mov ax, [dx] Mov ax, [ds]   

    2:在[…]中,这4个寄存器可以单个出现,或只能以4种组合出现:
    组合一:bx和si  
    组合二:bx和di  
    组合三:bp和si  
    组合四:bp和di

    比如下面的指令都是正确的:
    Mov ax, [bx]
    Mov ax, [si]
    Mov ax, [di]
    Mov ax, [bp]
    Mov ax, [bx+si]
    Mov ax, [bx+di]
    Mov ax, [bp+si]
    Mov ax, [bp+di]
    Mov ax, [bx+si+idata]
    Mov ax, [bx+di+idata]
    Mov ax, [bp+si+idata]
    Mov ax, [bp+di+idata]  

    而下面的指令则是错误的:
    Mov ax, [bx+bp]
    Mov ax, [si+di]   

    我们可以在访问内存单元的指令中显式地给出内存单元的段地址所在的段寄存器。比如:
    Mov ax, SS:[bx] 段地址在SS中。
    Mov ax, DS:[bp] 段地址在DS中。
    Mov ax, CS:[bp] 段地址在CS中。
    Mov ax, ES:[bp] 段地址在ES中。  

    在上面的指令 mov ax, ES:[bp] 中,我们接触到了一个新的段寄存器ES,ES叫附加段寄存器,它的功能与DS基本相同。  

    如果在[…]中使用寄存器bp,而指令中没有显式地给出段地址,段地址就默认在SS中。比如,下面的指令:
    Mov ax, [bp+200]   内存单元[bp+200]的段地址就在SS中。  

    BP被称作基址指针寄存器,它可以作SP使用,除了BP可以作为间接寻址寄存器而SP不能外,其余功能基本相同。
      
    事实上,通用寄存器除了第2章引言中提及的ax、bx、cx、dx这4个外,从广义上讲还应包括sp、bp、si、di这4个16位寄存器,以及ah、al、bh、bl、ch、cl、dh、dl这8个8位寄存器,也就是说,通用寄存器实际上一共有以上这16个。
    34(5.2) 机器指令处理的数据在什么地方

    5.2 机器指令处理的数据在什么地方

    绝大部分机器指令都是进行数据处理的指令,处理大致可分为3类:读取、写入、运算


    在机器指令这一层来讲,并不关心数据的值是多少,而关心指令执行前一刻,它将要处理的数据所在的位置。

    指令在执行前,所要处理的数据可以在3个地方:CPU内部、内存、端口

    比如下图所列的指令:

    果壳疯狂编程:汇编语言从入门到神通

    果壳疯狂编程:汇编语言从入门到神通

    35(5.3) 汇编语言中的数据位置的表达

    5.3 汇编语言中的数据位置的表达

    在汇编语言中如何表达数据位置?汇编语言中用3个概念来表达数据的位置,即立即数、寄存器、地址(段地址和偏移地址)。  


    1. 立即数(idata) 对于直接包含在机器指令中的数据(执行前在CPU的指令缓冲器中),在汇编语言中称之为“立即数”。例如:
    Mov ax, 136aH  指令要处理的数据就是立即数136aH
    Add ax, 2000H  指令要处理的数据就是立即数2000H
    Sub ax, a2c7H  指令要处理的数据就是立即数a2c7H  

    2. 寄存器 指令要处理的数据在寄存器中,在汇编指令中给出相应的寄存器名。例如:
    Mov ax, bx 指令要处理的数据在bx寄存器中。
    Mov ds, ax  指令要处理的数据在ax寄存器中。
    Push bx  指令要处理的数据在bx寄存器中。
    Mov DS:[123a], bx  指令要处理的数据在bx寄存器中,段地址在DS中。  

    3. 段地址和偏移地址 指令要处理的数据在内存中,在汇编指令中可用[…]的格式给出偏移地址,段地址在某个段寄存器中。

    例如:
    Mov ax, [107a]
    Mov ax, [di]
    Mov ax, [bx+8]
    Mov ax, [bx+si]
    Mov ax, [bx+si+8]   

    指令要处理的数据偏移地址在[…]中,段地址默认在DS中。   

    再看这样的指令:
    Mov ax, [bp]
    Mov ax, [bp+8]
    Mov ax, [bp+si]
    Mov ax, [bp+si+8]

    指令要处理的数据偏移地址在[…]中,段地址默认在SS中。   

    存放段地址的寄存器也可以是显性地给出,比如以下指令:
    Mov ax, DS:[bp]  指令要处理的数据偏移地址在[…]中,段地址在DS中。
    Mov ax, ES:[bx]  指令要处理的数据偏移地址在[…]中,段地址在ES中。
    Mov ax, SS:[bx+si]  指令要处理的数据偏移地址在[…]中,段地址在SS中。
    Mov ax, CS:[bx+si+8]  指令要处理的数据偏移地址在[…]中,段地址在CS中。
    36(5.4) 寻址方式(总结)

    5.4 寻址方式(总结)

    本节对寻址方式进行一下总结,见下表。  看表前先看以下说明:


    1.表中EA表示偏移地址,SA表示段地址。

    2.表中寄存器加上一个小括号,表示这个寄存器中的数值,比如:

    EA=(bx) (bx)就表示bx中的数值
    SA=(ds) (ds)就表示ds中的数值

    果壳疯狂编程:汇编语言从入门到神通

    果壳疯狂编程:汇编语言从入门到神通

    37(5.5) 指令要处理的数据有多长

    5.5 指令要处理的数据有多长

    8086CPU的指令,可以处理两种尺寸的数据:byte和word。所以在机器指令中要指明,指令进行的是字操作还是字节操作,对于这个问题,汇编语言中用以下方式处理。  


    1. 通过寄存器名指明要处理的数据的尺寸。

    例如,下面的指令中,寄存器指明了指令进行的是字操作,因为这些寄存器都是16位的。
    Mov ax, 123H Mov bx, DS:[210a]
    Add ax, 1000H
    Sub bx, 2ffH  

    下面的指令中,寄存器指明了指令进行的是字节操作,因为这些寄存器都是8位的。
    Mov al, 12H
    Mov bl, DS:[210a]
    Add al, 10H
    Sub bl, 2fH   

    2. 在没有寄存器名存在的情况下,用操作符word prt或byte prt指明内存单元的长度,前者为字单元,后者为字节单元。

    例如,下面的指令中,用word ptr指明了指令访问的内存单元是一个字单元。
    Mov word ptr DS:[a017], 28H
    Add word ptr [bx], 78H  

    下面的指令中,用byte prt指明了指令访问的内存单元是一个字节单元。
    Mov byte ptr DS:[1a7], 1aH
    Add byte ptr [bx], 62H  

    在没有寄存器参与的内存单元访问指令中,用word ptr或byte ptr显性地指明所要访问的内存单元的长度是很有必要的,否则,CPU无法得知所要访问的单元是字单元还是字节单元,从而造成错误。   

    3. 其它方法:有些指令已经默认了所访问的内存单元是字单元还是字节单元,比如push [123a]和pop[123c]就不用指明访问的是字单元还是字节单元,因为push和pop指令只进行字操作。  
    38(5.6) mul指令

    5.6 mul指令

    Mul为乘法指令,使用mul做乘法的时候,注意以下两点:

    1. 两个相乘的数:这两个相乘的数,要么都是8位,要么都是16位,如果是8位,一个默认放在al中,另一个放在8位寄存器或内存字节单元中;如果是16位,一个默认放在ax中,另一个放在16位寄存器或内存字单元中。
    2.结果:如果是8位乘法,其结果默认存放在ax中;如果是16位乘法,其结果的高位默认存放在dx中,低位则存放在ax中。  


    Mul 指令格式如下:
    Mul 通用寄存器 Mul 内存单元

    内存单元可以用不同的寻址方式给出,比如:

    Mul byte ptr DS:[7102] 8位乘法。
    Mul word ptr [bx+si+8] 16位乘法。  


    例1:计算100×10。 100和10都小于255,可以做8位乘法
    代码如下:
    Mov al, 100
    Mov bl, 10 Mul bl
    结果:ax=al×bl=100×10=1000(3E8H)  

    例2:计算100×10000。 100小于255,可10000大于255,所以必须做16位乘法
    代码如下:
    Mov ax, 100
    Mov bx, 10000
    Mul bx
    结果:ax×bx=100×10000=1000000(F4240H)
    存放:Ax=4240H(低16位值);Dx=FH(高16位值)  
    39(5.7) div指令

    5.7 div指令

    Div是除法指令,使用div做除法的时候,应注意以下问题:

    1. 除数:有8位和16位两种,在一个寄存器或内存单元中。
    2. 被除数:如果除数为8位,被除数则为16位,则默认放在ax中;如果除数为16位,被除数则为32位,则在dx和ax中存放,其中dx存放高16位,ax存放低16位。
    3. 结果:如果除数为8位,则al存储结果的商,ah存储结果的余数;如果除数为16位,则ax存储结果的商,dx存储结果的余数。  

    Div 指令格式如下:

    Div 通用寄存器 Div 内存单元  

    内存单元可以用不同的寻址方式给出,比如: Div byte ptr DS:[21a5] 除数为8位的除法。
    Div word ptr [bx+si+8]   除数为16位的除法。  

    例1:计算100001÷100
    被除数100001为32位,转化成16进制为186a1H,低16位值86a1H放在ax中,高16位值1H放在Dx中,除数100转化为16进制64H后,放在一个16位寄存器中,代码如下:

    Mov dx, 1H Mov ax, 86a1H Mov bx, 64H Div bx
    结果:(dx×10000H+ax)÷bx=186a1H÷64H=3E8H余1
    存放:Ax=3e8H(1000);dx=1H

    例2:计算1001÷100
    被除数1001可用ax存放,除数100可用8位寄存器存放,代码如下:

    Mov ax, 1001 Mov bl, 100 Div bl
    结果:ax÷bl=1001÷100=10余1
    存放:Al=10;Ah=1

    果壳疯狂编程:汇编语言从入门到神通

    果壳疯狂编程:汇编语言从入门到神通
    (寄存器存放高低位数据之遐思)


    40第六章:转移指令和原理

    第6章:转移指令和原理

    引言

    可以修改IP,或同时修改CS和IP的指令统称为转移指令。概括地讲,转移指令就是可以控制CPU执行内存中某处代码的指令。  


    8086CPU的转移行为有以下几类:

    1. 同时修改CS和IP时,称为段间转移,比如:jmp 100:2a7。
    2. 只修改IP时,称为段内转移,比如:jmp ax。由于转移指令对IP的修改范围不同,段内转移又分为“短转移”和“近转移”。
    3. 段内短转移IP的修改范围为:-128~127。
    4. 段内近转移IP的修改范围为:-32768~32767。  

    8086CPU的转移指令分为以下几类:
    1. 无条件转移指令(比如:jmp)
    2. 条件转移指令
    3. 循环指令
    4. 过程
    5. 中断

    这些转移指令转移的前提条件可能不同,但转移的基本原理是相同的,我们在这一章主要通过深入学习无条件转移指令jmp来理解CPU执行转移指令的基本原理。   
    41(6.1) jmp指令

    6.1 jmp指令

    Jmp为无条件转移指令,可以只修改IP,也可以同时修改CS和IP。  


    Jmp指令要给出两种信息:
    1. 转移的目的地址。
    2. 转移的距离(段间转移、段内短转移、段内近转移)。  

    不同的给出目的地址的方法,和不同的转移位置,对应有不同格式的jmp指令,下面的几节内容中,我们以给出目的地址的不同方法为主线,讲解jmp指令的主要应用格式和CPU执行转移指令的基本原理。   
    42(6.2) 依据位移进行转移的jmp指令

    6.2 依据位移进行转移的jmp指令

    Jmp short 标号(转到标号处执行指令)。

    这种格式的jmp指令,实现的是段内短转移,它对IP的修改范围为:-128~127。也就是说,它向前转移时可以多越过128个字节,向后转移可以最多越过127个字节。


    Jmp指令中的“short”符号,说明指令进行的是短转移,jmp指令中的“标号”是代码段中的标号,指明了指令要转移的目的地,转移指令执行结束后,CS:IP应该指向标号处的指令。

    请看下面一段代码:
    Mov ax, 0
    Jmp short S
    Add ax, 1
    S:add ax, 2


    最下面那条指令中的S就是标号,jmp short S指令执行后,CS:IP指向S:add ax, 2,上面那条指令Add ax, 1已被跳过,没有被CPU执行。


    有没有觉得:有点像高级语言中的GOTO语句?

    在“jmp short 标号”指令所对应的机器码中,不包含转移的目的地址,而包含的是转移的位移,这个位移是编译器根据汇编指令中的“标号”计算出来的,具体的计算方法如下图所示:

    果壳疯狂编程:汇编语言从入门到神通

    果壳疯狂编程:汇编语言从入门到神通


    上图中,标号处的指令s0:inc bx的偏移地址为6,指令jmp s0后的第一个字节的偏移地址为3,位移量就是:6-3= 3。

    再来看,标号处的指令s:inc ax的偏移地址为0,指令jmp s后的第一个字节的偏移地址为9,则位移量就是0-9 =﹣9。

    “Jmp short 标号”的功能表示:IP=IP+8位位移。请看以下四个要点:

    1:8位位移=标号处的地址-jmp指令后的第一个字节的地址。
    2:short指明此处的位移为8位位移。
    3:8位位移的范围为﹣128~127,用补码表示(本教程不讲解补码,若你想了解,请看相关书籍)。
    4:8位位移由编译程序在编译时算出。


    还有一种和“jmp short 标号”功能相近的指令格式:“jmp near ptr 标号”,它实现的是段内近转移。

    “jmp near ptr 标号”的功能表示为:IP=IP+16位位移。

    1:16位位移=标号处的地址-jmp指令后的第一个字节的地址。
    2:near ptr指明此处的位移为16位位移,进行的是段内近转移。
    3:16位位移的范围为﹣32768~32767,用补码表示。
    4:16位位移由编译程序在编译时算出。


    43(6.3) 转移地址的jmp指令

    6.3 转移地址在指令中或寄存器中的jmp指令

    Jmp far ptr 标号”实现的是段间转移(又称为远转移),功能如下:

    CS=标号所在段的段地址,IP=标号在段中的偏移地址。 "Far ptr"指明了指令用标号的段地址和偏移地址修改CS和IP。 在“jmp far ptr 标号”指令所对应的机器码中,包含转移目的地的地址。  


    转移的目的地在寄存器中的jmp指令,指令格式为: Jmp 16位通用寄存器。功能:IP=16位通用寄存器。这种指令我们在前面的内容(参见2.6节)中已经讲过,这里就不再详述。   

    果壳疯狂编程:汇编语言从入门到神通

    果壳疯狂编程:汇编语言从入门到神通
    师娘大变身:爱的魔力转圈圈(转移地址jmp指令之海边遐想)

    44(6.4) 转移地址在内存中的jmp指令

    6.4 转移地址在内存中的jmp指令

    转移地址在内存中的jmp指令有两种格式:  

    1. “jmp word ptr 内存单元地址”(段内转移)。


    功能:从内存单元地址处开始存放着一个字,用做转移的目标偏移地址。内存单元地址可用寻址方式的任一格式给出,比如,下面的指令:  
    Mov ax, 123H Mov DS:[200], ax
    Jmp word ptr DS:[200]   
    执行后,IP=123H  

    又比如,下面的指令:
    Mov ax, 123H
    Mov [bx], ax
    Jmp word ptr [bx]   
    执行后,IP=123H

    2. “jmp dword ptr 内存单元地址”(段间转移)。


    功能:从内存单元地址处开始存放着两个字,高地址处的字为转移的目标段地址,低地址处的字为转移的目标偏移地址。  

    CS=内存单元地址+2
    IP=内存单元地址  

    内存单元地址可用寻址方式的任一格式给出。

    比如,下面的指令:
    Mov ax, 123H
    Mov DS:[200], ax
    Mov word ptr DS:[202], 100
    Jmp dword ptr DS:[200]   
    执行后,CS=100H,IP=123H,CS:IP指向100:123

    又比如,下面的指令:
    Mov ax, 123H
    Mov [bx], ax
    Mov word ptr [bx+2], 100
    Jmp dword ptr [bx]   
    执行后,CS=100H,IP=123H,CS:IP指向100:123

    在上面的指令中,我们接触到了一个新的符号“dword”,它表示什么意思呢?前面我们已学过,Byte表示字节,word表示字,dword则表示双字,谢谢。   
    45(6.5) CALL指令

    6.5 CALL指令

    Call和ret指令都是转移指令,它们都修改IP,或同时修改CS和IP,它们经常被共同用来实现子程序的设计。


    这两节,我们讲解call和ret指令的原理。  CPU执行call指令时,进行两步操作:
    1.将当前的IP(或CS和IP)压入栈中。

    2.转移。   


    Call指令不能实现短转移,除此之外,call指令实现转移的方法和jmp指令的原理相同。

    下面我们以给出转移目标地址的不同方法为主线,讲解call指令的主要应用格式。


    1. 依据位移进行转移的call指令。

    Call 标号(将当前的IP压入栈后,转到标号处执行指令)。
    指令执行时,它的功能相当于: Push IP Jmp near ptr 标号

    2. 转移地址在指令中的call指令。

    Call far ptr 标号(实现的是段间转移)。
    指令执行时,它的功能相当于: Push CS Push IP Jmp far ptr 标号

    3. 转移地址在寄存器中的call指令。

    指令格式:call 16位通用寄存器。
    指令执行时,它的功能相当于: Push IP Jmp 16位通用寄存器
       
    4. 转移地址在内存中的call指令。 这种call指令有两种格式:


    格式1:call word ptr 内存单元地址。
    指令执行时,它的功能相当于: Push IP Jmp word ptr 内存单元地址  

    格式2:call dword ptr 内存单元地址。
    指令执行时,它的功能相当于: Push CS Push IP Jmp dword ptr 内存单元地址   
    46(6.6) 子程序

    6.6 子程序

    Ret指令用栈中的数据修改IP的数值,从而实现近转移。Ret指令执行时,进行下面两步操作:

    1. IP =(SS×16+SP)

    2. SP=SP+2  


    指令执行时,它的功能相当于:pop IP 学习了call和ret指令,现在来看一下,如何将它们配合使用来实现子程序的机制。请看下面的一段代码:  
    Mov ax, 1 Mov cx, 3
    Call s
    Mov bx, ax
    Mov ax, 4c00H
    Int 21H
    S:add ax,ax
    Loop s
    Ret  

    我们来分析一下CPU执行这一段代码的过程。

    1. CPU执行第一、第二条指令后,CS:IP指向call s。
    2. CPU将call s指令的机器码读入,IP指向call s后的指令mov bx, ax。

    3. 执行call s指令,将当前IP值(指令mov bx, ax的偏移地址)压入栈中,并将IP的值改变为标号s处的偏移地址。

    4. CPU从标号s处执行指令,直至loop指令循环完毕(loop将会循环3次,因为cx=3,cx寄存器用于存放loop循环次数)。

    5. CPU指向并执行ret指令,从栈中弹出一个数据(即先前压入栈中的指令mov bx, ax的偏移地址)送入IP,则CS:IP指向指令mov bx, ax。

    6. CPU执行指令mov bx, ax。此时bx中存放了ax累加其自身3次的值:3

    7.CPU继续向下执行,直到执行int 21H后,程序结束。


    上面第3、第5项是重点,它揭示了子程序执行完之后,如何让CPU接着call指令向下执行。什么是子程序?具有一定功能的程序段,我们称之为子程序。

    比如,上面的那一段代码,s:add ax, bx到ret那3条指令就是一个简单的子程序,它的功能是把ax中的数值累加3次,用循环指令loop实现累加(累加次数由cx中的值决定)。  

    在需要的时候,我们用call指令转去执行它,执行完子程序后,要让CPU接着call指令向下执行,则需要用到ret指令,call指令转去执行子程序之前,call指令后面的指令的地址将被存储在栈中,在子程序的后面使用ret指令,用栈中的数据设置IP的值,从而转到call指令后面的代码处继续执行。

    另外,在上面的示例代码中,int 21H和MOV AX,4C00H这两条指令又代表什么意思呢?请看下面的图文讲解。

    其实很简单,INT 21H 调用了系统中断。  MOV AX,4C00H,意即:AH=4CH。  那么INT 21H和AH=4CH两者搭配的作用就是:调用 INT 21H 的 4CH 号中断,该中断就是安全退出程序。

    换句话说,MOV AX,4C00H、INT 21H 这两句可以等价于以下两句: MOV AH,4CH INT 21H   接下来看看下面的图解,帮助自己理解所谓“中断例程”,实在理解不了也没关系,就当满足知识拓展之需要。   

    果壳疯狂编程:汇编语言从入门到神通

    果壳疯狂编程:汇编语言从入门到神通


    果壳疯狂编程:汇编语言从入门到神通

    果壳疯狂编程:汇编语言从入门到神通



    47第七章:标志寄存器

    第7章:标志寄存器

    引言

    CPU内部的寄存器中,有一种特殊的寄存器,它具有以下3种作用:
    1. 用来存储相关指令的某些执行结果。
    2. 用来为CPU执行相关指令提供行为依据。
    3. 用来控制CPU的相关工作方式。  这种特殊的寄存器在8086CPU中,被称为标志寄存器


    8086CPU的寄存器,在前面已经学过13个了,现在学习最后一个寄存器:FR-标志寄存器。  

    FR与其它寄存器不一样,其它寄存器是用来存放数据的,都是整个寄存器具有一个含义,而FR寄存器是按位起作用的,也就是说它的每一位都有专门的含义,记录特定的信息。  8086CPU的FR寄存器的结构如下图所示:


    果壳疯狂编程:汇编语言从入门到神通

    果壳疯狂编程:汇编语言从入门到神通


    FR的第1、3、5、12、13、14、15位是空白位,在8086CPU中没有使用,不具有任何意义,而第0、2、4、6、7、8、9、10、11位都具有特殊的含义。
    48(7.1) CF标志

    7.1 CF标志

    FR的第0位是CF,进位标志位。一般情况下,在进行无符号数运算的时候,它记录了运算结果的最高有效位向更高位的进位值,或从更高位的借位值。  


    对于位数为N的无符号数来说,其对应的二进制信息的最高位,即第N-1位,就是它的最高有效位,而假想存在的第N位,就是相对于最高有效位的更高位,如下图所示:

    果壳疯狂编程:汇编语言从入门到神通

    果壳疯狂编程:汇编语言从入门到神通


    我们知道,当两个数据相加的时候,有可能产生从最高有效位向更高位的进位,比如,两个位数据:98H+98H,将产生进位,这个进位值就用CF标志位来保存。  
    比如,下面的指令:

    Mov al, 98H Add al, al   

    执行后,计算结果为130H,al=30H,CF=1,CF记录了从最高有效位向更高位的进位值。

    两数相加,如果产生了进位,则CF=1,如果没有产生进位,则CF=0

    当两个数据做减法的时候,有可能向更高位借位,比如,两个8位数据:97H-98H,将产生借位,借位后相当于计算197H-98H,而FR的CF标志位也可以用来记录这个借位值。比如,下面的指令:

    Mov al, 97H Sub al, 98H  

    执行后,计算结果为197H-98H=ffH,al=ffH,CF=1,CF记录了向更高位的借位值。

    两数相减,如果产生借位,则CF=1,如果没有产生借位,则CF=0。   
    49(7.2) ADC指令

    7.2 ADC指令

    Adc是带进位加法指令,它利用了CF位上记录的进位值。

    格式:adc 操作对象1,操作对象2 功能:操作对象1=操作对象1+操作对象2+CF
       
    例1:

    mov ax, 1 Add ax, ax   结果:ax=2,没有产生进位值,CF=0
    Adc ax, 3   结果:ax=ax+3+CF=2+3+0=5
      
    例2:

    mov al, 98H Add al, al   结果=130H,产生了进位值,CF=1,al=30H
    Adc al, 3   结果:al=al+3+CF=30H+3+1=34H

    可以看出,adc指令比add指令多加了一个CF位的值,为什么要加上CF的值呢?CPU为什么要提供这样一条指令呢? 我们来看一下两个数据:0198H0183H是如何相加的,见下图:

    果壳疯狂编程:汇编语言从入门到神通

    果壳疯狂编程:汇编语言从入门到神通


    可以看出,加法可以分两步来进行:

    1.低位相加(98+83)


    2.高位相加再加上低位相加产生的进位值(1+1+1)

    看来CPU提供adc指令的目的,就是来进行加法的第二步运算的。用adc指令和add指令相配合就可以对更大的数据进行加法运算。   

    例3:计算1EF000H+201000H,结果放在ax(高16位)和bx(低16位)中。

    因为两个数据的位数都大于16位,用add指令无法进行计算,我们将计算分两步进行,先将低16位(F000H和1000H)相加,然后将高16位(1EH和20H)和进位值相加,代码如下:  

    Mov ax, 1eH Mov bx, f000H Add bx, 1000H(低16位相加,结果:f000H+1000H=10000H 产生了进位值,CF=1,bx=0)
    Adc ax, 20H(高16位相加,结果:ax=ax+20H+CF=1eH+20H+1=3fH)  
    最终结果:ax=3fH,bx=0,1EF000H+201000H=3f0000H。

    Adc指令执行后,也可能产生进位值,所以也会对CF位进行设置,由于有这样的功能,我们就可以对任意大的数据进行加法运算。   

    例4:计算1EF0001000H+2010001EF0H,结果放在ax(最高16位),bx(次高16位),cx(低16位)中。  

    计算分3步进行:


    1.先将低16位(1000H和1EF0H)相加,完成后,CF记录本次相加的进位值。

    2.再将次高16位(F000H和1000H)和CF(来自低16位的进位值)相加,完成后,CF记录本次相加的进位值。

    3.最后最高16位(1EH和20H)和CF(来自次高16位的进位值)相加,完成后,CF记录本次相加的进位值。  


    代码如下:


    Mov ax, 1eH
    Mov bx, f000H
    Mov cx, 1000H
    Add cx, 1ef0H

    (低16位相加,结果:1000H+1ef0H=2ef0H,没有产生进位值,CF=0,cx=2ef0H)

    Adc bx, 1000H

    (次高16位相加,结果:f000H+1000H+0=10000H,产生进位值,CF=1,bx=0)

    Adc ax, 20H

    (最高16位相加,结果:ax=ax+20H+CF=1eH+20H+1=3fH,没有产生进位,CF=0)  

    最终结果:ax=3fH,bx=0,cx=2ef0H,1EF0001000H+2010001EF0H=3F00002EF0H   
    50(7.3) SBB指令

    7.3 SBB指令

    Sbb是带借位减法指令,它利用了CF位上记录的借位值。



    格式:sbb 操作对象1,操作对象2


    功能:操作对象1=操作对象1-操作对象2-CF  Sbb指令执行后,将对CF进行设置,利用sbb指令和sub指令配合使用可以对任意大的数据进行减法运算。   

    例1:计算3E1000H-202000H,结果放在ax(高16位),bx(低16位)


    计算分两步进行,先将低16位(1000H和2000H)相减,然后将高16位(3EH和20H)和借位值相减,代码如下:  

    Mov bx, 1000H
    Mov ax, 3eH
    Sub bx, 2000H

    (低16位相减,结果:1000H-2000H=11000H-2000H=f000H,产生了借位值,CF=1,bx=f000H)
    Sbb ax, 20H

    (高16位相减,结果:ax=ax-20H-CF=3eH-20H-1=1dH,没有产生借位值,CF=0)  

    最终结果:ax=1dH,bx=f000H,3E1000H-202000H=1DF000H  

    例2:计算6E4F0031C0H-1FA2002700H。结果放在ax(最高16位),bx(次高16位),cx(低16位)

    计算分3步进行:


    1. 先将低16位(31C0H和2700H)相减,完成后,CF记录本次相减的借位值。

    2. 再将次高16位(4F00H和A200H)和CF(来自低16位的借位值)相减,完成后,CF记录本次相减的借位值。

    3. 最后将最高16位(6EH和1FH)和CF(来自次高16位的借位值)相减,完成后,CF记录本次相减的借位值。  


    代码如下:


    Mov ax, 6eH
    Mov bx, 4f00H
    Mov cx, 31c0H
    Sub cx, 2700H

    (低16位相减,结果:31c0H-2700H=ac0H,没有产生借位值,CF=0,cx=ac0H)

    Sbb bx, a200H

    (次高16位相减,结果:14f00H-a200H=ad00H,产生借位值,CF=1,bx=ad00H)

    Sbb ax, 1fH

    (最高16位相减,结果:ax=ax-1fH-CF=6eH-1fH-1=4eH,没有产生借位值,CF=0)  

    最终结果:ax=4eH,bx=ad00H,cx=ac0H,6E4F0031C0H-1FA2002700H=4EAD000AC0H
    51(7.4) ZF标志

    7.4 ZF标志

    FR的第6位是ZF,零标志位。它记录相关指令执行后,其结果是否为0


    如果(真),结果为0,那么ZF=1;如果(假),结果非0,那么ZF=0

    对于ZF的值,我们可以这样来看:在计算机中1表示逻辑真,表示肯定,所以当结果为0的时候,ZF=1;在计算机中0表示逻辑假,表示否定,所以当结果不为0的时候,ZF=0   

    且看下面的指令:

    Mov ax, 5
    Sub ax, ax

    执行后,结果为0,表示真,则ZF=1

    Mov ax, 5
    Sub ax, 1

    执行后,结果不为0,表示假,则ZF=0
    52(7.5) cmp指令

    7.5 cmp指令

    Cmp是比较指令,它的功能相当于sub指令,只是不保存结果。


    Cmp指令执行后,将对标志寄存器产生影响,其它相关指令通过识别这些被影响的标志位来得知比较结果。

    指令格式:cmp 操作对象1,操作对象2

    功能:计算操作对象1-操作对象2,但并不保存结果,仅仅根据计算结果对标志寄存器的标志位进行设置。  

    Cmp指令执行后,依据标志位的值就可以看出比较结果。  

    比如:cmp ax, bx  执行后,如果ZF=1,说明ax=bx,因为ax-bx=0,那么ax必定等于bx

    如果ZF=0,说明ax≠bx,因为ax-bx≠0,那么ax与bx必定不相等。
    如果CF=1,说明ax<bx,因为ax-bx产生了借位,那么ax必定小于bx。
    如果CF=0,说明ax≥bx,因为ax-bx没有产生借位,那么ax必定大于或等于bx。
    如果CF=0并且ZF=0,说明ax>bx,因为ax-bx没有产生借位,并且ax-bx≠0,那么ax必定大于bx。
    如果CF=1或ZF=1,说明ax≤bx,因为ax-bx产生了借位,又或者ax-bx=0,那么ax必定小于或等于Bx。
    53(7.6) 检测比较结果的条件转移指令

    7.6 检测比较结果的条件转移指令

    转移指的是它能够修改IP,而条件指的是它可以根据某种条件,决定是否修改IP,所有条件转移指令都是短转移,转移的位移范围为﹣128~127。   


    大多数条件转移指令都检测标志寄存器的相关标志位,根据检测的结果来决定是否修改IP,它们所检测的标志位都是被cmp指令影响的那些表示比较结果的标志位。   

    下面是常用的根据无符号数的比较结果进行转移的条件转移指令:

    果壳疯狂编程:汇编语言从入门到神通

    果壳疯狂编程:汇编语言从入门到神通


    以上这些条件转移指令是根据检测相关的标志位来决定是否转移,比如:je是检测ZF的值来决定是否转移,如果ZF=1则转移,至于根据逻辑含义来决定是否转移,则需要与cmp指令配合使用,这个在下一节会讲到。
    54(7.7) cmp与条件转移指令配合使用

    7.7 cmp与条件转移指令配合使用

    上一节介绍的条件转移指令,所检测的标志位都是cmp指令进行无符号数比较的时候,记录比较结果的标志位,比如,je检测ZF位,当ZF=1时转移,如果在je前面使用了cmp指令,那么je对ZF的检测,实际上是间接地检测cmp的比较结果是否为两数相等。  


    请看下面一段代码:
    Cmp ax, bx
    Je s
    Add ax, bx
    Jmp short ok
    S:add ax, ax
    ok: …   

    上面的代码执行时,如果ax=bx,则cmp ax, bx使ZF=1,而je检测ZF是否为1,如果为1,则转移到标号S处执行指令add ax, ax,我们也可以这样说,cmp比较ax, bx后所得到的相等的结果使得je指令进行转移,这种说法很好地体现了je指令的逻辑含义,即“相等则转移“。  

    “相等则转移”这种逻辑含义是通过和cmp指令配合使用来体现的,我们用cmp指令与条件转移指令配合使用的时候,不必再考虑cmp指令对相关标志位的影响和je等指令对相关标志位的检测,因为相关的标志位只是为 Cmp和je等指令传递比较结果,我们可以直接考虑cmp与je等指令配合使用时,表现出来的逻辑含义。   

    请看下面的指令:  

    Cmp byte ptr [bx], 8(和8比较)
    Je 标号(如果等于则转移)

    Cmp byte ptr [bx], 8(和8比较)
    Jne 标号(如果不等于则转移)

    Cmp byte ptr [bx], 8(和8比较)
    Jnb 标号(如果不低于则转移)

    Cmp byte ptr [bx], 8(和8比较)
    Ja 标号(如果高于则转移)

    Cmp byte ptr [bx], 8(和8比较)
    Jna 标号(如果不高于则转移)  

    Cmp byte ptr [bx], 8(和8比较)
    Jb 标号(如果低于则转移) 上面的指令,用[bx]中的数值和8比较,“如果怎么怎么样则转移”,我们在修改游戏时,可以根据这些逻辑含义,选择合适的条件转移指令。

    妲己师娘:Jb标号,和你的“8”相比较,如果怎么怎么样则转移体位,随着爱的魔力转圈圈!

    果壳疯狂编程:汇编语言从入门到神通

    果壳疯狂编程:汇编语言从入门到神通
    cmp与条件转移指令之迷思:主人的命令是绝对的,爱的魔力转不停,好疯狂!

    55(7.8) 其它标志位

    7.8 其它标志位

    标志寄存器的大部分标志位,我们都不必深入地去学习,因为这和修改游戏没有多大关系,我们只需简单了解一下即可,FR一共有9个标志位,前面已学习了ZF和CF这两个标志位,现在讲讲余下的7个标志位。  


    PF:奇偶标志位
    它记录相关指令执行后,其结果的所有二进制位中1的个数是否为偶数,如果(真),1的个数为偶数,PF=1,如果(假),1的个数为奇数,PF=0

    比如,某些指令执行后,其结果二进制值为01001011,有4(偶数)个1,则PF=1;某些指令执行后,其结果二进制值为00001011,有3(奇数)个1,则PF=0   

    SF:符号标志位
    它记录相关指令执行后,其结果是否为负,如果(真),结果为负,SF=1,如果(假),结果非负,SF=0

    OF:溢出标志位
    一般情况下,OF记录了有符号数运算的结果是否发生了溢出,如果(真),发生了溢出,OF=1,如果(假),没有发生溢出,0F=0  

    什么是溢出?在进行有符号数运算的时候,如结果超过了机器所能表示的范围称为溢出。

    那么,机器所能表示的范围是多少呢?对于8位的有符号数据,机器所能表示的范围就是:-128~127;对于16位的有符号数据,机器所能表示的范围就是:﹣32768~32767。如果运算结果超出了机器所能表达的范围,将会产生溢出。  

    比如,指令: Mov al, 98 Add al, 99   

    执行后,al=98+99=197

    197超出了机器所能表示的8位有符号数的范围:﹣128~127,所以产生了溢出。   

    DF:方向标志位
    在串处理指令中,控制每次操作后SI、DI的增减。
    DF=0,每次操作后SI、DI递增;DF=1,每次操作后SI、DI递减。DF标志位与串传送指令(movsb、movsw)有关,而串传送指令与游戏修改无关,所以不讲了(呵呵……)。   

    TF:跟踪标志位(用于程序调试
    如果TF=1,则CPU处于单步执行指令的工作方式,此时,每执行完一条指令,就显示CPU各个寄存器的当前值及CPU将要执行的下一条指令。如果TF=0,则处于连续工作模式。   

    AF:辅助进位标志位。
    在下列情况下,AF的值被设置为1,否则其值为0。
    1. 在字操作时,发生低字节向高字节进位或借位时。
    2. 在字节操作时,发生低4位向高4位进位或借位时。   


    IF:中断允许标志位
    用来决定CPU是否响应CPU外部的"可屏蔽中断"发出的中断请求,当IF=1,响应中断请求,当IF=0,不响应中断请求。   

    果壳疯狂编程:汇编语言从入门到神通

    果壳疯狂编程:汇编语言从入门到神通

    56(7.9) lea和nop指令

    7.9 lea和nop指令

    Lea为有效地址传送指令。  


    格式:lea 操作对象1,操作对象2
    功能:将源操作数给出的有效地址传送到指定的寄存器中。
    说明:操作对象1为目的操作数,可为任意一个16位的通用寄存器,操作对象2为源操作数,可为地址表达式。  
    比如,指令:
    Lea ax, [217a]   执行后,ax=217aH  
    Lea ax, [bx+si+200]   执行后,ax= bx+si+200H  

    Nop为空操作指令。

    格式:nop 功能:本指令不产生任何结果,仅消耗几个时钟周期的时间,接着执行后续指令,常用于程序的延时等待。
    其它用途:在修改游戏的时候,可用于锁定某些数据的数值。

    果壳疯狂编程:汇编语言从入门到神通

    果壳疯狂编程:汇编语言从入门到神通
    Nop锁定机械姬 • 邪恶大师娘的所有欲念数据——色即是空


    57第八章:寄存器(功能详解)

    第8章:寄存器(功能详解)

    引言

    本章对各种寄存器的功能进行详细的讲解,并且把32位寄存器和16位寄存器合并讲解。  


    32位寄存器有16个,分别是:

    4数据寄存器(EAX、EBX、ECX、EDX

    2变址寄存器(ESI、EDI

    2指针寄存器(ESP、EBP

    6段寄存器(ES、CS、SS、DS、FS、GS

    1指令指针寄存器(EIP

    1标志寄存器(EFlags
    58(8.1) 数据寄存器

    8.1 数据寄存器

    数据寄存器主要用来保存操作数和运算结果等信息,从而节省读取操作数所需占用总线和访问存储器(内存)的时间。  


    32位CPU有4个32位通用寄存器:EAX、EBX、ECX和EDX。  

    低16位寄存器分别是:AX、BX、CX和DX。

    它们和8086CPU中的寄存器相一致。 对低16位数据的取存,不会影响高16位的数据。  

    4个16位寄存器又可分割成8个独立的8位寄存器(AX:ah~al、BX:bh~bl、CX:ch~cl:DX:dh~dl)。  

    每个寄存器都有自己的名称,可独立存取。程序员可利用数据寄存器的这种“可合可分”的特性,灵活地处理字/字节的信息。

    为加深数据寄存器"可合可分"的印象,插播一篇:鬼谷子 • 捭阖第一

    粤若稽古,圣人之在天地间也,为众生之先,观阴阳之开阖以名命物,知存亡之门户。筹策万类之终始,达人心之理,见变化之联焉,而守司其门户。 故圣人之在天下也,自古及今,其道一也。变化无穷,各有所归。或阴或阳,或柔或刚,或开或闭,或驰或张。是故圣人一守司其门户,审察其先后,度权量能,校其伎巧短长。

    果壳疯狂编程:汇编语言从入门到神通

    果壳疯狂编程:汇编语言从入门到神通
    数据寄存器“可合可分”特性之鬼谷子 • 捭阖术之遐想

    AX和al通常称为累加器,用累加器进行的操作可能需要更少时间,累加器可用于乘、除、输入/输出等操作,它们的使用频率很高。  


    BX称为基地址寄存器,它可作为存储器指针来使用。  

    CX称为计数寄存器,在循环时,要用它来控制循环次数;在字符串操作(即位移操作)时,需要用cl来指明位移的位数。  

    DX称为数据寄存器,在进行乘、除运算时,它可以为默认的操作数参与运算,也可用于存放I/O的端口地址。

    在16位CPU中,AX、BX、CX和DX不能作为基址和变址寄存器来存放存储单元的地址,但在32位CPU中,其32位寄存器EAX、EBX、ECX和EDX不仅可传送数据、暂存数据、保存算术逻辑运算结果,而且也可作为指针寄存器,所以,这些32位寄存器更具有通用性。   
    59(8.2) 变址寄存器

    8.2 变址寄存器

    32位CPU有2个32位通用寄存器ESI和EDI,其低16位对应先前介绍的CPU中的SI和DI,对低16位数据的存取,不影响高16位的数据。  


    ESI、EDI、SI和DI称为变址寄存器,它们主要用于存放存储单元在段内的偏移量,用它们可实现多种存储器操作数的寻址方式,为以不同的地址形式访问存储单元提供方便。  

    变址寄存器不可分割成8位寄存器,作为通用寄存器,也可存储算术逻辑运算的操作数和运算结果。  

    它们可作一般的存储器指针使用,在字符串操作指令的执行过程中,对它们有特定的要求,而且还具有特殊的功能。   
    60(8.3) 指针寄存器

    8.3 指针寄存器

    32位CPU有2个32位通用寄存器EBP和ESP,其低16位对应先前介绍的CPU中的BP和SP,对低16位数据的存取,不影响高16位的数据。


    EBP、ESP、BP和SP称为指针寄存器,主要用于存放堆栈内存储单元的偏移量,用它们可实现多种存储器操作数的寻址方式,为以不同的地址形式访问存储单元提供方便。  

    指针寄存器不可分割成8位寄存器,作为通用寄存器,也可存储算术逻辑运算的操作数和运算结果。  

    指针寄存器主要用于访问堆栈内的存储单元,并且以下两条规定:

    一:BP为基指针寄存器,用它可直接存取堆栈中的数据。

    二:SP为堆栈指针寄存器,用它只可访问栈顶。   
    61(8.4) 段寄存器

    8.4 段寄存器

    段寄存器是根据内存分段的管理模式而设置的。内存单元的物理地址由段寄存器的值和一个偏移量组合而成的,这样可用两个较少位数的值组合成一个可访问较大物理空间的内存地址。  


    32位CPU有6个段寄存器,分别如下:

    CS:代码段寄存器     

    ES:附加段寄存器

    DS:数据段寄存器  
       
    FS:附加段寄存器

    SS:堆栈段寄存器   

    GS:附件段寄存器  

    在16位CPU系统中,只有4个段寄存器,所以,程序在任何时刻至多有4个正在使用的段可直接访问,在32位微机系统中,它有6个段寄存器,所以在此环境下开发的程序最多可同时访问6个段。

    32位CPU有两个不同的工作方式:实方式和保护方式。

    在每种方式下,段寄存器的作用是不同的,有关规定简单描述如下。  

    实方式:段寄存器CS、DS、ES和SS与先前介绍的CPU中的所对应的段寄存器的含义完全一致,内存单元的逻辑地址仍为“段地址:偏移地址”的形式,为访问某内存段内的数据,必须使用该段寄存器和存储单元的偏移地址。  

    保护方式:在此方式下,情况要复杂得多,装入段寄存器的不再是段值,而是称为“选择子”的某个值。
    62(8.5) 指令指针寄存器

    8.5 指令指针寄存器


    32位CPU把指令指针扩展到32位,并记作EIP,EIP的低16位与先前介绍的CPU中的IP作用相同。  

    指令指针EIP、IP是存放下次将要执行的指令在代码段的偏移地址,在具有预取指令功能的系统中,下次要执行的指令通常已被预取到指令队列中,除非发生转移情况,所以,在理解它们的功能时,不考虑存在指令队列的情况。  

    在实方式下,由于每个段的最大范围为64KB,所以,EIP的高16位肯定都为0,此时,相当于只用其低16位的IP来反映程序中的指令的执行次序。   
    63(8.6)  标志寄存器(详解)

    8.6  标志寄存器(详解)


    16位标志寄存器有9个标志位,可以分为两大类:
    1.运算结果标志位。一共6个,包括:CF进位标志位、PF奇偶标志位、AF辅助进位标志位、ZF零标志位、SF符号标志位、OF溢出标志位。

    2.状态控制标志位。一共3个,包括:TF追踪标志位、IF中断允许标志位、DF方向标志位。 以上标志位在第7章里都讲过了,在这里就不再解释了。


    现在讲讲32位标志寄存器增加的4个标志位。


    1、 I/O特权标志IOPL。 IOPL用两位二进制位来表示,也称为I/O特权级字段,该字段指定了要求执行I/O指令的特权级,如果当前的特权级别在数值上小于等于IOPL的值,那么,该I/O指令可执行,否则将发生一个保护异常。

    2、嵌套任务标志NT。 NT用来控制中断返回指令IRET的执行。具体规定如下:
    (1) 当NT=0,用堆栈中保存的值恢复EFlags、CS和EIP,执行常规的中断返回操作。
    (2) 当NT=1,通过任务转换实现中断返回。  


    3、 重启动标志RF。 RF用来控制是否接受调试故障。规定:RF=0时,表示接受,否则拒绝。

    4、虚拟8086方式标志VM。 如果VM=1,表示处理器处于虚拟的8086方式下的工作状态,否则,处理器处于一般保护方式下的工作状态。   
    64(8.7) 32位地址的寻址方式

    8.7 32位地址的寻址方式


    最后说一下32位地址的寻址方式。在前面我们学习了16位地址的寻址方式,一共有5种,在32位微机系统中,又提供了一种更灵活、方便但也更复杂的内存寻址方式,从而使内存地址的寻址范围得到了进一步扩大。  

    在用16位寄存器来访问存储单元时,只能使用基地址寄存器(BX和BP)和变址寄存器(SI和DI)来作为偏移地址的一部分,但在用32位寄存器寻址时,不存在上述限制,所有32位寄存器(EAX、EBX、ECX、EDX、ESI、EDI、EBP、和ESP)都可以是偏移地址的一个组成部分。  

    当用32位地址偏移量进行寻址时,偏移地址可分为3部分:


    1、 一个32位基址寄存器(EAX、EBX、ECX、EDX、ESI、EDI、EBP、ESP)。


    2、一个可乘以1、2、4、8的32位变址寄存器(EAX、EBX、ECX、EDX、ESI、EDI和EBP)。


    3、一个8位~32位的偏移常量。  


    比如,指令:mov ebx, [eax+edx*2+300]   eax就是基址寄存器,edx就是变址寄存器,300H就是偏移常量。  

    上面那3部分可进行任意组合,省去其中之一或之二。  

    下面列举几个32位地址寻址指令:

    Mov ax,  [123456]
    Mov eax,  [ebx]
    Mov ebx,  [ecx*2]
    Mov ebx,  [eax+100]
    Mov ebx,  [eax*4+200]
    Mov ebx,  [eax+edx*2]
    Mov ebx,  [eax+edx*4+300]
    Mov ax,  [esp]  

    由于32位寻址方式能使用所有的通用寄存器,所以,和该有效地址相组合的段寄存器也就有新的规定,具体规定如下:

    1、地址中寄存器的书写顺序决定该寄存器是基址寄存器还是变址寄存器。

    如:[ebx+ebp]中的ebx是基址寄存器,ebp是变址寄存器,而[ebp+ebx]中的ebp是基址寄存器,ebx是变址寄存器,可以看出,左边那个是基址寄存器,另一个是变址寄存器。

    2、默认段寄存器的选用取决于基址寄存器。


    3、基址寄存器是ebp或esp时,默认的段寄存器是SS,否则,默认的段寄存器是DS。


    4、在指令中,如果显式地给出段寄存器,那么显式段寄存器优先。   


    下面列举几个32位地址寻址指令及其内存操作数的段寄存器。

    指令列举: 访问内存单元所用的段寄存器  

    mov ax, [123456](默认段寄存器为DS)  
    mov ax,  [ebx+ebp](默认段寄存器为DS)  
    mov ebx,  [ebp+ebx] (默认段寄存器为SS)  
    mov ebx,  [eax+100] (默认段寄存器为DS)  
    mov edx,  ES:[eax*4+200](显式段寄存器为ES)
    mov [esp+edx*2], ax(默认段寄存器为SS)  
    mov ebx,  GS:[eax+edx*8+300](显式段寄存器为GS)  
    mov ax, [esp](默认段寄存器为SS)


    65第九章:与游戏修改相关的汇编指令

    第9章:与游戏修改相关的汇编指令

    引言  


    汇编指令总共116个,其中与游戏修改相关的指令大约20个,我们对这20个指令进行一下分类:  

    1. 传送指令(4个):mov、push、pop、lea
    2. 转移指令(8个):call、jmp、je、jne、jb、jnb、ja、jna
    3. 运算指令(7个):add、sub、mul、div、adc、sbb、cmp
    4. 处理器控制指令(1个):nop

    以上这些指令,我们在前面的章节中都已学过了,在本章中用表格的形式进行总结性的描述,包括:指令的名称类型格式功能说明示例

    如果你在修改游戏的时候,忘掉了某个指令的用法,可直接在本章中查询。   
    66(9.1) 传送指令

    9.1 传送指令

    果壳疯狂编程:汇编语言从入门到神通

    果壳疯狂编程:汇编语言从入门到神通


    果壳疯狂编程:汇编语言从入门到神通

    果壳疯狂编程:汇编语言从入门到神通


    果壳疯狂编程:汇编语言从入门到神通

    果壳疯狂编程:汇编语言从入门到神通


    果壳疯狂编程:汇编语言从入门到神通

    果壳疯狂编程:汇编语言从入门到神通

    67(9.2) 转移指令

    9.2 转移指令


    果壳疯狂编程:汇编语言从入门到神通

    果壳疯狂编程:汇编语言从入门到神通


    果壳疯狂编程:汇编语言从入门到神通

    果壳疯狂编程:汇编语言从入门到神通


    果壳疯狂编程:汇编语言从入门到神通

    果壳疯狂编程:汇编语言从入门到神通


    果壳疯狂编程:汇编语言从入门到神通

    果壳疯狂编程:汇编语言从入门到神通


    果壳疯狂编程:汇编语言从入门到神通

    果壳疯狂编程:汇编语言从入门到神通


    果壳疯狂编程:汇编语言从入门到神通

    果壳疯狂编程:汇编语言从入门到神通


    果壳疯狂编程:汇编语言从入门到神通

    果壳疯狂编程:汇编语言从入门到神通


    果壳疯狂编程:汇编语言从入门到神通

    果壳疯狂编程:汇编语言从入门到神通

    68(9.3) 运算指令

    9.3 运算指令


    果壳疯狂编程:汇编语言从入门到神通

    果壳疯狂编程:汇编语言从入门到神通


    果壳疯狂编程:汇编语言从入门到神通

    果壳疯狂编程:汇编语言从入门到神通


    果壳疯狂编程:汇编语言从入门到神通

    果壳疯狂编程:汇编语言从入门到神通


    果壳疯狂编程:汇编语言从入门到神通

    果壳疯狂编程:汇编语言从入门到神通


    果壳疯狂编程:汇编语言从入门到神通

    果壳疯狂编程:汇编语言从入门到神通


    果壳疯狂编程:汇编语言从入门到神通

    果壳疯狂编程:汇编语言从入门到神通


    果壳疯狂编程:汇编语言从入门到神通

    果壳疯狂编程:汇编语言从入门到神通

    69(9.4)  处理器控制指令

    9.4  处理器控制指令


    果壳疯狂编程:汇编语言从入门到神通

    果壳疯狂编程:汇编语言从入门到神通




    果壳 • 鬼谷子大法:“乃可以纵,乃可以横,而无敌于天下”

    果壳疯狂编程:汇编语言从入门到神通

    果壳疯狂编程:汇编语言从入门到神通
    妲己师娘的使命完成了——32位寻址全身——乃可以纵,乃可以横。




    (全书完)

    本书由果壳王子撰写,如有雷同,那就雷同!

    果壳王子微信:gkwz1983

    果壳学院QQ群:1群(327610461)、2群(465560580)

    果壳疯狂编程:汇编语言从入门到神通

    果壳疯狂编程:汇编语言从入门到神通




    1人点赞
    即便把我关在果壳中,我仍然是无限空间之王!

    大神点评1

    2020-12-14 09:33:05

    您需要登录后才可以回帖 登录 | 注册成为果猿 微信登录

    返回顶部