0%

介绍一下操作系统内存管理

操作系统每个系统都有自己的虚拟内存,我们所写的程序不会直接与物理内存打交道

虚拟内存的好处:

  • 虚拟内存使进程的运行内存超过物理内存大小,因为程序运行符合局部性原理,CPU访问内存会有很明显的重复访问的倾向性,对于那些没有经常被使用到的内存,我们可以把他们换出到物理内存之外
  • 由于每个进程都有自己的页表,所以每个进程的虚拟内存空间就是相互独立的。进程也无法访问其他进程的页表,所以页表是私有的,这就解决了多进程之间地址冲突的问题
  • 页表项中除了物理地址之外,还有一些标记属性的比特,在内存访问层面为操作系统提供了更好的安全性

linux通过对内存分页的方式进行内存管理。
虚拟地址和物理地址之间通过页表来进行映射

页表是存放在内存里的,内存管理单元(MMU)来进行虚拟内存地址和物理内存地址的转换工作

当进程访问的虚拟地址在内存中查不到时,系统会产生缺页异常,进入系统的内核空间分配物理内存,更新进程页表,最后返回用户空间,恢复进程运行。

什么是虚拟内存和物理内存

  • 虚拟内存:是操作系统为每个程序 “虚拟” 出来的独立地址空间,程序运行时会认为自己拥有一块连续的、专属的内存区域,但这块区域并不完全对应物理内存,而是由操作系统通过 “内存 - 磁盘” 的交换机制来管理
  • 物理内存:计算机的实际内存硬件,是处理器可以直接访问的存储空间,用于临时存储正在运行的程序、数据和操作系统指令

讲一下页表

  • 页表是虚拟内存分页机制的核心数据结构,它的核心作用是建立 “虚拟页面” 到 “物理页帧” 的映射关系
  • 页表的结构由多个页表项组成,每个页表项对应一个虚拟页面,包含这些关键信息:一是物理页号,直接指明虚拟页映射到物理内存的具体页帧;二是状态标志位,比如 “存在位” 标记该页是否在物理内存中,“修改位” 记录页面是否被修改过。
  • 当程序访问虚拟地址时,首先会被拆成 “虚拟页号” 和 “页内偏移”;MMU会用 VPN 作为索引查页表,找到对应的页表项;如果存在,就用页表项里的 物理页号 结合页内偏移,算出物理地址,完成访问;如果缺页,就触发操作系统的缺页异常处理,从磁盘加载页面到物理内存,更新页表后再继续执行

页表存在的作用:

  • 它让每个进程拥有独立的虚拟地址空间,保证了进程隔离;
  • 通过映射实现物理内存的灵活分配,提高了内存利用率;
  • 结合缺页机制和磁盘交换,让程序可以运行在比物理内存更大的地址空间中

讲一下段表

  • 段表是分段内存管理机制的核心数据结构,核心作用是实现 “逻辑段” 到 “物理内存块” 的映射。
  • 段表的结构由多个段表项组成,包含三个核心信息:一是段基址,也就是这个段在物理内存中的起始地址;二是段长度,记录该段的总大小,用于判断访问是否越界;三是属性位,比如读写执行权限、存在位
  • 当程序访问某个虚拟地址时,首先会拆成 “段号” 和 “段内偏移”;接着用段号查段表,找到对应的段表项后,先做越界检查 —— 如果段内偏移超过段长度,就触发 “段越界异常”;如果没问题,就用段基址加上段内偏移,得到最终的物理地址,完成访问。如果段表项的存在位为 0,则会触发 “缺段异常”,由操作系统从磁盘加载段到物理内存,更新段表后再继续执行

段表的实际意义:

  • 通过逻辑分段和权限控制,保障了内存安全
  • 简化程序开发,程序员无需关心物理内存布局,只需按功能组织代码和数据;

页表与段表的区别

  • 分页是按固定大小划分内存,更侧重物理内存的高效利用,但逻辑上不够直观
  • 分段是按逻辑功能划分,段的大小不固定,优势在于逻辑隔离清晰,权限控制更细粒度,但可能产生外部碎片
  • 简单说,页表解决 “物理内存怎么高效用”,段表解决 “逻辑功能怎么安全隔”

虚拟地址怎么转化为物理地址

当程序访问虚拟地址时,首先会被拆成 “虚拟页号” 和 “页内偏移”;MMU会用 VPN 作为索引查页表,找到对应的页表项;如果存在,就用页表项里的 物理页号 结合页内偏移,算出物理地址,完成访问;如果缺页,就触发操作系统的缺页异常处理,从磁盘加载页面到物理内存,更新页表后再继续执行

程序的内存布局是什么样的

