汇编个人学习笔记
2023-06-15 17:14:56 # Reverse Engineering

前言

汇编是很多课程的重要基础,正好最近在学习操作系统,发现在操作系统的学习中,汇编也表现得极为重要,同样在逆向的学习中,不会汇编基本等于与逆向无门了。

此篇是个人笔记,供自己复习和做笔记。

参考资料:《汇编语言(第3版) 》王爽著

基础知识

进制转换和数据宽度

16进制

0123456789ABCDEF

10 11 12 13 14 15 16 17 18 19 20 21….

One single hexadecimal digit = 4 bit

8进制

01234567

10 11 12 13 14 15

One single Octal digit = 3 bit

数据宽度:

1 Byte = 8 Bit

1 Word = 2 Bytes =》四个十六进制数字为一个word

Double Word = 2 Word

需要记住的一些数字

1024=2^10

1KB=1024B(Byte)

1MB=1024KB

1GB=1024MB

1TB=1024GB

CPU对存储器的读写

比如CPU要从内存地址3中读写数据:

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

地址总线

地址总线是一根导线,我们知道一根导线传输数据的时候只有两种稳定状态 - 要么是高电平要么是低电平 - 二进制表示就是0和1

CPU用地址总线来寻址,有多少不同的信息,CPU就可以对多少个存储单元(一个存储单元里有1byte的信息)寻址

所以如果现一CPU有10根导线,那么它可以发出10 bit的二进制数,而10bit的二进制数可以表示2^10=1024个不同的数据,也就是可以寻找2^10=1024个内存单元

image-20210906135049439

上图演示的是将地址1011通过地址总线发出

数据总线

CPU和其他器件之间的数据传送是通过数据总线进行的。数据总线的宽度决定了CPU和外界数据传送的速度。

一个数据总线可以传送一个bit(位)的数据,8根数据总线可以传送一个byte的数据

假设你有16根数据线,那么你一次性可以传输16bit的数据 - 所以你可以传输89D8H - 因为89D8是十六进制,一个十六进制数字为4bit,所以89D8刚好16bit. 如果数据线只有八根,意味着你只能传输8bit的数据,那么你只能分两次传送:先D8后89(从低位开始传)

image-20210906140434026

控制总线

CPU对外部器件的控制是通过控制总线来进行的。此处控制总线是一个总称,它有多少根控制总线,意味着CPU提供了对外部器件的多少种控制。所以控制总线的宽度决定了CPU对外部器件的控制能力。

比如读和写就分别为两条控制总线。

课后习题 1.1

image-20210906140647234

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
(1)CPU的寻址能力为8Kb,换算成Byte8*1024Byte。 我们知道一个内存单元可以储存1Byte,那么意味着该CPU可以寻址8*1024=2^13个内存单元。

所以我们可以得出他有13个地址总线,则宽度为13

(2) 1KB的存储器有1024个存储单元,因为一个存储单元存储1Byte信息。存储单元的编号从01023

(3) 1KB的存储器可以存储10248=2^13 bit,1024Byte

(4) 1GB=2^30 Byte, 1MB = 2^20 Byte, 1KB= 2^10 Byte.

(5) 2^16*2^(-10)=2^6, 2^20*2^(-20)=1, 2^24*2^(-20)=2^4,2^32*2^(-30)=2^2=4

(6) 1B,1B,2B,2B,4B

(7)

要读的数据:1024=2^10 byte,

根据上面的问题,8086一次可以读2 Byte,那么要读2^9=512

80386一次可以读4 Byte,那么要读2^8=256

(8) 在存储器中,数据和程序以二进制形式存放

寄存器

通用寄存器

CPU由运算器、控制器、寄存器构成。

  • 运算器进行信息处理
  • 寄存器进行信息存储
  • 控制器控制各种器件进行工作
  • 内部总线连接各种器件,在它们之间进行数据的传送

16位寄存器:

AX

BX

CX

DX

8位寄存器:会被单独使用,比如AH溢出后,不会把溢出的位补到AX前面的高位(AH)去

H表示high,L表示low

AH AL

