Advertisement

《汇编语言》- 读书笔记 - 附注

阅读量:

《汇编语言》- 读书笔记 - 附注

  • 附注1:当前Intel系列微处理器已支持超过三种工作模式。

    • 第一种是实时地址寻址模式。
      • 第二种是保护寻址模式。
      • 它们的主要区别体现在寻址能力方面。
        • 对于实时地址寻址模式而言:
          • 其核心特点是能够直接访问物理地址空间中的数据。
        • 相比之下:
          • 保护寻址模式通过虚拟地址空间实现了对特定区域的数据隔离管理。
  • 内存空间的高度安全保护机制

  • 根据赋予处理机执行特定任务所需的权限等级划分的不同特权级别

  • 系统采用的任务调度与资源管理策略以及虚拟内存技术的应用

    复制代码
    * 为何需要保护模式
    * 访问受保护资源
  • 3. 模拟8086模式

  • 4. 使用长模式(Long Mode)

    • 利用64bit CPU在32bit系统上执行旧应用程序
    • 利用64bit CPU在64bit系统上执行旧应用程序
  • 附注2为补码。

  • 汇编编译器(masm.exe)对jmp指令的相关处理如下:

    • 第一种情况:当disp∈[-128,127]时。

      • 所述内容为书中所述。
    • 上机验证结果表明该行为与所述内容不一致。

      • 1.2. 当 disp ∈ [-32768, 32767]
        • 书中描述
    • 上机验证:与书中描述一至

    • 2. 向后转移

      • 2.1. 当 disp ∈ [-128, 127]
        • 书中描述
    • 上机验证:与书中描述并不符

      • 2.2. 当 disp ∈ [-32768, 32767]
        • 书中描述
    • 上机验证:与书中描述一至

    • 3. 总结

      • 向前跳转的处理
      • 向后跳转的处理
    • 附注4:用栈传递参数

      • C 语言示例
  • 附注5:公式论证

      • 分析:
        • 基本原理:以分制优 各司其事
        • 公式拆解
        • 论证环节
        • 伪码解析验证

附注1:Intel 系列微处理器的3种工作模式(现在不止了)

在微型计算机中广泛应用的Intel系列微处理器的发展历程主要体现在以下几个方面:从4位至16位的微型处理器依次经历了从256字节到4MB主存储器的发展阶段,并且通过技术升级实现了从单周期至多周期时钟的速度提升。其中关键型号包括:从256KB到2GB主存储器版本以及从MMX技术到SSE指令集的各种性能优化版本。这些改进使得Intel系列微处理器能够满足不同层次的应用需求,并推动了现代计算机体系结构的进步

  • 8086 与 8088 微处理器的主要区别
    1. 数据总线宽度 :8086具有16位外部数据总线,而8088为8位,这直接影响了数据传输的速度和效率。
    2. 指令队列 :8086拥有6字节指令队列,相比8088的4字节,能更高效地预取指令。
    3. 系统兼容性 :8088设计上偏向于与8位系统及外设兼容,更适合当时市场的需求。
    4. 性能 :因总线宽度和指令队列的差异,8086通常提供更高的处理性能,而8088在某些应用场景下可能因系统集成的便利性和成本优势而被选用。
    5. 硬件接口 :两者的控制信号和引脚配置有细微差别,需针对性设计硬件接口。

总体而言,8086是高性能选择,而8088则是成本效益与兼容性更好的方案。

1. 实地址模式(Real Address Mode)

此外,在这种工作方式下:
由于该处理架构能够直接访问内存物理地址而无需进行虚拟到物理地址转换,在运行时其逻辑地址与物理地址之间实现了完全一致的关系特征。这充分体现了其实现方式基于与类似架构如8036/4751等处理器一致的原则进行设计。
为了确保向后兼容性,在推出一系列新处理器的过程中Intel选择让这些设备均采用实模式运行。

