终于是开了这个新坑了,很早就想系统学习汇编了,好处有很多。可以为我转型 pwn 和 re 打下基础,而且 web 方向到后面必须得能从底层分析别人的源码。还有就是顺带应付一下期末考试,选修了汇编却什么也没学(悲
: 机器语言是机器指令的集合。
: 机器指令是一台机器可以正确执行的命令。
: 机器指令由一串二进制数表示,例 01010000
操作:将寄存器BX的内容送到AX中
汇编语言发展至今,有以下3类指令组成。
CPU 是计算机的核心部件,它控制整个计算机的运作并进行运算。要想让一个CPU工作,就必须向它提供指令和数据。指令和数据在存储器(内存)中存放。离开了内存,性能再好的CPU也无法工作。
计算机中的数据和指令,存储在内存或磁盘上。
数据和指令,都是二进制信息。
二进制信息1000100111011000是数据,还是指令?
; 1000100111011000 ─> 89D8H (数据)
; 1000100111011000 ─> MOV AX,BX (程序)
:数据如何表示?
; 1000100111011000B (二进制)
; 89D8H (十六进制)
; 104730O(八进制)
; 35288D(十进制)
:数据量:B、KB、MB、GB、TB…
在计算机中专门有连接CPU和其他芯片的导线,通常称为总线。 物理上:一根根导线的集合;
逻辑上划分为
地址总线
数据总线 (地址总线找到对应内存块1后,通过数据总线传输数据)
例:向内存中写入数据89D8H时的数据传送,8088(8 位数据总线),8086(16 位数据总线) 的区别
控制总线
学习汇编就是直接与内存打交道
CPU要想进行数据的读写,必须和外部器件进行三类信息的交互:
存储单元的地址(地址信息,通过地址总线)
器件的选择,读或写命令(控制信息,通过控制总线)
读或写的数据(数据信息,通过数据总线)
eg:
机器码: 101000000000001100000000
16进制:A00300
汇编指令:MOV AL,[3]
含义:从3号单元读取数据送入寄存器AL
可以看到地址线找到内存的 3 号地址块,控制线说我只要读取即可,然后数据线从 3 号内存块把 08 拿了过去
什么是内存地址空间?
CPU地址总线宽度为N,寻址空间为 2^N B。8086CPU的地址总线宽度为20,那么可以寻址1MB个内存单元,其内存地址空间为1MB。
(2023.12.30,其实不用自己搭建环境啊,用 vscode 的插件就行了,还更快更方便,跑代码也不用编译连接啥的了,安装过程看这篇文章一分钟,在VSCode中使用MASM/TASM搭建汇编环境_masm6.11-CSDN博客 )
汇编有很多版本,80x86,Linux 汇编,ARM 汇编。这里我们选择高龄的 8086 汇编,于当今的技术而言,是一个理想的教学模型。未来的工作可能基于 80x86, Linux 汇编、ARM 汇编,在 8086 汇编基础上再做拓展即可。
作为汇编语言的基础阶段,还就是经典的 DOS 环境即可,MASM 汇编,LINK连接,DEBUG 调试。问题又来了。在 Win xp 环境下,有 MS DOS 方式,尚可以运行 DOS 程序,但在 Win8 中,DOS 命令不支持了。一个简便的方案,先下载 8086 汇编工作环境吧。其中提供了一个 DOS 模拟器(DOSBox,大多用于模拟 DOS 环境玩一些经典 DOS 游戏),以及一组用于 8086 汇编程序设计的实用命令。解压后的文件夹如下:
链接:https://pan.baidu.com/s/14qWK2NKAPF1fjyso2uoypg?pwd=84i7
提取码:84i7
DOSBox0.74-win32-installer.exe 是 DOS 模拟器的安装文件;MASM 文件夹中是汇编程序设计中用到的命令;MASM 文件夹 里的 EX 文件夹中提供了几个汇编程序作为示例。
然后按照 pdf 里的步骤来搭建就行了
是 CPU 内部的信息存储单元。
8086CPU有14个寄存器:
共性
8086 CPU 所有的寄存器都是 16 位的。可以存放两个字节
以通用寄存器 AX 为例子
一个16位寄存器存储一个16位的数据最大值?
; 2^16-1
: 例:在AX中存储18D
; 18D
--- 12H
--- 10010B
:再例:在AX中存储20000D
; 20000D
--- 4E20H
--- 0100111000100000B
问题:8086上一代CPU中的寄存器都是8位
的,如何保证程序的兼容性?
通用寄存器均可以分为两个独立的
8位寄存器使用
; AX可以分为AH和AL
; BX可以分为BH和BL
; CX可以分为CH和CL
; DX可以分为DH和DL
8086是16位CPU, 8086的字长(word size)为16bit
汇编指令不区分大小写
程序段中的指令 | 指令执行后AX中的数据 | 指令执行后BX中的数据 |
---|---|---|
mov ax, 4E20H | 4E20H | 0000H |
add ax, 1406H | 6226H | 0000H |
mov bx, 2000H | 6226H | 2000H |
add ax, bx | 8226H | 2000H |
mov bx, ax | 8226H | 8226H |
add ax, bx | 044CH | 8226H |
设原AX、BX中的值均为0000H
程序段中的指令 | 指令执行后AX中的数据 | 指令执行后BX中的数据 |
---|---|---|
mov ax, 001AH | 001AH | 0000H |
mov bx, 0026H | 001AH | 0026H |
add al, bl | 0040H | 0026H |
add ah, bl | 2640H | 0026H |
add bh, al | 2640H | 4026H |
mov ah, 0 | 0040H | 4026H |
add al, 85H | 00C5H | 4026H |
add al, 93H | 0058H | 4026H |
设原AX、BX中的值均为0000H |
最后一步 c5 + 93 本来 = 0158H,但是因为 aL 只是 ax 的 8 位低位寄存器,所以 1 溢出舍去。故结果为 0058 H
CPU访问内存单元时要给出内存单元的地址。
所有的内存单元构成的存储空间是一个一维的线性空间。
每一个内存单元在这个空间中都有唯一的地址,这个唯
一的地址称为物理地址。
8086有20位地址总线,可传送20位地址,寻址能力
为1M。
8086是16位结构的CPU 运算器一次最多可以处理16位的数据,寄存器
的最大宽度为16位。
怎么做才能不浪费 4 位寻址地址。
8086CPU的解决方法
用两个16位地址(段地址、偏移地址)合成一个20位的物理地址。地址加法器合成物理地址的方法,物理地址=**段地址×16+偏移地址 ** (*16 二进制下左移四位,16进制下左移一位)
基地址和偏移地址经过地址加法器运算即可扩展为 20 位地址
在8086PC机中存储单元地址的表示方法
例:数据在21F60H内存单元中,段地址是2000H,说法
(a)数据存在内存2000:1F60单元中;
(b)数据存在内存的2000H段中的1F60H单元中。
段地址很重要!——用专门的寄存器存放段地址。
4个段寄存器:
CS - 代码段寄存器 DS - 数据段寄存器
SS - 栈段寄存器 ES - 附加段寄存器
Debug是DOS系统中的著名的调试程序,也可以运行在windows系统实模式下。使用Debug程序,可以查看CPU各种寄存器中的内容、内存的情况,并且在机器指令级跟踪程序的运行!
: 用R命令查看、改变CPU寄存器的内容
: 用D命令查看内存中的内容
: 用E命令改变内存中的内容
: 用U命令将内存中的机器指令翻译成汇编指令
: 用A命令以汇编指令的格式在内存中写入机器指令
: 用T命令执行机器指令
: ....
打开 DOSBOX ,输入 C: 然后输入 debug 即可进入。
: R - 查看寄存器内容
: R 寄存器名 回车 写值- 改变指定寄存器内容
: D - 列出预设地址内存处的
128个字节的内容
: D 段地址:偏移地址 - 列出内
存中指定地址处的内容
: D 段地址:偏移地址 结尾偏移
地址 - 列出内存中指定地址范
围内的内容
E 段地址:偏移地址 数据1 数据2 ...
: E 段地址:偏移地址
; 逐个询问式修改
; 空格 - 接受,继续
; 回车 - 结束
:有汇编指令
mov ax, 0123H
mov bx, 0003H
mov ax, bx
add ax, bx
:对应的机器码为
B8 23 01
BB 03 00
89 D8
01 D8
: e 地址 数据 - 写入
: d 地址 - 查看
: u 地址 - 查看代码
:有汇编指令
mov ax, 0123H
mov bx, 0003H
mov ax, bx
add ax, bx
:对应的机器码为
B8 23 01
BB 03 00
89 D8
01 D8
: a 地址 回车
: - 写入汇编指令
: d 地址 - 查看数据
: u 地址 - 查看代码
: t + 回车 - 执行CS:IP处的指令
mov ax, 0123H
mov bx, 0003H
mov ax, bx
add ax, bx
我们 r 查看 ,命令在 073f:0000 处,CS的确是指向 073f ,但是IP 现在指向 0100 ,所以 r ip 修改一下命令寄存器内容,让它 = 0000
然后 t 回车即可执行 mov ax,0123,可以看到 ax 寄存器的值变了
再 t + 回车执行下一条代码 mov bx,003
: q - 退出Debug
CS:代码段寄存器
IP: 指令指针寄存器
CS:IP:CPU将内存中CS:IP
指向的内容当作指令执行。
8086PC工作过程的简要描述:
事实:执行何处的指令,取决于CS:IP
应用:可以通过改变CS、IP中的内容,来控制CPU要执行的目标指令
: 问题:如何在(程序)代码里改变CS、IP的值?
方法1:Debug 中的 R 命令可以改变寄存器的值——rcs, rip,Debug是调试手段,并非程序方式!(可以在 debug 里面调试)
方法2:用指令修改 (大错特错)
mov cs, 2000H
mov ip, 0000H
8086CPU不提供对CS和IP修改的指令!
方法3:转移指令 jmp
jmp 段地址:偏移地址
jmp 2AE3:3
jmp 3:0B16
功能:用指令中给出的段地址修改CS,偏移地址修改IP。
jmp 某一合法寄存器
jmp ax (类似于 mov IP, ax,当然 move 是不合法的,只是举个例子)
jmp bx
功能:用寄存器中的值修改IP。
-a 073f:0100
jmp 073f:2000
-a 073f:2000
mov ax,1000
用DS存放数据段的段地址,用相关指令(mov、add、sub…)访问数据段中的具体单元,单元地址由[address]指出
将123B0H~123BAH的内存单元定义为数据段,累加数据段中的前3个字型数据
给出00000H-0001F的数据,请写出下面代码的执行结果:
mov ax,[0000] | 2662 | 0000 |
---|---|---|
代码 | AX | BX |
mov bx,[0001] | 2662 | e626 |
mov ax,bx | e626 | e626 |
mov ax,[0000] | 2662 | e626 |
mov bx,[0002] | 2662 | d6e6 |
add ax, bx | fd48 | d6e6 |
add ax,[0004] | 2c14 | d6e6 |
mov ax,0 | 0000 | d6e6 |
mov al,[0002] | 00E6 | d6e6 |
mov bx, 0 | 00e6 | 0000 |
mov bl, [000C] | 00e6 | 0026 |
add al,bl | 000c | 0026 |
解释一下因为 8086 16 位,所以偏移 2 个字节读取下一个数 ([0002],[0004])
(2024.1.1,师傅们新年快乐,情不自禁更新一下)
栈是一种只能在一端进行插入或删除操作的数据结构。
栈有两个基本的操作:入栈和出栈。
入栈:将一个新的元素放到栈顶;
出栈:从栈顶取出一个元素。
栈顶的元素总是最后入栈,需要出栈时,又最先被从栈中取出。栈的操作规则:LIFO(Last In First Out,后进先出)PUSH(入栈)和 POP(出栈)指令
push ax:将ax中的数据送入栈中
pop ax:从栈顶取出数据送入ax
(8086 以字为单位对栈进行操作)
8086CPU中,有两个与栈相关的寄存器:
栈段寄存器SS - 存放栈顶的段地址
栈顶指针寄存器SP - 存放栈顶的偏移地址
——任意时刻,SS:SP指向栈顶元素。
目前已学习
代码段 cs
将段地址放在 CS中,将段中第一条指令的
偏移地址放在IP中
. CPU将执行我们定义的代码段中的指令;
数据段 ds
将段地址放在Ds中
用mov、add、sub等访问内存单元的指令
时,CPU将我们定义的数据段中的内容当
作数据段来访问;
栈段 ss
将段地址放在SS中,将栈顶单元的偏移地
置放在 SP 中
. CPU在需要进行栈操作(push、pop)时,就
将我们定义的栈段当作栈空间来用。
源文件 .asm -> 目标文件.obj -> 目标文件 -> .obj -> 可执行文件 .exe
举个例子:
mov ax,[bx] 相当于 (ax)=((ds) *16 +(bx))
实现循环(计数型循环),指令的格式 loop 标号
① (cx)=(cx)-1;
② 判断cx中的值
不为零则转至标号处执行程序,如果为零则向下执行。
cx 中要提前存放循环次数,因为(cx)影响着loop指令的执行结果,要定义一个标号
assume cs:code
code segment
mov ax,1
mov cx,5
s: add ax,ax
loop s
mov ax,4c00h
int 21h
code ends
end
Debug中,mov al, [0]的功能是 ——将DS:0存储单元的值传给AL,编译好的程序中,mov al, [0]变成了将常量0传给AL
对策:在[idata]前显式地写上段寄存器,mov al,ds:[0]
数据放在代码段就可以通过代码段寄存器拿到数据了
mov bx,0
mov ax,cs:[bx]
但是这样有问题?
解决问题的关键:让数据从CS:0000开始,让代码从CS:0010开始!
效果:程序加载后,CS:IP指向要执行的第一条指令在start处!
assume cs:code,ds:data,ss:stack
data segment
dw 0123H,0456H,0789H,0abcH,0defH,0fedH,0cbaH,0987H
data ends
stack segment
dw 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
stack ends
code segment
start:
mov ax,start
mov ss,ax
mov sp,20h
mov data
mov ds,ax
code ends
end start
可以发现代码段里,除了代码段寄存器 cs,数据段 ds 和 堆栈段 ss 寄存器(包括其 sp )都要初始化其地址
mov ax,start
mov ss,ax
mov sp,20h
mov data
mov ds,ax