BH BL

CH CL

DH DL

几条汇编指令

mov 寄存器,数据 mov ax,2

mov 寄存器,寄存器 mov ax,bx

mov 寄存器,内存单元 mov ax,[0]

mov 内存单元,寄存器 mov [0], ax

mov 段寄存器,寄存器 mov ds,ax

mov 寄存器,段寄存器 mov ax,ds

课后习题2.1

(1)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
mov ax,62627	AX=F4A3H
mov ah,31H AX=31A3H
mov al,23H AX=3123H
add ax,ax AX=6246H
mov bx,826CH BX=826CH
mov cx,ax CX=6246H
mov ax,bx AX=826CH
add ax,bx AX=04D8H
mov al,bh AX=0482H
mov ah,bl AX=6C82H
add ah,ah AX=D882H
add al,6 AX=D888H
add al,al AX=D810H
mov ax,cx AX=6246H

(2)用目前学过的汇编指令,最多使用四条指令,计算2的4次方

1
2
3
4
mov ax,0002
add ax,ax
add ax,ax
add ax,ax

物理地址、段和偏移地址

8086中,地址总线有20根,但是数据总线只有16根。所以需要用两个16进制表示20进制的地址。段地址(16位)×16+偏移地址(16位)=地址(20位)

地址加法器处理后可以得到物理地址

物理地址=段地址*16+偏移地址

短地址*16表示十六进制左移1位,也就是二进制左移4位

比如十进制20要左移一位:20*10=200

二进制10B左移一位:100B转换成十进制2*2=4

一个数据的二进制形式左移1位,相当于该数据×2

一个数据的二进制形式左移N位,相当于该数据×2^N

image-20210907170044257

偏移地址16位,最多可以寻址64KB(2^16bit个内存单元,一个内存单元8bit,所以2^16*2^3=2^19bit=2^6KB=64KB)

课后习题2.2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
1)给定段地址位0001H,仅通过变化偏移地址寻址,CPU的寻址范围为?

最小:0001H*16+0000H=0010H

最大:0001H*16+FFFFH=1 000FH

**所以寻址范围为0001H~1000FH**

2)有一段数据存放在内存 20000H单元中,先给定段地址为SA,想用偏移地址寻到此单元,则SA应该满足的条件是:

最小为:

当偏移地址为最大FFFFH时,20000H-FFFFH=1 0001H

SA必须为16的倍数,所以SA左移后的最后一位必须是0

1 0010H-1001H

**所以最小为1001H**

最大为:

20000H->2000H

**所以最大为2000H**

段寄存器

8086CPU有四个段寄存器:

CS(code segment), DS(data segment), SS(stack segment), ES(extra segment)

CS和IP(instructor pointer指令指针寄存器)是最关键寄存器 -》指向了CPU当前要读的地址

8086CPU工作过程:

  • 从CS:IP指向的内存单元获取指令
  • IP=IP+所读取指令的长度,从而指向下一条按指令
  • 执行指令,回到第一步,重复这个过程

修改CS IP的指令不用mov指令(因为不支持把数据直接送入段寄存器)而是用jmp

  • 同时修改CS 和 IP:
    • jmp CS: IP
    • 如 jmp 2AE3:3
      • CS=2AE3H, IP=0003H -> 2AE30+0003=2AE33H
    • jmp 3:0B16
      • CS=0003H, IP =0B16H -> 0030+0B16=0B46H
  • 只修改IP,当前CS不变
    • jmp IP

课后习题 2.3

下列3条指令执行后,CPU几次修改IP?都是在什么时候?最后IP中的值是多少?

1
2
3
4
5
6
7
8
mov ax,bx#读取指令后IP被修改

sub ax,ax#读取指令后IP被修改

jmp ax #读取指令后IP被修改,然后IP又被修改为ax

# cpu修改了4次IP,因为每执行完一个指令,IP都会递增1, -》 在jmp ax处,最后IP被修改为了0

image-20210909145406877

1
2
3
mount c: d:\asm
c:\
debug

寄存器(内存访问)

内存中字的存储