CPU地址总线的最大访问宽度限定为20位,在这种情况下可实现的最大物理地址空间为1,048,576字节(即1MB)。该地址总线系统能够跨越从00\text{H}FFFF\text{H}的所有内存地址范围进行操作。

  1. 采用十六位寄存器组。段寄存器与偏移量组合形成二十位地址用于访问内存。
  2. 现代处理器通常配备页表和分页机制以简化内存管理。
  3. 中断与异常处理相对基础,在保护模式下不具备高级特性。
  4. 现代操作系统通过硬件抽象层进行操作。
  5. 处理器执行单一任务但可暂中断(Interrupt)处理外围设备请求。

现行模式的存在使传统的16位操作系统及应用程序能够在更为先进的32位或64位处理器平台之上正常运行,并非需要做任何改动。

补充:VMware, VirtualBox 等虚拟机也可以提供一个高度仿真的环境来运行DOS,但这种模拟是在软件层次完成的,而非物理CPU直接进入或模拟8086的工作模式。
虚拟化技术的意义之一就在于,它允许宿主机的CPU保持在高效的保护模式下运行现代操作系统,同时在虚拟机内部模拟出一个包含8086/8088兼容环境的平台,来运行16位的操作系统如DOS以及为其设计的软件。这种方式不仅保留了对旧软件的兼容性,还确保了宿主机系统的安全性和资源的有效隔离,同时也充分利用了现代硬件的高性能特性。因此,用户可以在享受最新技术带来的便利的同时,无缝兼容和利用遗留的软件资源,这是虚拟化技术在保持向后兼容性方面的一个重要贡献。

2. 保护模式(Protected Mode)

保护模式 与 实模式 的主要区别

寻址能力
  • 实模式: 基于一段寄存器配合偏移量计算的方式限定内存地址范围,在这种架构下,默认配置允许访问的最大内存容量为一百万字节。
    • 保护模式: 采用更为成熟的地址转换技术包括分段与分区等方法使得系统能够支持比一兆字节更大的内存范围例如在32位处理器中最高可达四千兆字节的内存空间。
内存保护
  • 实模式: 在这种模式下, 内存访问没有任何防护机制, 允许任何应用程序自由地读取或写入内存中的任意位置.
  • 这种情况容易导致系统崩溃或出现安全漏洞.
  • 保护模式: 该模式通过建立一个完善的内存管理机制并实施严格的权限控制策略,
  • 确保每个应用程序仅能访问与其授权相关的内存区域.
  • 这种设计实现了与其他应用程序及用户应用程序与操作系统核心之间的严格隔离,
  • 从而显著提升了系统的整体稳定性和安全性.
特权级别
  • 实模式: 取消了传统的权限体系, 用户应用与操作系统处于同等地位。(导致即使用户的程序出现错误也很容易危及整个系统)
    • 保护模式: 划分了不同的特权等级(例如从Ring 0到Ring 3)。核心代码始终运行在最高优先级下以确保系统的稳定性和安全性。通过将应用程序限制在较低级别的操作空间, 这样的设计进一步降低了潜在的安全风险。
任务管理和虚拟内存
  • 实模式:系统不提供内置的任务管理功能及虚拟内存机制。通过中断机制、固定资源进程(FPP)与动态资源进程(DRP)、直接硬件操作等多种策略实现类似多个独立应用程序协同工作的功能。
    • 保护模式:实现了多任务与虚拟内存的支持。操作系统借助分页技术将虚拟地址转换为物理地址,并确保每个应用程序拥有独立的虚拟地址空间区域。

为何需要保护模式

被引入以应对实模式中所存在的安全稳定性及资源管理效率等挑战。该模式主要通过内存保护措施、将权限层级划分为特权等级以及设置虚拟地址空间等手段来实现多任务处理能力的同时,在保障系统与用户数据安全性方面也展现出显著优势,并为其提供了必要的技术支持以满足现代计算系统的快速发展需求

访问受保护资源

当采用保护模式时,在这种情况下程序无法直接访问受保护的内存或硬件资源。为了与这些资源交互以便获取所需功能内容,在这种情况下程序必须通过操作系统提供的API发起请求。其中API充当了一个安全代理角色,并会对请求执行权限验证,并采用控制和监管的方式执行相关操作以确保所有访问均在安全且有序的环境中完成。

3. 虚拟 8086 模式