用户的内存空间,从低到高分为六种不同的内存段:

  1. 代码段:包括二进制代码
  2. 数据段:包含已初始化的静态常量和全局变量
  3. BSS段:包含未初始化的静态变量和全局变量
  4. 堆段:包含动态分配的内存,从低地址向上增长
  5. 文件映射段:包括动态库,共享内存等
  6. 栈段:包含局部变量和调用上下文的函数等

堆和栈的区别

  • 分配方式:堆是动态分配内存的,由程序员手动申请和释放,通常用于存储动态数据结构或对象。栈是静态分配内存的,由编译器静态申请或释放,用于存储函数的局部变量和函数调用信息
  • 内存管理:堆需要程序员手动管理内存的分配和释放,如果管理不当可能导致内存泄漏或内存溢出。栈由编译器自动管理内存,遵循后进先出的原则,变量的生命周期由作用域决定,函数调用时分配内存,函数结束时释放内存
  • 大小和速度:堆通常比栈大,内存空间较大,动态分配和释放内存需要时间开销。栈大小有限,通常比较小,内存分配和释放速度较快,因为是编译器自动管理

fork()会复制哪些东西

  • fork阶段会复制父进程的页表
  • fork之后,如果发生了写时复制,就会复制物理内存

介绍写时复制(copy on write)

主进程执行fork()时,操作系统会把主进程的页表复制一份给子进程,这个页表记录着虚拟地址和物理地址的映射关系,而不会复制物理内存,也就是说虚拟空间不同,物理地址相同

这样能节约物理内存资源,页表对应的页表项会标记该物理内存的权限为只读

当父进程在向这个内存发起写操作时,CPU会触发写保护中断,这个写保护中断是由于违反权限导致的,然后操作系统会在【写保护中断处理函数】里进行物理内存复制,并重新设置其内部的映射关系,将父子进程的内存权限设置为可读写,最后才对内存进行写操作。这个过程被称为写时复制。

写时复制:在触发写操作时,操作系统才会去复制物理内存,这是为了防止fork创建子进程时由于物理内存数据复制时间过长导致父进程长时间阻塞的问题。

写时复制节省了什么资源

节省了物理内存的资源,因为fork时,子进程不需要复制父进程的物理内存,避免了不必要的内存复制开销,子进程只需要复制父进程的页表,这时候父子进程指向相同的物理内存

只有当父子进程任意一方对这片共享内存有修改操作,才会触发写时复制,这时候才会复制发生修改操作的物理内存地址

malloc 1KB和1MB有什么区别

  • 如果用户分配的内存小于1KB,则用brk()申请内存
  • 如果大于128KB,则用mmap()申请内存

介绍一下brk()和mmap()

malloc申请内存时,拥有两种方式向操作系统申请堆内存:

  • 通过brk()系统调用从堆申请内存
  • 通过mmap()在文件映射区域分配内存

对于brk(),即把堆顶的指针向高地址移动,获得新的内存空间

mmap()用于将文件或设备的地址空间直接映射到进程的虚拟地址空间。通过这种映射,进程可以像访问内存一样直接读写文件 / 设备,无需传统的 read/write 系统调用,从而显著提升 IO 效率。
调用 mmap() 后,内核仅分配虚拟内存,不会立即将文件数据加载到物理内存,首次访问触发缺页异常,减少内存浪费

  • 减少数据拷贝:传统文件读写需要经过 “磁盘→内核缓冲区→用户缓冲区” 两次拷贝,而 mmap 直接映射到用户空间,仅需一次磁盘到内存的拷贝。
  • 共享性:支持多进程共享映射区域,实现进程间高效通信。
  • 灵活性:可通过参数指定映射模式、权限等。

操作系统内存不足的什么会发生什么

当没有空闲的物理内存时,内核就开始内存回收工作,回收的方式包括直接内存回收和后台内存回收

  • 后台内存回收:在内存紧张的时候,会唤醒kswapd内核线程来回收内存,这个回收内存的过程是异步的,不会阻塞进程的执行
  • 直接内存回收:如果后台异步回收跟不上进程申请的速度,就开始直接回收,这个回收内存的过程是同步的,会阻塞进程执行

如果直接回收内存后,空闲内存仍然无法满足物理内存的申请,那就会触发OOM机制

OOM killer机制会根据算法选择一个占用物理内存较高的进程,然后将其杀死,以便释放内存资源,如果物理内存仍然不足,会继续这样过程,直到释放足够的内存位置

