汇编五 - 汇编语言补充

知识点

  1. 学习环境为什么要选择VC6?
    1. 为了更好地学习细节,因为越是高版本的开发环境,在编译的时候替我们添加的额外代码越多,不利于我们学习
    2. VC6比较小,安装方便
    3. 凡是搞底层的人开发环境一定是VC6
  2. UltraEdit
    1. 用于查看二进制文件的一款PC软件。
  3. 熟练背会二进制与16进制之间的对应关系。
  4. 数据宽度
    1. 数学上的数字,是没有大小限制的,可以无限的大。但在计算机中,由于受硬件的制约,数据都是有长度限制的(我们称之为数据宽度),超过最多宽度的数据会被丢弃。
    2. 计算机中常见的数据宽度
      1. 位:bit,一个二进制位
      2. 字节(byte):8个bit,至少
      3. 字(word):16个bit
      4. 双字(doubleword):32个bit
  5. 原码、反码、补码
    1. 正数:三码合一
    2. 负数:
      1. 反码:符号位不变,其余所有二进制码取反。
      2. 补码:符号位不变,其余所有二进制码取反,然后再加1.
      3. 补码的补码等于源码。
    3. 计算机在内存中以补码的方式存储。
    4. 16位存储的方式 图1
      1. 无符号数存储的范围:0~FF
      2. 有符号数存储的范围:
        1. 正数:0~7F
        2. 负数:FF~80
        3. 一个圆形被分割成2半,左边负数,右边正数。(注意,负数都是补码的形式存储的,FF就是-1)
  6. 计算机只会做位运算(与、或、非、异或、左移、右移动),不会四则运算
    1. 加:(计算机使用位运算实现加法)
      1. 2个数先异或,结果为X
      2. 判断是否有进位,用与判断,将2个数与的结果有值为Y,则说明有进位,没有值则没有进位
        1. 没有进位,这异或的结果X就是加法的值。
        2. 有进位
          1. 将Y左移一位,然后与X异或,得结果为Z
          2. 再次执行2,进行判断,直到没有进位,得到最终结果。
    2. 减:
      1. 本质还是加法运算
      2. 4-5 => 4+ (-5).
      3. -5的补码为FB
    3. 乘:本质是加法
    4. 除:本质是减法
    5. 如下图: 图1
  7. 学汇编的目的是为了了解程序运行的原理。
    1. 当学习一种新语法,不太理解时
    2. 直接转化成汇编,就能看到本质。
  8. 配置汇编的学习环境
    1. 下载DTDEBUG
      1. 百度搜索下载DTDebug(VT-O)专业版V1.0.025
    2. 配置路径。
      1. 下载之后解压,打开文件夹,然后打开DTDebug文件夹
      2. 双击DTDebug.exe
      3. 然后,选择工具栏->options->appearance->Directories
      4. 设置udd路径,找到DTDebug文件加下的udd文件即可。
      5. 设置plugin路径,找到DTDebug文件加下的plugin文件即可。
      6. 然后关闭重新打开DTDebug即可。
    3. 窗口简介
      1. 工具栏打开View->CPU,然后展示一个窗口,这个窗口有4个区域
      2. 第一个区域:通常用于写汇编代码的区域,叫反汇编窗口
      3. 第二个区域:register(FPU),寄存器窗口。
      4. 第三个区域:左下角,为内存窗口
      5. 第四个区域:右下角,堆栈窗口
    4. 常用快捷键:

       F8 :单步步过,单步执行
       F7 : 单步步入,不跳跃函数单步执行。
       F2: 设置断点
      
    5. 内存窗口的使用
      1. 这个内存窗口就是虚拟内存的4G空间。
      2. 可以输入命令查看内存的数据

         db:一个字节一个字节的查看
         db 18ff9c
         dw:2个字节2个字节的查看
         dw 18ff9c
         dd: 4个字节4个字节的查看
        
    6. 寄存器窗口
      1. EFL为标志寄存器,以及内部存储的值
      2. EFL上面的:C/P/A/Z/S/T/D/O为该寄存器的有效标志位,
      3. 比如D为DF标志位,方向标志位。在数据串操作时确定操作的方向,EFL的第10位。
    7. 堆栈窗口
      1. 这里展示的就是一个应用进程启动后,操作系统分配的4G虚拟内存空间的堆栈段。