虚拟8086模式基于保护模式下对实模式8086处理器行为进行模仿的一种独特机制。其价值体现在它能在现代操作系统及硬件平台上继续支持原先基于16位实模式设计的经典应用程序及DOS程序。而在虚拟化环境中运行时,在实际处于保护状态的情况下仍能创建独立环境供每个任务使用,并维持与早期软件兼容性的能力。然而那些直接通过MS-DOS程序接口访问计算机硬件资源的应用程序无法得到支持。

随着64位时代的到来,
尽管CPU硬件仍持续支持虚拟8086模式,
但如今的64位Windows操作系统已不再直接兼容16位应用程序。
这是因为现代64位Windows系统主要运行于长模式(Long Mode)环境中,
这种模式与实模式及虚拟8086模式均存在不兼容性,
从而导致无法直接执行16位代码指令。

64位系统环境下执行16位应用的常用方法是:借助虚拟化技术,在配置有16及32位操作系统的虚拟机中部署这类程序。

4. 64位模式(长模式 Long Mode)

该处理器架构支持64位运算,在AMD Opteron和Intel Xeon系列处理器中应用广泛。这种架构能提升CPU的内存地址范围,并进一步提升了通用寄存器的数量。这些特性在扩展过程中并未受到影响。
长模式是在保护模式基础上,在64位计算环境中的一种自然发展演变,并非取代。

64位CPU + 32位系统上运行32位应用

32位操作系统基于32bit架构的标准规范构建与运行机制。它遵循32bit架构的标准化交互规则与接口,在与硬件设备的交互过程中始终保持一致性和规范性。这些设计要素均不受特定处理器类型的影响。
当该操作系统被部署至64bit处理器上时,则会自动识别并相应地进行行为调整以实现兼容性支持。
这些设计要素均基于32bit系统标准展开构建与实现。

64位处理器遵循特定的保护机制(即所谓的兼容模式),向下兼容32位指令集与寻址方式,在此基础之上保障32位操作系统能够顺利运行。这种设计不仅使CPU意识到它正在服务于一个基于32位的操作系统,并且会积极地调整自身的运行模式与功能设置以适应这一需求。
尽管64位处理器具备更为高级的功能(如64位运算与寻址能力),但在执行基于32位的操作系统任务时却受限于维护与该系统的兼容性需求。

该应用实体能够在由操作系统的软件层营造出的32位兼容环境中实现无缝运行,在此过程中无需深入理解或关注底层硬件的具体位宽细节

64位CPU + 64位系统上运行32位应用

在64位处理器上运行的32位应用程序无需脱离长模式。相反地,在这种情况下,CPU与操作系统的协作营造了一个虚拟的32位环境。具体来说,在这种协作中,系统会限制地址空间至32位范围,并采用32 bit指令集等措施。这些措施都是通过长模式框架内完成的。简单来说,在这种情况下,CPU仍在执行于长模式下,仅仅依靠特定的子模式以及操作系统的辅助手段,巧妙地模拟出一个适合 32 位程序执行的环境,最终效果就是让这个应用程序认为自己正运行在一个 32 位系统上。

补充:

附注2:补码

之前的内容已经介绍过,请访问 笑虾:学习笔记:原码, 反码, 表示

附注3:汇编编译器(masm.exe)对jmp 的相关处理

1. 向前转移

复制代码
    s:  :
    :
    :
    jmp s ( jmp short s, jmp near ptr s, jmp far ptr s )

在编译过程中设置了地址计数器(AC)。该计数器用于记录操作码的位置信息,在每条指令处理完毕后会自动递增一次数值。当执行某些伪操作指令时系统也会相应地增加该计数器的数值以反映其执行效果例如db和dw等指令就是典型的例子它们分别对应不同的数据类型并赋予相应的增量数值。当处理标号s时系统会在内存中保存当前状态下的该计数器数值并将其存储为变量as随后每当遇到jmp … s这类跳跃指令系统又会在内存中保存此时该计数器的新数值并将它赋给变量aj最后通过计算as减去aj就能得到所需的位移量参数disp

1.1. 当 disp ∈ [-128, 127]

书中描述