一个内存单元是一个byte(字节)

一个字单元由两个内存单元组成,也就是两个字节(bytes)=1个word

0地址: 2E

1地址:3C

问0地址单元存储的**字型(word)**数据是:3C2EH

0地址单元存储的**字节型(byte)**数据是:2EH

DS和[address]

要给ds寄存器赋值,必须要先把值赋给通用寄存器,然后通用寄存器再赋值给ds寄存器。数据不能直接送进段寄存器。

[]是对数据段操作

例如:从10000H中读取数据

1
2
3
mov bx,1000H#为什么是1000H,因为1000H段地址*16=左移一位,然后加上偏移地址就可以得到最后的真实地址
mov ds, bx
mov al,[0]#10000+0=10000H

字的传送

image-20210909155520090

ax是一个word的长度,所以这里mov ax,[0]其实取的是1123而不是23

11是高位,22是低位,我们取了连续的两个内存单元而不是一个内存单元

1
sub ax,1CH#ax=ax-1CH

mov, add, sub指令

mov ds, ax和mov ax,ds都可以实现。

段寄存器也可以给通用寄存器传数据

数据段

我们用123B0H~123B9H这段空间来存放数据:

  • 段地址:123BH
  • 长度:10字节(一个地址指向一个内存单元,一个内存单元等于一个字节,0~9有10个内存单元,所以有10个字节)

高位字节存放在高位内存

低位字节存在低位内存

由高地址向低地址增长

CS:IP指向指令

DS:IP指向数据

SS栈段寄存器(Stack segment) - 存放栈顶的段地址

SP寄存器(Stack pointer) - 存放栈顶的偏移地址

SS:SP指向栈顶元素,但如果stack为空的时候,会指向最高地址单元的下一个单元,比如栈空间是10000H~1000FH,如果stack为空,会指向1000:10(所以不存在栈顶元素)

  • 也可以说是最后一个元素被pop, SP=E(最后一个元素所在栈地址)+2=10

push ax

  • 先SP = SP-2(因为ax是2个bytes = word,所以是2个内存空间)

  • 把ax放进SS:SP指向的位置

pop ax

  • 先把SS:SP指向位置的内容pop并赋值给ax

  • SP = SP+2

pop的时候其实没有删除数据,只是指针改变了(所以被pop的数据就不在栈里了,因为它的地址不在栈顶),下次push的时候原本的东西就被覆盖了。

C语言里面的函数其实就是用到了栈,函数会把局部变量什么的都放在stack里,当函数返回后,stack就没有了。

栈顶越界的问题

8086CPU只知道:

  • 当前栈顶在何处
  • 当前要执行的指令是哪一条

要防止push和pop导致栈顶超界,根据可能用到的最大栈空间,来安排栈的大小 - 防止push的数据太多而导致超界、栈空的时候继续pop导致超界。

push、pop指令

1
2
3
4
5
push 寄存器
push 段寄存器
push 内存单元#push [0] -> ds:ip
pop 寄存器
pop 段寄存器

image-20211202103533130

上图演示了将2266H放入10000H的位置。因为push会先减去sp,所以我们要先把sp+2

栈段

再次强调:

SS:SP指向栈顶元素,但如果stack为空的时候,会指向最高地址单元的下一个单元,比如栈空间是10000H~1000FH,如果stack为空,会指向1000:10(所以不存在栈顶元素)

  • 也可以说是最后一个元素被pop, SP=E(最后一个元素所在栈地址)+2=10

第一个程序

伪指令是让编译器去处理

汇编指令是

1
2
3
4
5
6
7
8
9
10
11
12
13
14
assume cs:codesg #假设代码代码段叫做codesg

codesg segement

start: mov ax,0123H
mov bx,056H
add ax,bx
add ax,ax

mov ax,4c00H
int 21H
codesg ends #段的结束

end start#结束 这里的start可以随便什么名字,程序只会查找end后面的东西

程序返回:

1
2
mov ax,4c00H
int 21H

CMD将程序载入内存,将CPU的CS:IP指向程序,程序运行结束后回到command,CPU继续运行command