寄存器

  1. 寄存器有多大?
    1. 32位CPU: 8位、16位、32位,3种
    2. 64位CPU: 8位、16位、32位,64位,4种。
  2. 当前计算机的cup是多少位?
    1. 05年以后买的电脑都是64位的。
  3. 为何要学32位的汇编?
    1. 因为64位就是从32位拓展过来的,并没有整体变化。
    2. 只是增加了一些寄存器,汇编指令还是一样的。

通用寄存器

  1. 32位通用寄存器,共8个,每个32位

     EAX     ESP
     EBX     EBP
     ECX     ESI
     EDX     EDI
    
  2. 所有的通用寄存器

    图1

    1. 注意观察:ESP/EBP/ESI/EDI,这四个没有8位的寄存器。
    2. AH/BH/CH/DH分别是由AX/BX/CX/DX的高八位。

内存

  1. 虚拟内存映射到物理内存,注意物理内存还不是内存条,他还需要再一次映射到内存条中。 图1
  2. 内存地址32位
  3. 往内存中写数据时一定要注明宽度

     mov byte ptr ds:[0018fff0],1
     常见宽带:BYTE /WORD /DWORD
    

内存地址在汇编中的5种表示形式

  1. 形式一:[立即数]

     //从内存中读取数据
     MOV EAX,DWORD PTR DS:[0X13FFC4]
     //向内存中写入数据
     MOV DWORD PTR DS:[0X13FFC4] EAX,
    
  2. 形式二:[reg] reg代表寄存器,可以是8个通用寄存器中的任意一个。

     读取内存的值:
     ;立即数(内存地址)传到ECX寄存器
     MOV ECX,0X13FFD0
     ;速去内存的值
     MOV EAX,DWORD PTR DS:[ECX]
        
     向内存中写入数据
     MOV EDX,0X13FFD8
     ;将立即数存入内存
     MOV DWORD PTR DS:[EDX],0X87654321
    
  3. 形式三:[reg+立即数]

     读取内存的值:
     MOV ECX,0X13FFD0
     ;速去内存的值
     MOV EAX,DWORD PTR DS:[ECX+4]
        
     向内存中写入数据
     MOV EDX,0X13FFD8
     ;将立即数存入内存
     MOV DWORD PTR DS:[EDX+0xC],0X87654321
    
  4. 形式四:[reg+reg*{1,2,4,8}]
    1. 乘的数字只能是1、2、4、8
     读取内存的值:
     MOV EAX,13FFC4
     MOV ECX,2
     MOV EDX,DWORD PTR DS:[EAX+ECX*4]
    
  5. 形式五:[reg+ret*{1,2,4,8}+立即数]

     读取内存的值:
     MOV EAX,13FFC4
     MOV ECX,2
     MOV EDX,DWORD PTR DS:[EAX+ECX*4+4]
    

数据的存储模式(大小端)

  1. x86 的cpu采用的是小端模式存储的
  2. arm 采用的是大端模式存储

常用汇编指令

  1. mov指令
     ;把一个8位的寄存器的值,存入到一个8位寄存器或者8位的内存中,比如:mov byte ptr ds:[0x13FFC4],AL
     MOV r/m8,r8         r代表通用寄存器
     MOV r/m16,r16       m代表内存
     MOV r/m32,r32       imm代表立即数
     MOV r8,r/m8         r8代表8位通用寄存器
     MOV r16,r/m16       m8代表8位内存
     MOV r32,r/m32       imm8代表8位立即数
     MOV r8,imm8
     MOV r16,imm16
     MOV r32,imm32
    
  2. movs指令
    1. mov指令是不允许内存->内存移动数据的,但是movs指令可以

       MOVS BYTE PTR ES:[EDI],BYTE PTR DS:[ESI] ;简写为MOVSB
       MOVS WORD PTR ES:[EDI],WORD PTR DS:[ESI] ;简写为MOVSW
       MOVS DWORD PTR ES:[EDI],DWORD PTR DS:[ESI] ;简写为MOVSD
      
    2. ESI/EDI的第二作用:
      1. 当要把一串数据从内存的一个地方复制到内存的另一个地方,这时就要用到ESI/EDI
      2. 即只要用MOVS指令,那么就要用到ESI/EDI
      3. ESI存储的是要复制的数据地址,EDI存储的是要存放的数据地址。
      4. 每执行一次MOVS指令,EDI/ESI中的内存地址就会相应的自动增加一次。
    3. FEL标志寄存器的第10位DF,叫做方向位
      1. 当DF为0时,每执行一次MOVS,EDI/ESI自动增加一次
      2. 当DF为1是,每执行一次MOVS,EDI/ESI自动减一次
  3. stos指令
    1. 将AL/AX/EAX值存储到[EDI]指定的内存单元。

       STOS BYTE PTR ES:[EDI] ;简写为STOSB
       STOS WORD PTR ES:[EDI] ;简写为STOSW
       STOS DWORD PTR ES:[EDI] ;简写为STOSD
      
    2. 执行完STOS指令后,EDI值也会加一次,至于加还是减取决于标志寄存器的DF位。

  4. REP指令:
    1. 按照计数寄存器ECX中指定的次数重复执行字符串指令。

       MOV ECX,10
       REP MOVSD
              
       REP STOSD
      
    2. 每执行一次,ECX中的值会减1,直到0为止。