当前无论运行哪些指令如 \texttt{jump~s}, \texttt{jump~short~s}, \texttt{jump~near~ptr~s}\texttt{jump~far~ptr~s} 都将统一转换为目标指令 \texttt{jump~short~s}.

复制代码
    assume cs:code
    code segment
    s:  jmp s
    	jmp short s
    jmp near ptr s
    jmp far ptr s
    code ends
    end s
上机验证:与书中描述并不符

通过调试审查发现实际运行结果与书中所述存在差异。在MASM环境下具体来说,在MASM环境下, 指令 jmp s 被解析为短跳指令 short, 即生成的汇编指令为 jmp short s;而针对指针的操作指令如 jmp near ptr 和 jmp far ptr 则会生成对应的指针跳转指令 jmp near ptr 和 jmp far ptr 。这可能源于作者所使用的MASM版本不同吗?这个问题值得进一步探讨。

复制代码
    assume cs:code
    code segment
    s:  jmp s			; 076E:0000	EBFE		JMP 0000		此时IP已是2,所以位移-2(补码 FE)
    	jmp short s		; 076E:0002	EBFC		JMP 0000		此时IP已是4,所以位移-4(补码 FC)
    jmp near ptr s  ; 076E:0004	E9F9FF		JMP	0000		此时IP已是7,所以位移-7(补码 FFF9)
    jmp far ptr s   ; 076E:0007	EA00006E07	JMP	076E:0000	far 是绝对地址跳转。段地址:偏移地址
    code ends
    end s
在这里插入图片描述
使用指令 机器码 机器码对应指令
jmp s EB disp(disp 1字节,共占2字节) jmp short s
jmp short s EB disp(disp 1字节,共占2字节) jmp short s
jmp near ptr s E9 disp(disp 2字节,共占3字节) jmp near ptr s
jmp far ptr s EA disp:disp(disp 4字节,共占5字节) jmp far ptr s

1.2. 当 disp ∈ [-32768, 32767]

书中描述
指令 结果 占空间
jmp short s 编译报错

| jmp s
jmp near ptr s| 生成 jmp near ptr s 对应机器码 E9 disp| disp 2字节,共占3字节 |
|jmp far ptr s|生成 jmp far ptr s 对应机器码 EA disp:disp|2个disp 4字节,共占5字节|

编译以下程序 jmp short s 将产生编译错误,去掉后再编译即可成功。

复制代码
    assume cs:code
    code segment
    s:  db 100 dup (0b8h, 0, 0)     	; 重复 (0b8h , 0 , 0 ) 100次,共占 300 字节
    jmp short s       				; disp 已经超出 short 的偏移范围,将产生编译错误
    jmp s
    jmp near ptr s
    jmp far ptr s
    code ends
    end s
上机验证:与书中描述一至

用 Debug 进行反汇编查看如下图:

在这里插入图片描述
使用指令 机器码 机器码对应指令
jmp short s 产生编译错误
jmp s E9 disp(disp 2字节,共占3字节) jmp near ptr s
jmp near ptr s E9 disp(disp 2字节,共占3字节) jmp near ptr s
jmp far ptr s EA disp:disp(两个disp 各2字节,共占5字节) jmp far ptr s

2. 向后转移

复制代码
    jmp s ( jmp short s, jmp near ptr s, jmp far ptr s )
    :
    :
    s:  :

在解析 jmp ... s 指令时,在这种情况下

指令 机器码 预留
jmp short s EB nop 预留1字节空间,存放8位 disp
jmp s EB nop nop 预留2字节空间,存放16位 disp
jmp near ptr s EB nop nop 预留2字节空间,存放16位 disp
jmp far ptr s EB nop nop nop nop 预留4字节空间,存放段地址偏移地址

编译器继续运行中,并当它向后扫描至标号s时会记录AC的值as,并计算转移位移量为disp=as−aj

首先记录目标地址差值并生成跳转指令

2.1. 当 disp ∈ [-128, 127]

书中描述

在处理跳转指令时,在使用机器码表示的ss-near-addr指针类型下,在相应的跳转操作之后还需添加一条nop指令;而当采用s-far-addr指针类型时,在相应的跳转操作之后还需添加三条nop指令。