回收内存的类型和回收方式

  • 文件页:内核缓存的磁盘数据和内核缓存的文件数据都叫文件页。大部分文件页都可以直接释放内存,以后有需要时再从磁盘重新读取就可以。而那些被应用程序修改过,暂时没有写入磁盘的数据,就得先写入磁盘,然后才能进行内存释放。所以回收干净内存页的方式是直接释放内存,回收脏页的方式是先写回磁盘再释放
  • 匿名页:这部分内存没有实际载体(堆、栈等),这部分内存可能还要再访问,所以不能直接释放内存,他们的回收方式是通过linux的swap机制,swap会把不常用的内存先写到磁盘中,然后释放内存。再次访问这些内存,直接从磁盘读入就可以了

文件页和匿名页的回收都基于LRU算法,也就是优先回收不常访问的内存。

页面置换算法有哪些

最佳页面置换算法

原理:置换在未来最长时间不访问的页面

该算法实现需要计算内存中每个页面下一次访问时间,然后比较,选择未来最长时间不访问的页面

实际系统中无法实现,因为程序访问页面是动态的,无法预知每个页面在下一次访问前的等待时间

所以最佳页面置换算法是为了衡量算法效率,算法效率越接近于该算法的效率,说明越高效

先进先出置换算法(FIFO)

原理:选择在内存中滞留时间最长的页面进行置换

最近最久未使用算法(LRU)

选择最长时内存间没有被访问的算法进行置换,也就说该算法假设已经很久没有使用的页面很有可能在未来较长一段时间仍然不会被使用

时钟页面置换算法

原理:把页面都保存在一个类似时钟的环形链表中,一个指针指向最老的页面。

当发生缺页中断:

  • 如果他的访问位是0就淘汰该页面,并把新页面插入这个位置,然后把表指针前移一个位置
  • 如果访问位是1就清除访问位,并把表指针前移一个位置,重复这个过程直到找到访问位为0的页面

最不常用算法(LFU)

原理:对每个页面设置一个访问计数器,每当页面被访问时,该页面的访问计数器就累加1,发生缺页中断时,选择访问次数最少的页面并将其淘汰

LFU只考虑了频率问题,没有考虑时间问题,比如有些页面过去访问频率很高但现在不再访问了,而当频繁访问的页面由于没有这些页面访问的频率高,在发生缺页中断时就可能先淘汰刚开始频繁访问的页面

可以定期减少访问次数,比如发生时间中断时,把过去时间访问的页面访问次数除以2就可以,也就是说,随着时间流逝,过去访问频率高的页面的访问次数会逐渐减少,相当于加大了被置换的概率

概述

转移指令就是可以控制CPU执行指令顺序的指令。

8086CPU的转移行为分为:

  • 段内转移——只修改IP的值
    • 短转移:IP的变化范围为-128—+127
    • 近转移:IP的变化范围为-32768—+32767
  • 段间转移——同时修改CS和IP的值

8086CPU的转移指令分为:

  • 无条件转移指令
  • 条件转移指令
  • 循环指令
  • 过程
  • 中断

无条件转移指令(JMP)

1、段内转移
段内转移又可分为下面三种形式:

  • 段内直接短转移
  • 段内直接近转移
  • 段内间接转移

2、段间转移
段间转移又可分为下面二种形式:

  • 段间直接转移
  • 段间间接转移

段内转移指令的转移范围在JMP指令所在的段内,只需将IP的值加上转移目的地的偏移量就可控制指令的转移

  • 只需修改IP的值
  • 转移目的地址是由JMP指令到目的地的偏移量决定的
  • 偏移量为8位或16位的带符号数(8位偏移量的范围为-128—127;16位的偏移量的范围为-32768—32767,其中负数为向前转移,正数为向后转移)

段内直接短转移

指令格式: JMP SHORT 标号
执行操作: (IP)=(IP)+8位偏移量

段内直接近转移

指令格式: JMP NEAR PTR 标号
执行操作: (IP)=(IP)+16位偏移量

注:上述两种转移指令都可以写作简化格式
JMP 标号

段内间接转移

指令格式:JMP WORD PTR OPR JMP 寄存器
注:OPR为除立即数寻址方式以外的任一种寻址方式。

1
2
3
4
JMP    CX   ;     ;(IP)=(CX)
JMP WORD PTR [BX]; ;(IP)=([BX])
JMP WORD PTR DS:[0];
JMP WORD PTR [BX][SI]

段间转移

段间转移时,程序将从一个代码段转移到另一个代码段中支执行,转移的目的地址由段地址和偏移地址构成,因此段间转移需要同时修改CS和IP的值。
需同时修改CS和IP的值;
偏移量由段地址和偏移地址组成

段间直接转移

指令格式: JMP FAR PTR 标号
执行操作:
(IP)=标号的段内偏移地址
(CS)=标号所在段的段地址
例: JMP FAR PTR S

段间间接转移

指令格式: JMP DWORD PTR [ ]
执行操作:用确定的内存单元中的双字的低字修改IP,高字修改CS的值。
例:
JMP DWORD PTR [BX]
JMP DWORD PTR [BX][DI]