堆栈操作指令

  1. 堆栈段就是一块内存,操作系统在程序启动的时候就已经分配好的,供程序执行的时候使用。
  2. 通过DTDEBUG查看堆栈窗口
    1. 在DTDEBUG的堆栈窗口展示的,就是当前应用程序由操作系统分配的堆栈区域
    2. 也可以从内存窗口中查看
      1. 寄存器窗口中有个FS寄存器,假设内部存储的值为7EFDD000
      2. 然后在内存窗口输入命令:dd 7EFDD000

         7EFDD000  FFFFFFFF  (Pointer to SEH chain)
         7EFDD004  00190000  (Top of thread's stack)
         7EFDD008  0018D000  (Bottom of thread's stack)
        
      3. 可以看到,系统分配的堆栈的内存地址为:0018D000~00190000这段内存空间。
      4. 然后查看堆栈窗口的内存地址为:0018D000~0018FFC,0018FFC+4 = 00190000,所以正好为这段内存。
  3. 堆栈内存,是从大往小开始用的,即先用0018FFC
  4. 可以通过查看esp寄存器,中存储的值,查看当前堆栈用到哪了
    1. 右击esp->follow in stack ,可以定位到堆栈窗口的相应位置。
    2. 那么此时的堆栈段:从当前位置到0018FFC这段堆栈已经被之前的程序用掉了。
  5. 如何使用堆栈?
    1. 使用push、pop指令。
    2. 作用:
      1. 将数据存、取到堆栈段
      2. 修改ESP寄存器的值。

修改EIP的指令

  1. EIP寄存器存储的是CPU要执行的下一套指令的值。
  2. 通过汇编语言不能直接修改EIP的值。
  3. 通过jmp指令、call指令
    1. JMP指令的本质
      1. 修改EIP的值为JMP的值
    2. call指令的本质
      1. 修改EIP的值为CALL指令的值
      2. 将下一条指令的值放入栈中。
      3. ESP的值减4
  4. RET指令
    1. ESP值+4
    2. 将堆栈的值存储到EIP

fake反调试

  1. 反调试是什么?
    1. 不让别人调试我的代码,比如F8单步执行,但是不允许别人使用F8调试。
    2. Fake F8:伪装F8,让F8不能调试。
  2. 断点的原理
    1. 断点:0xCC
      1. 设置断点:F2
      2. 断点的本质就是指令:int 3,当CPU执行这个指令是就会停下来
      3. F2设置断点,就是将当前代码设置为int 3;对应的16进制码为0XCC;
  3. F7与F8的区别
    1. 表面区别:
      1. 在执行call指令时有区别,F8会直接跳过call调用的函数
      2. F7会进入函数内部一步一步执行。
    2. 本质区别
      1. 单步步入(F7):设置EFLAGS标志寄存器的TF为1,不是设置断点。
      2. 单步步过(F8):在下一行设置断点。所以F8在执行call指令的时候会直接跳过
  4. 如何实现让F8失效?
    1. 编写call指令:

       CALL 0041840d
      
    2. 在调用函数地址处添加如下代码

       MOV DWORD PTR DS:[ESP],00418481
       RET
      
    3. 这么一写如果按得是F8,那么,就会直接执行call函数,但是下一条代码可不是原来CALL下面的代码了,而是这个地址(00418481)的代码。这么一来就跟飞了

      1. 因为调用call函数,会把下一条指令地址放入栈中,就是DS:[ESP]这个内存单元,但是由于你执行了MOV DWORD PTR DS:[ESP],00418481,把他改变了,那么当再次执行ret的时候,把栈中的数据返回给EIP的时候,就是00418481了,程序就会从这里执行了

JCC指令

  1. 标志寄存器 图1

    1. 单步标志:用于调试器
    2. 中断标志:中级才会使用
    3. AF为很少用,主要用与BCD运算时用。
    4. ZF位工作中用到的最多
Table of Contents