指令 机器码 预留空间
jmp short s EB disp 预留1字节空间,存放8位 disp
jmp s EB disp nop 预留2字节空间,存放16位 disp
jmp near ptr s E8 disp nop 预留2字节空间,存放16位 disp
jmp far ptr s E8 disp nop nop nop 预留4字节空间,存放段地址偏移地址

编译,连接以下程序,用 Debug 进行反汇编查看。

复制代码
    assume cs:code
    code segment
    begin:
    	jmp short s
    	jmp s
    	jmp near ptr s
    	jmp far ptr s
    s:        
    	mov ax,0
    code  ends
    end  begin
上机验证:与书中描述并不符

Debug查看效果如下图:

在这里插入图片描述

这里 jmp s 最终生成的是 EB08 与书描述的预留 EB nop nop 不符,可能原因有2:

  1. 书上说错了。
  2. 编译器对结果进行了优化,去掉了多出来的那个 nop

(继续向下看,有新发现)

2.2. 当 disp ∈ [-32768, 32767]

书中描述

编译该程序时会遇到编译错误。这些错误是由 jmp short s 指令引起的。移除该指令后重新进行编译则可解决问题。

复制代码
    assume cs:code
    code segment
    begin:
        jmp short s				; disp 已经超出 short 的偏移范围,将产生编译错误
        jmp s
        jmp near ptr s
        jmp far ptr s
        db 100 dup (0b8h, 0, 0)	; 重复 (0b8h , 0 , 0 ) 100次,共占 300 字节
    s:  
        mov ax,2
    code ends
    end begin

用 Debug 进行反汇编查看:

上机验证:与书中描述一至
在这里插入图片描述

从这里可以看出 jmp s 最终生成了 E93401 ,这与书中所留的两个 nop 相符,
我们有理由认为,在 disp ∈ [-128, 127] 的情况下编译器对最终结果进行了优化处理 ,去除了多余的 nop

3. 总结

向前跳转的处理

位移量计算机制:当进行前向跳跃操作时,在编译器能够立即确定从跳转指令到目标标号之间的位移量(disp)。这是因为已经在处理过程中记录了目标标号的位置信息,并且此偏移量基于当前处理代码段中的地址差值。

若计算所得的位移量超出短跳转的能力范围,则该位移量无法落在短跳转的有效编码区间[-128, 127]内;此时编译器将生成相应的条件转移指令

  • jmp short distance s会引起编译错误,
  • jmp near ptr s会产生E9 disp指令格式(其中disp表示补码位移量),该指令占用2个字节并支持更大的地址空间。
  • jmp far ptr s则会执行跨段跳跃操作,并产生EA disp:disp指令格式(其中第一个字节存放段地址(2个字节),第二个字节存放偏移地址(2个字节))。

向后跳转的处理

占位符使用
当编译器处理跳转指令尚未解析目标标记时,在生成相应的跳转操作码之前会先创建一个占位符,并为后续填入实际偏移量留出足够的存储空间。根据不同的jmp指令类型,在处理时所需的空间预留程度也会有所差异

位移量确定与回填

一旦确定了所有指令的长度...编译器随后会根据这些信息计算出正确的位移量...对于后向\texttt{jmp near ptr s}指令\texttt{...}若所得偏移值处于短跳范围\texttt{...}理论上可能采用另一种编码方式\texttt{...}但为了保持一致性与兼容性\texttt{...}一般仍采用\texttt{E9 disp}格式以确保代码的一致性与稳定性。
而对于\texttt{jmp far ptr s}这一远距离跳跃指令\texttt{...}不论偏移量有多大\texttt{...}最终会生成完整的远距离跳跃指令...即EA disp:disp$.

借助这种方式,在处理无论是向前跳跃还是向后跳跃的情况下,编译器能够保证生成正确的机器码序列,并以满足程序在运行时进行跳跃的需求。

附注4:用栈传递参数