注:JMP 1000:0020这种格式只能用于DEBUG中,在源程序中出现时编译器不能编译,会报错。

取值运算符OFFSET

功能:取得一个标号的偏移地址。
举例: MOV AX,OFFSET S

条件转移指令JCXZ

格式: JCXZ 标号
功能: 当CX=0时转移到标号处执行。
注:所有的条件转移都为段内短转移

循环指令LOOP

功能:当CX≠0时转移到标号处执行。
注:所有的循环指令都是段内短转移

例题

已知DS=1000H,ES=2000H,SS=3800H,SI=1010H,BX=0200H,BP=0020H,请指出下列指令的源操作数字段是什么寻址方式?源操作数字段的物理地址是多少?

1
2
3
4
5
6
7
8
1)MOV  AL,[1000H]
2)MOV AH,SI
3)MOV AX,[BP]
4)MOV AL,BYTY PTR [BX][SI]
5)ADD AX,[BP+10]
6)ADD AL,ES:[BX]
7)MOV AL,[BX][SI+8]
8)MOV AL,ES:[BX+SI]

(1)直接寻址 DS*16+1000H=11000H
(2)寄存器寻址 无(寄存器操作不涉及内存地址)
(3)寄存器间接寻址 SS × 16 + BP
(4)基址变址寻址 DS × 16 + BX + SI
(5)寄存器相对寻址 SS × 16 + BP + 10
(6)寄存器间接寻址 ES × 16 + BX
(7)基址变址相对寻址 DS × 16 + BX + SI + 8
(8)基址变址寻址 ES × 16 + BX + SI

补全下面程序,使该程序在运行中将S处的一条指令复制到S0处。

1
2
3
4
5
6
7
8
9
10
11
assume cs:code
code segment
s: mov ax,bx
mov si, offset s
mov di, offset s0
--------
--------
s0: nop
nop
code ends
end s
1
2
mov ax, [si]     
mov [di], ax

判断下列转移指令的转移方式:

1
2
3
4
5
6
7
Jmp  word  ptr  [bx+8 ] ; 段内间接转移
Jmp far ptr s(S为标号) ;段间直接转移
Jmp bx ; 段内间接转移
Jmp near ptr s ; 段内直接近转移
Jmp dword ptr [bx] ; 段间间接转移
Jmp short ptr s ; 段内直接短转移
Jmp word ptr [bx+si] ;段内间接转移

若要使程序中的JMP
指令执行后,CS:IP指向程序的第一指令,在data段中应定义哪些数据?

1
2
3
4
5
6
7
8
9
10
11
assume   cs:code
data segment
?
data ends
code segment
start: mov ax,data
mov ds,ax
mov bx,0
jmp word ptr [bx+1]
code ends
end start
1
2
db  0        ; 偏移地址0(占位,不影响)
dw 0000H

补全程序,使jmp指令
执行后,CS:IP指向
程序的第一条指令。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
2、程序如下:
assume cs:code
data segment
dd 12345678h
data ends
code segment
start: mov ax,data
mov ds,ax
mov bx,0
mov [bx],---
mov [bx+2],---
jmp dword ptr ds:[0]
code ends mov bx,0
end start
1
2
mov  [bx], 0h 
mov [bx+2], cs

bx、si、di、bp

在8086CPU中,只能使用这4个寄存器寻址内存单元。
在[ ]中,这4个寄存器可以单个出现,或只能以下列组合出现:

  • bx和si、di
  • bp和si、di

只要在[ ]中使用寄存器bp,段地址就默认在ss中。

机器指令处理的数据所在位置

数据处理的方式:读、写、运算
数据类型:指令、数值
数据所在位置:CPU内部、内存、端口

汇编语言中(存取)数据位置的表达

立即数:要处理的数据直接包含在指令中
寄存器:要处理的数据在指令中的寄存器中
段地址(SA):偏移地址(EA):要处理的数据在内存中

只有内存单元才有地址:SA、EA和PA

指令要处理的数据长度

8086CPU可以处理的数据长度:

  • 8位 byte 字节型数据
  • 16位 word 字数据

汇编语言中的处理方法:

  • 通过寄存器指明要处理的数据长度
  • 在没有寄存器的情况下用 word ptr/byte ptr指明数据长度
  • 其他方法(默认长度)例:push [1000h]

寻址方式的综合应用

一般来说,我们可以用[bx+idata+si]的方式来访问结构体中的数据。
用bx定位整个结构体,用idata定位结构体中的某一个数据项,用 si 定位数组项中的每个元素 。
为此,汇编语言提供了更为贴切的书写方式。
如:[bx].idata、[bx].idata[si]

div除法指令

