装配中的随机数

use*_*385 4 random x86 assembly x86-16 emu8086

我是汇编的新手,想知道如何在EMU8086中编写一个程序,在每次运行时打印一个不同的随机数.是否可以在不使用中断的情况下完成?

Mic*_*tch 6

如果您使用的是真实版本的DOS(不是EMU8086),那么@fuz方法就是您可以做到的方式,并且它不需要中断.您只需在BIOS数据区(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
Run Code Online (Sandbox Code Playgroud)

然后,您可以使用该值并将其打印出来.该值是半随机的.您可以直接打印出来,但最好将其作为种子值传递给伪随机数生成器(PRNG).有关基本LCG,请参阅以下部分.打印整数是一个单独的问题,尽管EMU8086有一个宏/函数来做到这一点.此代码可以生成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
Run Code Online (Sandbox Code Playgroud)

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

unsigned char num = (get_rand_value() % 10) + 1
Run Code Online (Sandbox Code Playgroud)

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

注意:时钟滴答是一个半随机源,并且转换为1到10之间的值的方法存在模偏差.我将上面的代码作为一种快速而肮脏的方法提供,但应该足以让您开始完成任务.


可以在不发出INT指令的情况下执行此操作,但我们仍然通过对中断处理程序的代码执行间接FAR CALL来使用中断向量表.当你提出问题是否可以在没有中断的情况下完成时,我怀疑这是你的想法.一个INT引擎盖下指令将当前FLAGS(使用寄存器PUSHF),接着是FAR CALL的等价物.控制转移到0x0000的FAR地址:[interrupt_num*4],它位于中断向量表(IVT)中.当中断例程完成时,它将发出一个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
Run Code Online (Sandbox Code Playgroud)

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

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

该值是半随机的.您可以直接打印出来,但最好将其作为种子值传递给伪随机数生成器(PRNG).

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

在这个相关的Stackoverflow Answer中可以找到一个简单的基于LCG的PRNG.根据该答案,您可以创建一个函数,使用计时器滴答对PRNG进行种子处理,以及从PRNG返回下一个值的函数.下面的代码演示了如何设置种子一次,然后显示1到10之间的两个随机值:srandsystimerand()

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
Run Code Online (Sandbox Code Playgroud)

备注:

  • 在开发16位汇编程序时,我使用Watcom的寄存器调用约定(第12页作为描述).这可以根据自己的需要量身定制.
  • 在模式重复之前,这个特定的LCG PRNG具有大约65536的周期.这对于大多数简单任务来说应该足够了.
  • 缺陷:将PRNG中的随机值转换为1到10之间的数字仍然存在模偏差.