调用者将参数压入栈中(即执行入栈操作),然后子程序从栈顶依次取出参数进行处理。
栈操作的基本单位是每个字(即每个汉字占用2个字节空间)。
因此,在返回时会执行 ret 2n 指令。
汇编语言中描述 ret n 指令的功能如下:

复制代码
    pop ip
    add sp, n

分析下面程序,观察栈顶指针 sp 的变化:

复制代码
    assume cs:code
    code segment
    	mov ax,1	; 参数 b
    	push ax
    	mov ax,3	; 参数 a
    	push ax
    	call difcube
    	
    	mov ax,4c00h
    	int 21h
    
    ;说明: 计算(a-b)^3,a、b 为字型数据 word
    ;参数: 进入子程序时,栈顶存放IP,后面依次存放a、b
    ;结果: (ds:ax)=(a-b)^3
    
    difcube:
    	push bp
    	mov bp,sp
    	mov ax,[bp+4]	; 将栈中a的值送入ax中
    	sub ax,[bp+6]	; 减栈中b的值
    	mov bp,ax
    	mul bp
    	mul bp
    	pop bp
    	ret 4
    	
    code ends
    end

Debug 单步过程:

C 语言示例

通过一个C语言程序经过编译生成的汇编语言程序来观察其汇编版本中栈的参数传递机制

复制代码
    void add (int,int,int);
    
    main()
    {
    	int a=1;
    	int b=2;
    	int c=0;
    	add(a,b,c);
    	c++;
    	printf("c = %d", c);
    }
    
    void add(int a,int b,int c)
    {
    	c=a+b;
    }

我不清楚书上使用了哪种编译器来编译源代码,并且在这里使用的是 tcc -S demo.c 进行编译,并与书中使用的编译方法有所不同。

复制代码
    _TEXT	segment	byte public 'CODE'
    _main	proc	near
    	push bp					; 备份调用者栈帧基址
    	mov	bp,sp				; 创建 main 当前栈帧
    	sub	sp,2				; 开辟 2 字节局部空间
    	push	si				; 备份寄存器
    	push	di
    
    	mov	di,1				; int a=1;
    	mov	word ptr [bp-2],2	; int b=2;
    	xor	si,si				; int c=0;
    
    	push	si				; 参数 c 压栈
    	push	word ptr [bp-2]	; 参数 b 压栈
    	push	di				; 参数 a 压栈
    	call	near ptr _add	; add(a,b,c); 
    	add	sp,6					; add 返回后清掉栈中的3个参数
    	inc	si					; c++;
    
    	pop	di					; 还原寄存器
    	pop	si
    	mov	sp,bp				; 销毁 main 当前栈帧
    	pop	bp					; 恢复调用者栈帧基址
    	ret						; 返回
    _main	endp
    
    _add	proc	near
    	push bp					; 备份调用者栈帧基址
    	mov	bp,sp				; 创建 add 当前栈帧
    
    	mov	ax,word ptr [bp+4]
    	add	ax,word ptr [bp+6]
    	mov	word ptr [bp+8],ax
    
    	pop	bp					; 恢复调用者 main 栈帧基址
    	ret						; 返回
    _add	endp
    _TEXT	ends
    
    	public	_main			; 声明为公共
    	public	_add			; 声明为公共
    end

附注5:公式证明

证明公式:X/N = int(H/N) * 65536 + [ rem(H/N) * 65536 + L ] / N 不会溢出

分析:

原理:分而治之各各击破

  1. 一个32位数除以16位数的结果,对于16位寄存器并不是总能装下。如:
复制代码
    1111 1111 1111 1111 1111 1111 1111 1111		; 32位
    0111 1111 1111 1111 1111 1111 1111 1111		; 除以2相当于右移一位。
    											; 这 31 位的结果 16 位寄存器肯定是存不下的
  1. 但是如果把高位,低位拆分开来处理,就是16位对16位了,无论如何都能存下。
复制代码
    1111 1111 1111 1111		; 16位
    1111 1111 1111 1111		; 除以 1 够狠了吧,16位寄存器还是能装下
  1. 最后再将高位,低位的结果各自放对应位置上就组成了正确的结果。

公式分解

让我们逐步分析这个公式的各个部分,以及它是如何避免溢出的。

