【问题标题】:Random number in assembly装配中的随机数
【发布时间】:2017-12-02 10:52:07
【问题描述】:

我是汇编新手,想知道如何在 EMU8086 中编写一个程序,在每次运行时打印一个不同的随机数。不使用中断可以做到吗?

【问题讨论】:

  • 你为什么还要提到中断?您是否正在考虑使用int 0x21 进行 DOS 系统调用?还是您的意思是设置一个中断处理程序来对随机事件或其他什么的时钟进行采样?
  • 没有中断是不容易做到的,因为你需要使用中断来打印一些东西。你的限制到底是什么?你能解释一下为什么存在这个限制吗?
  • int X 就是你在 DOS 中调用内置例程的方式;你也许可以使用旧的 CP/M-esque CALL 5 我猜,但由于没有人这样做,它甚至可能不会被你的 DOS 实现。例如。 DOSBOX 中没有 stackoverflow.com/questions/16669352/call-5-interface-on-ms-dos 。所以只需使用int
  • @fuz:我认为emu8086 可以模拟带有VGA 适配器的PC,因此您可以直接写入VGA 文本模式内存。这是最容易的部分(或者至少是我知道如何做/在哪里寻找文档的部分);在没有系统调用的情况下获得随机性更难。虽然也许硬件定时器寄存器中的某种时间或定时器并不难。
  • @PeterCordes 您可以询问时钟的当前时间,它已映射到某个 IO 端口。您还可以检查存储当前时间的 BIOS 数据区域0040:006c 以获得一些“随机性”。另一种选择是以一些高频率配置 PIT 并等待一些不可预测的事件。

标签: assembly random x86 x86-16 emu8086


【解决方案1】:

如果你使用的是真实版本的 DOS(不是 EMU8086),@fuz 方法是你可以做到的,它不需要中断。您只需在BIOS Data Area(BDA) 中的内存地址 0x46c (0x00040:0x006c) 处读取 32 位值的低 16 位。该位置的值是一个 32 位值,表示自午夜以来的计时器滴答数。可惜 EMU8086 不支持这种方式。

要在 EMU8086 中获取带有中断(系统调用)的随机数,您可以使用Int 1ah/ah=0h

时间 - 获取系统时间

AH = 00h
Return:
CX:DX = number of clock ticks since midnight
AL = midnight flag, nonzero if midnight passed since time last read

然后您可以使用该值并将其打印出来。该值是半随机的。您可以直接将其打印出来,但最好将其作为种子值传递给 Pseudo-random Number Generator (PRNG)。请参阅下面的部分以获取basic LCG。打印整数是一个单独的问题,尽管 EMU8086 有一个 macro/function 来做到这一点。这段代码可以产生一个介于 1 到 10 之间的半随机数并打印出来:

org 100h
include emu8086.inc                                                       

xor ax,ax            ; xor register to itself same as zeroing register
int 1ah              ; Int 1ah/ah=0 get timer ticks since midnight in CX:DX
mov ax,dx            ; Use lower 16 bits (in DX) for random value

xor dx,dx            ; Compute randval(DX) mod 10 to get num
mov bx,10            ;     between 0 and 9
div bx               ; Divide dx:ax by bx
inc dx               ; DX = modulo from division
                     ;     Add 1 to give us # between 1 and 10 (not 0 to 9)

mov ax,dx            ; Move to AX to print     
call PRINT_NUM_UNS   ; Print value in AX as unsigned

ret
                                                      
DEFINE_PRINT_NUM_UNS ; Needed to support EMU8086 PRINT_NUM_UNS function 

END

每次运行该程序时,它都应该打印一个介于 1 和 10 之间的数字。在我们从时钟滴答中获取随机值后,我们将其转换为介于 1 和 10 之间的数字。代码将类似于此伪代码1:

unsigned char num = (get_rand_value() % 10) + 1

我们除以 10 并使用模(模值将介于 0 和 9 之间)并加 1 使其成为介于 1 和 10 之间的值。get_rand_value 实际上是 Int 1ah/AH=0 系统调用。

注意:时钟滴答是半随机源,转换为从 1 到 10 的值的方法受到modulo bias 的影响。我将上面的代码作为一种快速而肮脏的方法提供,但应该足以让你开始你的任务。


可以在不发出INT 指令的情况下执行此操作,但我们仍然通过对中断处理程序代码执行间接FAR CALL 来使用中断向量表。当您问是否可以无中断完成时,我怀疑这就是您的想法。 INT 指令在底层推动当前的 FLAGS 寄存器(使用PUSHF),然后是相当于 FAR CALL。控制权转移到位于中断向量表 (IVT) 中的 FAR 地址 0x0000:[interrupt_num * 4]。当中断例程完成时,它将发出IRET 指令,该指令撤消推送,恢复标志并在 FAR CALL 之后返回指令。修改后的代码可能如下所示:

org 100h
include emu8086.inc                                                       

xor ax,ax            ; xor register to itself same as zeroing register
mov es,ax            ; Zero the ES register for use with FAR JMP below so that we
                     ;     can make a FAR CALL relative to bottom of Interrupt Vector Table
                     ;     in low memory (0x0000 to 0x03FF)

; Do a system call without the INT instruction
; This is advanced assembly and relies on the
; understanding of how INT/IRETD work. We fake a 
; system call by pushing FLAGS and rather 
; than use int 1ah we do a FAR CALL indirectly 
; through the interrupt vector table in lower memory
pushf                ; Push FLAGS
call far es:[1ah*4]  ; Indirectly call Int 1ah/ah=0 through far pointer in IVT
                     ;     get timer ticks since midnight in CX:DX

mov ax,dx            ; Use lower 16 bits (in DX) for random value

xor dx,dx            ; Compute randval(DX) mod 10 to get num
mov bx,10            ;     between 0 and 9
div bx
inc dx               ; DX = modulo from division
                     ;     Add 1 to give us # between 1 and 10 (not 0 to 9)

mov ax,dx            ; Move to AX to print
call PRINT_NUM_UNS   ; Print value in AX as unsigned

ret

DEFINE_PRINT_NUM_UNS ; Macro from include file to make PRINT_NUM_UNS usable

END

相关问题?可能的问题。简单的 LCG PRNG

在此问题的一天之内,还有一个与此模糊相似的问题是posted。如果此分配与另一个分配相关,则需要注意,如果您尝试从系统计时器滴答中快速连续获取随机数,您将遇到问题。在我上面的回答中,我说:

值为半随机。您可以直接将其打印出来,但最好将其作为种子值传递到Pseudo Random Number Generator (PRNG)。

计时器分辨率为每秒 18.2 次。这不是一个非常高的分辨率,并且可能一个接一个地调用 Int 1ah/ah=0 将导致返回相同的数字,或者第二次调用返回更高值的机会高于首先。这可以通过创建一个 PRNG(比如一个简单的LCG)来解决,并使用一次计时器值来播种它。对于您需要的每个值 - 您在 PRNG 中查询下一个值而不是系统时间。

一个简单的LCG based PRNG 可以在这个相关的Stackoverflow Answer 中找到。基于该答案,您可以创建一个 srandsystime 函数来为 PRNG 播种计时器滴答声,以及一个 rand() 函数从 PRNG 返回下一个值。下面的代码演示了设置种子一次,然后显示 1 到 10 之间的两个随机值:

org 100h
include emu8086.inc

start:
    call srandsystime   ; Seed PRNG with system time, call once only 

    call rand           ; Get a random number in AX
    call rand2num1to10  ; Convert AX to num between 1 and 10
    call PRINT_NUM_UNS  ; Print value in AX as unsigned
    PRINT ", "          ; Print delimiter between numbers
    call rand           ; Get another random number in AX
    call rand2num1to10  ; Convert AX to num between 1 and 10
    call PRINT_NUM_UNS  ; Print value in AX as unsigned
    ret 

; Return number between 1 and 10
;    
; Inputs:   AX = value to convert
; Return:   (AX) value between 1 and 10

rand2num1to10:
    push dx
    push bx
    xor dx,dx           ; Compute randval(DX) mod 10 to get num
    mov bx,10           ;     between 0 and 9
    div bx
    inc dx              ; DX = modulo from division
                        ;     Add 1 to give us # between 1 and 10 (not 0 to 9)
    mov ax,dx
    pop bx
    pop dx
    ret

; Set LCG PRNG seed to system timer ticks
;
; Inputs:   AX = seed
; Modifies: AX 
; Return:   nothing 

srandsystime:
    push cx
    push dx
    xor ax, ax          ; Int 1Ah/AH=0 to get system timer in CX:DX 
    int 1ah
    mov [seed], dx      ; seed = 16-bit value from DX
    pop dx
    pop cx
    ret

; Updates seed for next iteration
;     seed = (multiplier * seed + increment) mod 65536
;     multiplier = 25173, increment = 13849
;
; Inputs: none
; Return: (AX) random value

rand:
    push dx
    mov ax, 25173       ; LCG Multiplier
    mul word ptr [seed] ; DX:AX = LCG multiplier * seed
    add ax, 13849       ; Add LCG increment value
    mov [seed], ax      ; Update seed
    ; AX = (multiplier * seed + increment) mod 65536
    pop dx
    ret
        