格式:div op(内存单元、寄存器
说明:

  • 被除数
    • 16位放在AX
    • 32位DX放高16位,AX放低16位
  • 除数
    • 放在OP 中
  • 结果
    • 8位除数:商存放在AL中,余数放在AH
    • 16位除数:商存放在AX,余数放在DX

注意:被除数长度必须是除数长度的两倍!

定义重复变量伪指令dup

格式:
DB n DUP (重复的数据)

例:
DATA1 DB 10 DUP (0)
定义DATA1为10个“0”组成的字节变量
DATA2 DW 2 DUP (?)
定义DATA2为2个不确定数值的字变量
DATA3 DB 4 DUP (1,2 DUP(50H)
定义DATA3为(1,50H,50H)重复4次共12个字节的字节变量

and和or指令

“与”运算指令AND

格式:AND OP1,OP2
功能:对OP1、OP2按位相“与”
说明:常用于使指定位数置0的操作中。

1
2
MOV   AL,63H       ;输入0110 0011
AND AL,0FH ;使AL高四位为0,低四位不变

逻辑“或”指令OR

格式:OR OP1,OP2
功能:对OP1、OP2按位相“或”。
说明:通常用于使指定位数置1的操作。

举例:使AL中的最低两位置1。

1
OR   AL,03H       

ASCII码

在ASCII编码中,将常用的128个字符用八位二进制数(00000000—01111111)表示,其中最高一位为0。

符号 十进制范围
0~9 48~57
A~Z 65~90
a~z 97~122

以字符形式给出的数据

用一对单引号‘….’括起的内容作为字符处理

1
2
3
4
data segment
db ‘unIX’ ;Db 75h,6eh,49h,58h
db ‘foRK’ ;Db 66h,6fh,52h,4bh
data ends
1
mov  bl,’b’     ;Mov bl, 62h

大小写转换问题

补全程序,将DATA中的第一个字符串转化为大写,将第二个字符串转化为小写

1
2
3
4
5
ASSUME CS:CODE,DS:DATA
DATA SEGMENT
DB ‘BaSiC’
DB ‘iNfOrMaTiOn’
DATA ENDS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Start:    
mov ax,data
mov ds,ax
mov bx,0
mov cx,5
S1:
mov al,[bx]
and al,0dfh
mov [bx],al
inc bx
loop s1
mov bx,5
mov cx,11
S2:
mov al,[bx]
or al,20h
mov [bx],al
inc bx
loop s2
mov ax,4c00h
int 21h

转换为大写可以and 0dfh,转换成小写or 20h

[bx+idata]

MOV AX,[BX]
MOV AX,[BX+200]

用[bx+idata]方式处理数组

补全程序,将DATA中的第一个字符串转化为大写,将第二个字符串转化为小写。

1
2
3
4
5
ASSUME CS:CODE,DS:DATA
DATA SEGMENT
DB ‘BaSiC’
DB ‘MinIX’
DATA ENDS
1
2
3
4
5
6
7
8
9
10
11
12
13
Start:    mov  ax,data
mov ds,ax
mov bx,0
mov cx,5
S1:
mov al,[bx]
and al,0dfh
mov [bx],al
mov al,[bx+5]
or al,20h
mov [bx+5],al
inc bx
loop s1

SI和DI

SI:源变址寄存器
DI:目的变址寄存器
SI和DI只能用作16位寄存器,常用于对内存单元的寻址,功能与BX寄存器相近。

用寄存器SI和DI实现字符串‘welcome to masm!’复制到它后面的数据区。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
assume cs:code,ds:data
data segment
db ‘welcome to masm!’
db ‘…………….’
data ends
code segment
start:
Start:
mov ax,data
mov ds,ax
mov si,0
mov di,16
mov cx,8
S:
mov ax,[si]
mov [di],ax
add si,2
add di,2
loop s
mov ax,4c00h
int 21h
code ends
end start

[bx+si]和[bx+di]

MOV AX,[BX+SI]
MOV AX,[BX][DI]

[bx+si+idata]和[bx+di+idata]

MOV AX,[BX+SI+100]
MOV AX,[BX+DI+100]
MOV AX,[BX+100+SI]
MOV AX,[BX+100+DI]

在代码段中使用数据

计算以下8个数据的和,结果保存在AX中:0123H,0456H,0789H,0ABCH,0DEFH,0FEDH,0CBAH,0987H

思路1:

1
2
3
4
5
6
7
MOV   AX,0
ADD AX,0123H
ADD AX,0456H
ADD AX,0789H
......
ADD AX,0CBAH
ADD AX,0987H

思路二:

思路2:

1
2
3
4
5
6
7
8
9
MOV   AX, XXXXH
MOV DS,AX
MOV BX,0
MOV CX,n
MOV AX,0
S:
ADD AX,[BX]
ADD BX,2
LOOP S

定义数据伪指令:
DW—字型数据
DB—字节型数据
DD—双字数据
格式:
DW(DB) 数据1,数据2,数据3,……
num1 db 10hordb 10h

END标号
标号—为程序中第一条要执行的指令的标号。
在编译中向编译器提供程序的入口地址和结束地址。

在代码段中使用栈

利用堆栈,编程将程序中定义的数据逆序存放。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Assume cs:code
Code segment
Dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h
Dw 0,0,0,0,0,0,0,0
Start:
mov ax,cs
mov ss,ax
mov sp,20h
mov bx,0
mov cx,8
S:
push cs:[bx]
add bx,2
loop s
mov bx,0
mov cx,8
S0:
pop cs:[bx]
add bx,2
loop s0
mov ax,4c00h
int 21h
Code ends
End start

将数据、代码、栈放入不同的段

1、定义多个段的方法:
同定义代码段一样,我们可以分别定义数据和栈段。
2、对段地址的引用:
每个段的名称对应着该段的段地址。
3、“代码段”、“数据段”、“栈段”完全是我们的安排:

  • 数据、堆栈、代码在加载到内存时是在地址连续的一段内存空间上。
    我们在源程序中为每个段起上名字只是为了便于阅读程序,并可以借用该名字(标号)所在的段地址。
  • 我们在源程序中用伪指令ASSUME CS:CODE,DS:DATA,SS:STACK进行段分配后,CPU并不能自动将段寄存器指向该段。程序刚加载到内存时,CS可根据END 标号来指向程序入口地址,而DS和ES的值为PSP的段地址;SS为DS+10H。所以在源程序中我们要通过指令初始化DS,ES和SS的值

约定

[BX]:指偏移地址为(BX)的内存单元。

LOOP循环指令

( ):指内容

用idata表示常量(立即数)

[BX]

  • EA:偏移地址
  • SA:段地址
  • PA:物理地址

我们常用[BX]来提供内存单元的偏移地址,通过修改BX的值,可由DS:[BX]来寻址不同地址的内存单元。

循环控制指令LOOP

格式:LOOP 标号 ;CX≠0循环
功能:当CX≠0时,(CX)=(CX)-1;转移到标号处循环执行。

计算2^12^

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
assume cs:code,ds:data
data segment
res dw 0
data ends

code segment
start:
mov ax, data
mov ds, ax
mov cx, 000ch
mov ax, 0001h
s:
add ax, ax
loop s
mov res, ax
mov ax, 4c00h
int 21h
code ends
end start

CX和LOOP指令配合实现循环功能的三个要点:
1、在CX中存放循环次数
2、LOOP指令中的标号所标识地址要在前面
3、要循环执行的程序段写在标号和LOOP指令之间。

DEBUG和汇编编译器MASM对指令的不同处理

DEBUG中我们可以使用下面指令来读写内存:

1
2
3
MOV AX,[0]
MOV AL,[2]
MOV BX,[1234H]

但在汇编程序中只能使用[寄存器]来寻址内存:
MOV AX,[BX]
在汇编编译器处理中,
MOV AX,[1234H]= MOV AX,1234H

LOOP和[BX]的联合使用

计算:FFFF:0—FFFF:B单元中的数据的和,结果保存在DX中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
assume cs:code
code segment
start:
mov ax ,0ffffh
mov ds ,ax
mov bx, 0h
mov cx ,0ch
mov dx, 0
mov ah, 0
s:
mov al, ds:[bx]
add bx, 1h
add dx, ax
loop s
mov ax, 4c00h
int 21h
code ends
end start

段前缀

在访问内存单元的指令中,用于显式地指明内存单元的段地址的“段寄存器:”,在汇编语言中称为段前缀。

一段安全的空间

汇编语言程序直接面向机器,如果我们要向内存空间写入数据时,要保证所写入的内存中没有重要的数据,否则会影响系统的正常运行,在一般的PC机中都不使用0:200—0:300这段内存空间,所以我们可以放心使用这段安全的空间。

段前缀的使用

编程将内存FFFF:0—FFFF:B单元中的数据拷贝到0:200—0:20B单元中

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
assume cs:code
code segment
start:
mov ax, 0FFFFH
mov ds, ax

mov ax, 0H
mov es, ax

mov si, 0
mov di, 200H
mov cx, 0CH

copy_loop:

mov al, ds:[si]
mov es:[di], al

inc si
inc di
loop copy_loop

mov ax, 4C00H
int 21H
code ends
end start

对软件测试的理解

  • 软件=程序 + 文档
  • 软件测试 ≠ 程序测试

使用人工或自动手段,来运行或测试某个系统的过程。其目的在于检验它是否满足规定的需求或弄清预期结果与实际结果之间的差别。
软件测试是以检验是否满足需求为目标。

对缺陷的理解

Bug是软件中(包括程序和文档)不符合用户需求的问题

Bug类型:

  1. 完全没有实现的功能
  2. 功能或性能问题或差异
  3. 多余的功能

软件测试PIE模型:

第一章-概述及基础-2025-10-23-16-32-29

  • 通过执行软件,检查执行结果的动态测试活动能够发现的问题只有外部层面的软件失败,内部静态层次缺项和内部中间状态层次错误无法检测到
  • 测试设计的重要工作之一,如何恰当地设计测试数据,使得软件缺陷尽可能地产生失败而被外部观察到

软件测试基本原则

  1. 测试表明缺陷存在,而不是证明缺陷不存在
  2. 穷尽测试是不可能的
  3. 测试活动应尽早开展
  4. 缺陷往往集中出现
  5. 注意杀虫剂悖论(重复同样的测试用例会逐渐失效)
  6. 测试依赖于上下文
  7. 错误谬论(软件没有缺陷≠软件能满足用户需求)

追溯需求:

  • 覆盖性保证:保证每一个需求都有相应的测试用例(避免遗漏)。
  • 可追踪性:当需求变更时,能快速定位需要修改的测试。
  • 目标一致性:测试不应该“随便测”,而是始终对准需求目标。

软件测试的分类

单元测试、集成测试、系统测试和验收测试

为什么进行软件测试

  • 提升软件质量
  • 降低成本
  • 提高客户满意水平

软件测试的4W1H

  • Why :保证软件质量
  • What:测试是为发现bug而执行程序或系统的过程
  • When:不测试的代价比测试的代价大的时候,越早发现bug修复成本越低
  • Who:测试工程师,软件开发人员,QA,软件过程改进组成员
  • How:测试的原理、方法和工具

一个源程序从写出到执行的过程

  1. 编写汇编源程序(.txt)
  2. 对源程序编译(.obj)
  3. 对目标文件连接(.exe)
  4. 执行可执行文件程序

源程序

汇编源程序:用汇编语言写出的程序代码
汇编指令:告诉计算机如何处理数据的命令。

伪指令

不要求CPU执行具体操作,汇编时不产生机器码,仅仅给汇编程序提供相应的汇编信息(程序中段的信息、堆栈的大小、调用的数据库)

段定义伪指令SEGMENT和ENDS

一个完整的源程序可由3个段组成:堆栈段、数据段、代码段。其中堆栈段和数据段可以没有,但代码段是必须的。

SEGMENT:定义一个段开始(一个段必须有一个名称来标识)
ENDS:说明一个段的结束,和SEGMENT成对使用。

格式:

1
2
3
4
5
段名  SEGMENT
-
-
-
段名 ENDS

汇编结束伪指令END

格式: END [标号]
功能:表示源程序到此结束。
说明:一个源程序必须有且只能有一个END语句,一般放在源程序的最后一行。

ASSUME 段分配伪指令

格式: ASSUME 段寄存器:段名 [,段寄存器:段名,…]
功能:用于说明源程序中定义的段或组由哪个寄存器去寻址,即建立寄存器与段间的对应关系。

源程序中的“程序”

标号:代表某一存储单元地址的名字

字母:AZ,数字:09,特殊字符:?. @_ $

注意:数字不能作名称的第一个字符。圆点只能用作第一个字符,标号最长为31个字符。

汇编语言源程序的结构

一般来说,一个完整的汇编源程序应由三个程序段组成,即代码段、数据段和堆栈段,每个段都以SEGMENT开始,以ENDS结束,代码段包含程序要执行的指令;堆栈段用来在内存中建立一个堆栈区;数据段用来在内存中建立一个适当容量的工作区,以存放程序中所需的数据。

例题

计算2^3^

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
; 定义数据段,用于存放结果
DATA SEGMENT
result DW 0 ; 用字型(16位)存储结果(2^3=8)
DATA ENDS

; 定义代码段
CODE SEGMENT
ASSUME CS:CODE, DS:DATA ; 关联段寄存器

start:
mov ax, DATA ; 初始化数据段寄存器DS
mov ds, ax

mov al, 2 ; AL = 2(初始值)
mul al ; AX = AL * AL = 2*2=4(第一次乘法)
mul al ; AX = AX * AL = 4*2=8(第二次乘法,得到2^3)

mov result, ax ; 将结果存入result变量

; 程序结束(返回DOS)
mov ah, 4ch
int 21h

CODE ENDS
END start ; 程序入口为start

语法错误和逻辑错误

语法错误:程序在编译时被编译器发现的错误
逻辑错误:源程序编译后运行时发生的错误

对目标文件进行连接

连接的作用:

  • 当源程序较大时,编译器会将源程序文件分成多个部分来编译,每个源程序编译成为目标文件后,再用连接程序将它们连接在一起,生成一个可执行文件。
  • 程序中调用了某个库文件中的内容时,需要将这个库文件和该程序生成的目标文件连接在一起。
  • 一个源程序编译后,得到了存有机器码的目标文件,目标文件中有的内容不能直接生成可执行文件,必须通过连接程序将这些内容处理为最终可执行的信息。

内存中的字

字单元:即存放一个字型数据(16位)的内存单元,由两个地址连续的内存单元组成。高地址内存单元中存放字型数据的高位字节,低地址内存单元中存放字型数据的低位字节。

第三章-寄存器(内存访问)-2025-10-22-19-28-23

  1. 20H
  2. 4E20H
  3. 12H
  4. 0012H
  5. 124EH

DS和[address]

DS—数据段寄存器:提供数据所在内存的段地址。
[address]:表示一个偏移地址为address的内存单元。

例:[1234H]指偏移地址为1234H的内存单元。

1
2
3
MOV BX, 1000H
MOV DS, BX
MOV AL, [0000H]

读取10000H地址数据写入AL

CS只能作为源操作数

判断下列指令是否正确,若错误,为什么?

  1. MOV CL,1000H :CL是 8 位寄存器,而1000H是 16 位立即数
  2. MOV AL,[1000H]:需确保DS已正确初始化
  3. MOV [BX],[SI]:8086 汇编不允许两个内存单元直接传送数据
  4. MOV AH,BH
  5. MOV AX,[SI]
  6. MOV 1234H,BX:目的操作数必须是寄存器或可寻址的内存单元,而1234H是立即数
  7. MOV CL,AX:CL是 8 位寄存器,AX是 16 位寄存器
  8. MOV CS,AX:不允许直接用MOV指令修改CS的值
  9. MOV DS,CS

字的传送

当向内存单元中存取数据时,若操作的为字节型数据则一次读写一个内存单元,若为字型数据则按低地址为低8位,高地址为高8位的原则存取数据

MOV、ADD、SUB指令

指令: 操作码+操作数

CPU提供的栈机制,PUSH,POP指令

PUSH OPPOP OP

执行PUSH、POP指令时,CPU如何找到要操作的位置?
CPU进行堆栈操作—SS:SP
SS—堆栈段寄存器:指定堆栈段的段地址
SP—堆栈指针寄存器:指向栈顶的偏移地址

任意时刻,SS:SP指向栈顶地址。

栈顶超界的问题

在8086CPU中没有预防栈超界的机制,所以程序员必须自己考虑,防止栈超界。

PUSH、POP指令

题目一

将10000H—1000FH这段空间当作栈,初始状态栈是空的,将AX,BX,DS中的数据入栈。

1
2
3
4
5
6
MOV AX,1000H
MOV SS,AX
MOV SP,0010H
PUSH AX
PUSH BX
PUSH DS

题目二

  1. 将10000H—1000FH这段空间当作栈,初始状态是空的;
  2. 设置AX=001AH,BX=001BH;
  3. 将AX,BX中的数据入栈;
  4. 然后将AX、BX清零;
  5. 从栈中恢复AX、BX原来的内容;
1
2
3
4
5
6
7
8
9
10
11
MOV AX,1000H
MOV SS,AX
MOV SP,0010H
MOV AX,001AH
MOV BX,001BH
PUSH AX
PUSH BX
MOV AX,0000H/SUB AX,AX
MOV BX,0000H/SUB BX,BX
POP BX
POP AX
  1. 将10000H—1000FH这段空间作栈,初始状态栈是空的
  2. 设置AX=002AH,BX=002BH;
  3. 利用栈,交换AX和BX中的数据。
1
2
3
4
5
6
7
8
MOV AX,1000H
MOV SS,AX
MOV SP,0010H
MOV AX,002AH
MOV BX,002BH
PUSH AX
MOV AX,BX
POP BX

题目三

如果要将10000H处写入字型数据2266H,可以用以下指令完成:

1
2
3
4
MOV     AX,1000H
MOV DS,AX
MOV AX,2266H
MOV [0],AX

补全下面的代码,使它能够完成同样的功能:

1
2
3
4
5
————————
————————
————————
MOV AX,2266H
PUSH AX
1
2
3
4
5
MOV AX,1000H
MOV SS, AX
MOV SP, 0002H
MOV AX,2266H
PUSH AX

栈段

分析:如果我们将10000H—1FFFFH这段空间作栈段,初始状态栈是空的,此时SS=1000H,SP=?一个栈段最大可以设为多少?

SP=0000H,栈段216字节