公式为:X/N = int(H/N) * 65536 + [ rem(H/N) * 65536 + L ] / N

  • X是一个32位长的被除数项,在数值上其数值范围限定在[0x0至0xFFFFFFFF]之间。
  • N代表一个具有16位整数特性的除数单元,在数值上其数值范围限定在[0x0至0xFFFF]之间。
  • H为X中的高位连续16位数据字段,在数值上其数值范围限定在[0x0至0xFFFF]之间。
  • L则对应着X中的低位连续16位数据字段,在数值上其数值范围也限定在[OxO至OxFFFF]区间内。

证明过程

处理高阶十六位(H)
计算方式为:将 H 与 N 相除所得商的整数部分乘以 2^{16}
其中涉及的计算步骤包括将 H 与 N 相除取其商的整数部分。
其结果值域限定在 [0, 2^{16} - 1] 区间。
由于 H 的最大可能值是 2^{16} - 1
因此当 H 最大时,
该商值达到最大可能值即 floor((\text{}2{16}-\text{$})/(\text{$}2{17}-\text{})) = \text{'}( ( (\text{}2^{n}\text{’}) ) ) 再将其乘以 2^{8}, 最终得到的结果范围限定在 [0, ( (\text{}2^{n}\text{’}) ) \times (\text{}+ \times (\text{‘}) ) ] 区间内,
这确保了整个运算过程不会溢出到更高位数的空间。

该算法中采用了一种特殊的计算方式:将高位十六进制余数值与低位十六进制值(L)进行结合处理,并计算其平均值。具体而言,在此计算过程中所得出的结果具有明确的数学表达式:(rem(H/N) * 65536 + L) / N。其中此计算结果的最大范围限定在 [0, 4,294,901,759] 区间内,并且这一结果仍然未超出 (2^32 即 4,294,967,296) 的安全边界。

计算余数 取值范围 10进制表示 16进制表示

| rem(H/N)| [0, N-1]| = [0, 65535-1]
= [0, 65534]| [0, FFFE] |
| rem(H/N) * 65536| [0, (N-1) * 65536]| = [0, 65534 * 65536]
= [0, 4,294,836,224]| [0, FFFE 0000] |
| rem(H/N) * 65536 + L| [0, (N-1) * 65536 + 65535]| = [0, 4,294,836,224 + 65535]
= [0, 4,294,901,759]| [0,FFFE FFFF] |

  • 余数的性质之一:余数 = 被除数 - 除数 × 商。根据这一性质可知余数值范围:0 <= 余数 < 除数
  • 在这里公式中的乘上65536这一操作,并不需要真正地在16位寄存器中执行乘法运算。
    比如将结果存入dx变量中(该变量代表高16位),就等价于对数值进行了左移16位处理(即将低16位数据转移至高16位区域)。

伪代码分析验证

复制代码
    	; 先处理32位被除数的高16位,【商】和【余数】一次 div 就到手
    	mov ax,0ffffh	; dword 被除数的高 16位
    	mov dx,0		; 32位被除数,高16位放到 ax 去了,dx要补 0
    	mov cx,2		; 16位除数
    
    	div cx			; 执行后,对应公式中这两段:
    					; AX = int(H/N) * 65536 = 高16位商
    					; DX = rem(H/N) * 65536 = 高16位余数
    	push ax			; 暂存高 16 位的商
    	
    	; 再处理32位被除数低16位
    	; 因为【高16位余数】已经在 dx 里(相当于已经 x 65536)
    	mov ax,0ffffh	; 现在只要将低16位装进 ax 即完成了 [ 高16位余数 + L ]
    
    	div cx			; 执行后,对应公式中这两段:
    					; AX = [ 高16位余数 + L ] / N = 低16位商
    					; DX = [ 高16位余数 + L ] / N = 低16位余数
    
    	; 调整一下位置即可得到最终结果
    	mov cx,dx		; 余数归位
    	pop dx			; 高 16 位的商归位
寄存器 被除数 除数 余数
ax 低16位 低16位
dx 高16位 高16位
cx 16位 16位

全部评论 (0)

还没有任何评论哟~