如果你使用的是真实版本的 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要获得lower 到upper(含)范围内的随机数,您可以使用以下通用公式:
rndvalue = (rand() % (上下+1)) + 下;
-
不足之处:将 PRNG 中的随机值转换为 1 到 10 之间的数字仍然存在模偏差。
-
在开发一般的 16 位汇编程序时,我使用Watcom's register calling convention(第 12 页的描述)。这可以根据自己的需要量身定制。
-
这种特殊的 LCG PRNG 在模式重复之前的周期约为 65536。这对于大多数简单的任务应该足够了。