seed dw 11             ; Default initial seed of 11    

    DEFINE_PRINT_NUM_UNS; Macro from include file to make PRINT_NUM_UNS usable    

END

脚注

  • 1要获得lowerupper(含)范围内的随机数,您可以使用以下通用公式:

    rndvalue = (rand() % (上下+1)) + 下;

  • 不足之处:将 PRNG 中的随机值转换为 1 到 10 之间的数字仍然存在模偏差。

  • 在开发一般的 16 位汇编程序时,我使用Watcom's register calling convention(第 12 页的描述)。这可以根据自己的需要量身定制。

  • 这种特殊的 LCG PRNG 在模式重复之前的周期约为 65536。这对于大多数简单的任务应该足够了。

【讨论】:

  • Int 21/ah=2 注释与代码不匹配。我想你的意思是int 1a/ah=0。 (顺便说一句,对像 ah 这样的部分寄存器进行异或归零对大多数 CPU 没有好处。mov ah,0 在 Haswell/Skylake 上可能是最好的,但 xor ax,ax 可能在 Core2 或 Sandybridge 上具有优势。我不认为xor ah,ah 永远是一场胜利,因为唯一在没有执行单元的情况下处理异或归零的 CPU(与同样为 2 字节的 mov ah,2 不同)是 Sandybridge 系列。On HSW xor ah,ah is not even dep-breaking。可能也是 SnB 本身。
  • @peterCordes 那是我无意复制粘贴的残余。以前的版本是别人的,在mov ah, 0上更差。本来是AX,是的,评论不正确。
  • 实际上重新阅读我之前的评论,xor ax,ax 可能很好,而不是 mov ah,0,在 Haswell 上。它避免了单独写入 AH,如果您读取 AX,则在插入合并 uop 时会导致前端停顿(1 个周期)。 xor ax,ax 不是 HSW 上的 dep-break 或归零习惯用法,但它对 Core2 非常有用。
  • 如果您使用的是实模式,通常的选择是追求尺寸而不是速度。如果大小是问题(而且通常是),在过去我永远不会编码mov ax, 0。如果生成的代码更小,您还会看到人们使用 pusha popa 推送所有内容并弹出所有内容(性能不佳)。
  • mov ah,0 是 2 个字节,但是 3 字节 mov ax,0 在我看来也是错误的,尽管现代 CPU 实际上可能更喜欢它的速度而不是大小。有一个 2 字节的 mov r8, imm8 使用操作码 B0-B7,就像 mov r16/32, imm16/32 使用操作码 B8-BF。与 3+ 字节 mov r/m8, imm8 分开。
【解决方案2】:

标准 PC 系统配置为使 PIT 通道 0 每秒触发 18.2 次。每次触发时,都会发生中断,BIOS 会计算自系统启动以来发生的次数,并将此数字存储在地址 0040:006c 的 BIOS 数据区域中。您可以将此值用作程序 PRNG 的“随机”种子值:

mov ax,0040h
mov es,ax       ; load segment for BIOS data area
mov ax,es:006ch ; load number of IRQ8 since boot into ax

由于这个值经常变化,它应该看起来是随机的。我建议你稍微调整一下,这样它就不会随着每次调用而增加。 (即播种 PRNG 并运行几次迭代)。

我不确定 Emu8086 是否能正确模拟这个,但在 PC 上它应该可以工作。

【讨论】:

  • IDK 为什么这被否决了;看起来像是在不使用任何软件中断 (int) 指令的情况下获得一些随机性的明智方法。我添加了一些关于使用时间作为随机数生成器的种子的细节,以将一系列递增的值变成一个真正的随机序列。
  • 在 EMU8086 地址 46ch 中始终为 0,因为不存在 int 70h (IRQ0) 支持。 EMU8086 也不支持 int 1ah 中断(支持系统时间接口的中断)
  • @MichaelPetch 这确实是个问题。删除我的答案。
  • @MichaelPetch 这可能是课堂练习,OP应该编写自己的随机数生成器,这就是为什么不能使用中断的原因。
  • 为您找到了解决方案。我之前没有尝试过所有场景并且错过了一些东西。尽管在 EMU8086 中的地址 46ch 处没有任何内容,并且没有 CMOS 系统时钟访问,但实际上有一个 int 1ah 例程确实有效 - 即 int 1ah/ah=0 它返回自午夜以来的时钟滴答声(您期望在正常环境下读取 46ch)。这意味着 DX 可以用作变化值的来源。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2015-10-27
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多