编程语言的发展
机器语言
- 由0和1组成
汇编语言(Assembly Language)
- 用符号代替了0和1,比机器语言便于阅读和记忆
- 操作: 将寄存器BX的内容送入寄存器AX
- 汇编语言:
mov ax, bx
高级语言
C\C++
、Java
、Swift
、Go
、Python
等,更接近于人类的自然语言。
- 汇编语言与机器语言一一对应,每一条机器指令都有与之对应的汇编指令
- 汇编语言可以通过编译得到机器语言,机器语言可以通过反汇编得到汇编语言
- 高级语言可以通过编译得到汇编语言\机器语言,但汇编语言\机器语言几乎不可能还原成高级语言
高级语言反编译表现
汇编语言的特点
- 可以直接访问、控制各种硬件设备,比如存储器、CPU等,能最大限度地发挥硬件的功能
- 汇编指令是机器指令的助记符,同机器指令一一对应。每一种CPU都有自己的机器指令集\汇编指令集,所以汇编语言不具备可移植性
- 知识点过多,开发者需要对CPU等硬件结构有所了解,不易于编写、调试、维护
- 不区分大小写,比如
mov
和MOV
是一样的
编译阶段比较
- 采用高级语言C++和汇编语言编写同一个功能
将a+b的结果赋值给c,然后在屏幕上打印c的结果
- 采用高级语言C++和汇编语言编写同一个功能
汇编语言的用途
- 编写驱动程序、操作系统(比如Linux内核的某些关键部分)
- 对性能要求极高的程序或者代码片段,可与高级语言混合使用(内联汇编)
- 软件安全
- 病毒分析与防治
- 逆向、加壳、脱壳、破解、外挂、免杀、加密解密、漏洞、黑客
- 是理解整个计算机系统的最佳起点和最有效途径
- 为编写搞笑代码打下基础
- 弄清代码本质
sizeof
switch
和if
的效率究竟谁高?
汇编语言的种类
- 目前讨论比较多的汇编语言有
- 8086汇编(8086处理器是16bit的CPU)
- Win32汇编、Win64汇编
- AT&T汇编(Mac、iOS模拟器)
- ARM汇编(嵌入式、iOS设备)
- 入门建议先从学些8086汇编开始,推荐参考书籍:王爽《汇编语言》
- 目前讨论比较多的汇编语言有
学前须知
软件\程序的执行过程
最为关键的是需要了解CPU和内存
在学习汇编语言过程中,遇到的绝大部分指令都是跟内存、CPU有关的
总线概念及分类
内存
各类存储器的逻辑连接情况
内存地址及区域划分
各类存储器的逻辑连接-物理地址对应图
8086的寻址方式
内存的分段管理
8086是用“起始地址(段地址×16) + 偏移地址 = 物理地址”的方式给出物理地址
为了开发方便,我们可以采取分段的方法来管理内存,比如:
- 地址10000H~100FFH的内存单元组成一个段,该段的起始地址为10000H,段地址为1000H,大小为100H
- 地址10000H1007FH、10080H100FFH的内存单元组成2个段,它们的起始地址为:10000H和10080H,段地址为1000H和1008H,大小都为80H
1 2 3 4 5 6 7
地址合成 段地址: 1200H 偏移地址: 1000H 物理地址 = 段地址 * 16 + 偏移地址 物理地址 = 段地址 * 10H + 偏移地址 物理地址 = 1200H * 10H + 1000H = 12000H + 1000H = 13000H
寻址能力计算
- 偏移地址为16位,16位地址的寻址能力为64KB,所以一个段的长度最大为64KB
寄存器
CPU的典型构成
CPU的典型构成
通用寄存器
AX
、BX
、CX
、DX
这4个寄存器通常用来存放一般性的数据,称为通用寄存器(有时也有特定用途)- 通常,CPU会先将内存中的数据存储到通用寄存器中,然后再对通用寄存器中的数据进行运算
- 假设内存中有块红色内存空间的值是3,现在想把它的值加1,并将结果存储到蓝色内存空间
- CPU首先会将红色内存空间的值放到AX寄存器中:
mov ax,红色内存空间
- 然后让AX寄存器与1相加:
add ax,1
- 最后将值赋值给内存空间:
mov 蓝色内存空间,ax
- CPU首先会将红色内存空间的值放到AX寄存器中:
通用寄存器分类
AX
、BX
、CX
、DX
这4个通用寄存器都是16位的,如下图所示
- 上一代8086的寄存器都是8位的,为了保证兼容,
AX
、BX
、CX
、DX
都可分为2个独立的8位寄存器来使用H
代表高位寄存器L
代表低位寄存器
字节与字
- 在汇编的数据存储中,有2个比较常用的单位
字节
:byte
,1个字节由8bit
组成,可以存储在8位寄存器
中字
:word
,1个字由2个字节
组成,这2个字节
分别称为字的高字节
和低字节
- 比如数据20000(4E20H,0100111000100000B),高字节的值是78,低字节的值是32
- 1个字可以存在1个16位寄存器中,这个字的高字节、低字节分别存储在这个寄存器的高8位寄存器、低8位寄存器中
- 在汇编的数据存储中,有2个比较常用的单位
段寄存器
- 8086在访问内存时要由相关部件提供内存单元的段地址和偏移地址,送入地址加法器合成物理地址
- 是什么部件提供段地址?段地址在8086的段寄存器中存放
- 8086有4个段寄存器:
CS
、DS
、SS
、ES
,当CPU需要访问内存时由这4个段寄存器提供内存单元的段地址CS (Code Segment)
:代码段寄存器DS (Data Segment)
:数据段寄存器SS (Stack Segment)
:堆栈段寄存器ES (Extra Segment)
:附加段寄存器
- 每个段寄存器的具体作用是?
CS和IP
指令的执行过程
指令和数据
jmp指令
Debug
下载
debug.exe
、下载安装DOSBox
。1 2 3 4 5 6
把debug.exe放到D盘,打开DOSBox z:\>mount c d:/ Device C is mounted as local directory d:/ z:\>c: c:\>debug _
Debug
的常用功能- 用Debug的
R命令
查看、改变CPU寄存器的内容。 - 用Debug的
D命令
查看内存中的内容 - 用Debug的
E命令
改写内存中的内容 - 用Debug的
U命令
将内存中的机器指令翻译成汇编指令 - 用Debug的
T命令
执行一条指令 - 用Debug的
A命令
以汇编指令的格式在内存中写入一条机器指令 q命令
: 退出Debugp命令
: 类似于step over
(t命令
类似于step into
),可用于跳过loop循环g命令
: 跳过签名的代码,停留到指定的代码位置
- 用Debug的
R命令
- 输入
r
可以查看所有寄存器的只 - 输入
r 寄存器名称
可以修改寄存器的值 - 输入
r ax
将ax寄存器
的只改为0100H
- 输入
D命令
- 输入
d
可以查看内存中的内容 - 输入
d 段地址:偏移地址
查看特定位置的内存数据 - 输入
d 段地址:起始偏移地址 结尾偏移地址
查看特定位置和特定范围的内存地址 - 输入
d 偏移地址
、d 起始偏移地址 结尾偏移地址
,会将DS的内容作为段地址
- 输入
E命令
- 输入
e 段地址:偏移地址 数据串
修改特定位置的内存数据 - 输入
e段地址:偏移地址
后按enter也可以修改特定位置的内存数据,数据之间用空格隔开
- 输入
U命令
- 输入
u
、u 段地址:偏移地址
可以将内存中的内容翻译为对应的汇编指令 - 由3部分组成
- 最左边一列: 是指令的地址
段地址:偏移地址
- 中间一列: 是指令对应的机器指令
- 最右边一列: 是汇编指令
- 最左边一列: 是指令的地址
- 输入
A命令
- 输入
a
、a 段地址:偏移地址
可以从某位置开始写入汇编指令
- 输入
DS和[address]
CPU要读写一个内存单元时,必须要先给出这个内存单元的地址,在8086中,内存地址由段地址和偏移地址组成
8086中有一个DS段寄存器,通常用来存放要访问数据的段地址
1 2 3
mov bx, 1000H mov ds, bx mov al, [0]
- 上面3条指令的作用将10000H(1000:0)中的内存数据复制到al寄存器中
- mov al, [address]的意思将DS:address中的内存数据复制到al寄存器中
- 由于al是8位寄存器,所以是将一个字节的数据复制给al寄存器
8086不支持将数据直接送入段寄存器中,
mov ds, 1000H
是错误的的
大小端
- 大端模式: 是指数据的高字节保存在内存的低地址中,而数据的低字节保存在内存的高地址中(高低\低高)
- 小端模式: 是指数据的高字节爆粗在北村的高地址中,二数据的低字节保存在内存的低地址中(高高\低低)
1 2 3 4 5 6 7 8 9 10
16bit宽的数0x1234在CPU内存中的存放方式,假设从地址0x4000开始存放 内存地址 小端模式 大端模式 0x4000 0x34 0x12 0x4001 0x12 0x34 32bit宽的数0x12345678,CPU内存的存放方式 内存地址 小端模式 大端模式 0x4000 0x78 0x12 0x4001 0x56 0x34 0x4002 0x34 0x56 0x4003 0x12 0x78
mov指令
1 2 3 4 5 6
mov 寄存器, 数据 比如: mov ax, 8 mov 寄存器, 寄存器 比如: mov ax, bx mov 寄存器, 内存单元 比如: mov ax, [0] mov 内存单元, 寄存器 比如: mov [0], ax mov 段寄存器, 寄存器 比如: mov ds, ax mov 寄存器, 段寄存器 比如: mov ax, ds
add和sub指令
1 2 3 4 5 6 7 8
add 寄存器, 数据 比如: add ax, 8 add 寄存器, 寄存器 比如: add ax, bx add 寄存器, 内存单元 比如: add ax, [0] add 内存单元, 寄存器 比如: add [0], ax sub 寄存器, 数据 比如: add ax, 8 sub 寄存器, 寄存器 比如: add ax, bx sub 寄存器, 内存单元 比如: add ax, [0] sub 内存单元, 寄存器 比如: add [0], ax
栈
- 栈: 是一种具有独特的访问方式的存储空间(后进先出,
last in out first
) - 8086会将CS作为代码段的段地址,将CS:IP指向的指令作为吓一跳需要去除执行的指令
- 8086会将DS作为数据段的段地址,mov ax, [address]就是去除DS:address的内存数据放到ax寄存器中
- 8086会将SS作为栈段的段地址,任意时刻,SS:SP指向栈顶元素
- 8086提供了PUSH(入栈)和POP(出栈)指令来操作站段的数据
- 比如push ax是将ax的数据入栈,pop ax 是将栈顶的数据送入ax。
- 栈: 是一种具有独特的访问方式的存储空间(后进先出,
栈顶超界
- 上面描述了执行push、pop指令是,发生的栈顶超界问题,可以看到,当栈满的时候再使用push指令入栈,或栈空的时候再使用pop指令出栈,都将发生栈顶超界问题
- 栈顶超界是危险的,因为我们既然将一段空间安排为栈,那么在栈空间之外的空间里很可能存放了具有其他用途的数据、代码等,这些数据、代码可能是我们自己程序中的,也可能是别的程序中的。但是由于我们在入栈出栈是的不小心,而将这些数据、代码意外地改写,将会引发一连串的错误。
push和pop
push
和pop
指令的格式可以是如下形式1 2 3 4 5 6 7 8 9 10 11
push 寄存器 将一个寄存器中的数据入栈 pop 寄存器 出栈,用一个寄存器介绍出栈的数据 push 段寄存器 将一个段寄存器中的数据入栈 pop 段寄存器 出栈,用一个段寄存器接受出栈的数据 push 内存单元 将一个内存子单元出的子入栈。(注意: 栈操作都是以 字为单位) pop 内存单元 出栈,用一个内存单元接收出栈的数据 如: mov ax, 1000H mov ds, ax push [0] pop [0]
段总结
- 我们用一个段存放数据,那将它定义为
数据段
;我们用一个段存放代码,那将它定义为代码段
;我们可以用一个段当作栈,那将它定义为栈段
- 对于数据段,将它的段地址放在DS中,用
mov
、add
、sub
等访问内存单元的指令时,CPU就将我们定义的数据段中的内容当作数据来访问;对于代码段,将它的段地址放在CS中,将段中第一条指令的偏移地址放在IP中,这样CPU就将执行我们定义的代码段中的指令;对于栈段,将它的段地址放在SS中,将栈顶单元的偏移地址放在SP中这样CPU在需要进行栈操作,比如执行push、pop指令等,就将我们定义的站短当作栈空间来用
- 我们用一个段存放数据,那将它定义为
汇编语言的组成
1 2 3 4 5 6 7 8
assume cs:code code segment mov ax, 1122h mov bx, 3344h mov ah, 4c00h int 21h code ends end
伪指令 - segmen
t、ends
、end
中断
- 中断时由于软件的或硬件的信号,使得CPU暂停当前的任务,转而去执行另一段子程序。
- 在程序运行过程中,系统出现了一个必须由于CPU立即处理的情况,此时,CPU暂时中止当前程序的执行转而处理这个新情况的过程叫做中断。
- 中断分类
- 硬中断: 由外部设备(比如网卡、硬盘)随机引发的
- 软中断: 由执行终端指令产生的,可以通过程序控制触发
- 可以通过指令int n产生中断
- n是中断码,内存中由一张终端向量表,用来存放中断码对应中断处理程序的入口地址。
- CPU在接收到中断信号后,暂停当前正在执行的程序,跳转到中断码对应的中断向量表地址处,去执行中断处理程序。
- 常见中断
int 10h
用于执行BIOS中断int 3
是断点中断
,用于调试程序int 21h
用于执行DOS系统功能条用,AH寄存器存储功能号
打印Hello World
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 27 28 29 30 31 32
;提醒开发者每个段的含义,写不写不影响程序 assume cs:code, ds:data ;数据段 data segment db "Hello World!$" string db "Hello World!$" data ends ;代码段 code segment start: ;设置ds的值 mov ax, data mov ds, ax ;打印字符串 mov dx, 0h mov ah, 9h int 21h ;打印 mov dx, offset string mov ah, 9h int 21h ;退出程序 mov ax, 4c00h int 21h code ends ;编译结束,start是程序入口 ;start所在的段就是代码段 ;所以cs的值就是code段的段地址 ;相当于cs的值已经自动设置完毕 end start
通过栈交换两个值
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 27 28 29 30 31 32 33
assume cs:code, ds:data, ss:stack ;栈段 stack segment db 10 dup(8) stack ends ;数据段 data segment db 20 dup(9) data ends ;代码段 code segment start: ;手动设置ss和ds mov ax, stack mov ss, ax mov ax, data mov ds, ax mov ax, 1122h mov bx, 3344h ;使用栈 mov sp, 10 push ax push bx pop ax pop bx ;退出 mov ax, 4c00h int 21h ;产生中断, 21执行DOS系统功能调用 code ends end start
call和ret指令
call
标号- 将下一条指令的偏移地址入栈后
- 转到标号出执行指令
ret
: 将栈顶的值出栈,赋值给ip
函数使用
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 27 28 29 30 31 32 33 34 35 36
assume cs:code, ds:data, ss:stack ;栈段 stack segment db 10 dup(8) stack ends ;数据段 data segment db 20 dup(9) string db "HELLO WORLD!$" data ends ;代码段 code segment start: ;手动设置ss和ds mov ax, stack mov ss, ax mov ax, data mov ds, ax ;业务逻辑 call print ;退出 mov ax, 4c00h int 21h ;打印字符串 print: ;打印,ds:dx告知字符串地址 mov dx, offset string mov ah, 9h int 21h ret code ends end start
返回值
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 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
assume cs:code, ds:data, ss:stack ;栈段 stack segment db 100 dup(0) stack ends ;数据段 data segment res dw 0 ;全局变量 db 100 dup(0) data ends ;代码段 code segment start: ;手动设置ss和ds mov ax, stack mov ss, ax mov ax, data mov ds, ax ;业务逻辑 call mathFunc3 ;1:mov bx, [0] ;2:mov bx, res mov bx, ax ;退出 mov ax, 4c00h int 21h ;返回2的3次方 ;返回值放到ax寄存器中 mathFunc3: mov ax, 2 add ax, ax add ax, ax ret ;返回值放到全局变量res中 mathFunc2: mov ax, 2 add ax, ax add ax, ax mov res, ax ret ;返回值放到ds:0中 mathFunc1: mov ax, 2 add ax, ax add ax, ax mov [0], ax ret code ends end start
传递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 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80
assume cs:code, ds:data, ss:stack ;栈段 stack segment db 100 dup(0) stack ends ;数据段 data segment res dw 0 ;全局变量 db 100 dup(0) data ends ;代码段 code segment start: ;手动设置ss和ds mov ax, stack mov ss, ax mov ax, data mov ds, ax ;业务逻辑 push 1122h push 3344h call sum3 add sp, 4 ;移到栈顶,栈(释放) mov bx, ax ;栈平衡: 函数调用前后的栈顶指针要一致,如果不平衡,栈空间迟早被用完. ;外平栈 add sp, 4 ;内平栈 ret 4 push 2233h push 4455h call minus add sp, 4 mov bx, ax ;sum2 ;mov word ptr [0], 10h ;mov word ptr [2], 20h ;call sum2 ;mov bx, ax ;sum1 ;mov cx, 10h ;mov dx, 20h ;call sum1 ;mov bx, ax ;退出 mov ax, 4c00h int 21h ;返回值放a寄存器 ;传递两个参数 ;相减 minus: mov bp, sp mov ax, ss:[bp+2] sub ax, ss:[bp+4] ret ;传递2个参数(放到栈中) ;相加 sum3: ;访问栈中的参数 mov bp, sp mov ax, ss:[bp+2] add ax, ss:[bp+4] ret ;传递2个参数(分别放到ds:0, ds:2) sum2: mov ax, [0] add ax, [2] ret ;分别放在cx,dx中 sum1: mov ax, cx add ax, dx ret code ends end start
函数调用的本质
- 参数:
push
参数值 - 返回值: 返回值存放到
ax
- 栈平衡
- 参数:
局部变量
1 2 3 4 5 6
int sum(int a, int b) { int c = 3; int d = 4; int e = c + d; return a + b + e }
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 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83
assume cs:code, ds:data, ss:stack ;栈段 stack segment db 100 dup(0) stack ends ;数据段 data segment res dw 0 ;全局变量 db 100 dup(0) data ends ;代码段 code segment start: ;手动设置ss和ds mov ax, stack mov ss, ax mov ax, data mov ds, ax ;业务逻辑 push 1 push 2 call sum add sp, 4 ;移到栈顶,栈清空 mov bx, ax ;退出 mov ax, 4c00h int 21h ;传递2个参数(放到栈中) ;相加 sum: ;保护bp push bp ;访问栈中的参数 mov bp, sp ;局部变量,减10 sub sp, 10 ;预留10个字节的空间给局部变量 ;保护可能会用到的寄存器 push si push di push bx ;给局部变量空间填充int 3 (cccc), int 3->中断 ;stosw的作用,将ax的值拷贝到es:di中,di的值同时 +2 mov ax, 0cccch mov bx, ss mov es, bx mov di, bp sub di, 10 ;cx决定了stosw的执行次数,rep一次cx减1 mov cx, 5 ;rep的作用,重复执行某个指令,次数有cx决定 rep stosw ;------ 业务逻辑 -------- ;定义两个局部变量 mov word ptr ss:[bp-2], 3 mov word ptr ss:[bp-4], 4 ;int e = c + d mov ax, ss:[bp-2] add ax, ss:[bp-4] mov ss:[bp-6], ax ; a + b + e mov ax, ss:[bp+2] add ax, ss:[bp+4] add ax, ss:[bp-6] ;------ end --------- ;恢复寄存器的值 pop bx pop di pop si ;恢复sp mov sp, bp ;恢复bp pop bp ret code ends end start
- 函数的调用流程(内存)
1.push 参数
2.push 函数的返回地址
3.push bp,保留bp之前的值,方便以后恢复
4,mov bp, sp,保留sp之前的值,方便以后恢复
5.sub sp, 空间大小,分配空间给局部变量
6.保护可能要用到的寄存器
7.使用cc(int 3 中断)填充局部变量的空间
8.执行业务逻辑
9.恢复寄存器之前的值
10.mov sp, bp,恢复sp之前的值
11.pop bp,恢复bp之前的值
12.ret,将函数的返回地址出栈,执行下一条指令
13.恢复栈平衡 add sp, 参数所占的空间
栈帧(Stack Frame Layout)
JCC跳转指令