Random number in emu8086 without using DOS/BIOS calls?
Asked Answered
S

2

4

I am new to assembly and would like to know how to write a program in EMU8086 that prints a different random number in every run of it. Is it possible to do it without using interrupts?

Stedt answered 2/12, 2017 at 10:52 Comment(11)
which architecture? assembly is not a type of architectureInviting
I am using emu 80x86Stedt
Why do you even mention interrupts? Are you thinking of DOS system calls with int 0x21? Or do you mean setting up an interrupt handler to sample the clock on random events or something?Margeret
I mean the DOS system calls.Stedt
It is not easily possible to do so without interrupts because you need to use interrupts to print something. What exactly is your restriction? Can you explain why this restriction exists?Alcaic
int X is just how you call the built-in routines in DOS; you might be able to use the old CP/M-esque CALL 5 I guess, but since nobody ever did it may not even be implemented by your DOS. E.g. it's absent from DOSBOX per https://mcmap.net/q/2036790/-call-5-interface-on-ms-dos . So just use the int.Twiggy
@fuz: I think emu8086 emulates a PC with a VGA adapter, so you can write to VGA text-mode memory directly. That's the easy part (or at least the part I'd know how to do / where to look for docs); getting randomness without system calls is harder. Although maybe some kind of time or timer from a HW timer register is no harder.Margeret
@PeterCordes You could ask the clock for the current time, it's mapped into some IO port. You could also check the BIOS data area which stores the current time at 0040:006c to get some “randomness.” Another option is to configure the PIT with some high frequency and wait for some unpredictable event.Alcaic
@PeterCordes in worst case you can time the time-to-next-VSYNC after being executed. With a bit of luck the emu8086 isn't VSYNC based, so running it will lead to different values. Still it sounds like there's some ongoing misunderstanding between the teacher and student, while doing output directly to VRAM makes lot of sense to me personally (educational value I mean, the principle is universal enough), calling ordinary "get time" service from DOS sounds just as good to me, don't see any reason to teach them whole details about IRQ/BIOS low memory values/etc... (almost all of it obsolete).Bajaj
@PeterCordes (I just assume the emu8086 emulates VSYNC and VGA ports .. maybe not.. dosbox certainly does).Bajaj
@Ped7g: from MichaelPetch's comments, emu8086 doesn't emulate external interrupts at all. So good idea, but apparently again emu8086 doesn't emulate enough of a PC for it to work.Margeret
S
11

If you were using a real version of DOS (not EMU8086) @fuz method is the way you can do it, and it doesn't require interrupts. You just read the lower 16-bits of the 32-bit value at memory address 0x46c (0x00040:0x006c) in the BIOS Data Area(BDA). The value at that location is a 32-bit value representing the number of timer ticks since midnight. Unfortunately EMU8086 doesn't support this method.

To get a random number in EMU8086 with interrupts (system call) you can use Int 1ah/ah=0h:

TIME - GET SYSTEM TIME

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

You can then use that value and print it out. The value is semi random. You can print it out directly but it is preferable to pass it into a Pseudo-random Number Generator (PRNG) as a seed value. See section below for a basic LCG. Printing an integer is a separate issue although EMU8086 has a macro/function to do that. This code could produce a semi-random number between 1 and 10 and print it:

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

Each time you run this program it should print a number between 1 and 10. After we get the random value from the clock ticks we convert it to a number between 1 and 10. The code would have been similar to this pseudo code1:

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

We divide by 10 and use the modulo (Modulo value will be between 0 and 9) and add 1 to make it a value between 1 and 10. get_rand_value is effectively the Int 1ah/AH=0 system call.

Note: The clock ticks is a semi random source, and the method to convert to a value from 1 to 10 suffers from modulo bias. I present the code above as a quick and dirty method but should be enough to get you started on your assignment.


It is possible to do this without issuing an INT instruction, but we are still using the interrupt vector table by doing an indirect FAR CALL to the code for the interrupt handler. I doubt this is what you had in mind when you asked the question whether it can be done without interrupts. An INT instruction under the hood pushes the current FLAGS register (using PUSHF) followed by the equivalent of a FAR CALL. Control is transferred to the FAR address at 0x0000:[interrupt_num * 4] which is in the interrupt vector table (IVT). When the interrupt routine finishes it will issue an IRET instruction which undoes the pushes, restores the flags and returns the instruction after the FAR CALL. The revised code could look like:

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

Related Question? Possible Issues. Simple LCG PRNG

There is another question vaguely similar to this that was posted within a day of this one. If this assignment is related to the other one then it needs to be noted that you will encounter issues if you attempt to get random numbers in quick succession from the system timer tick. In my answer above I stated:

The value is semi random. You can print it out directly but it is preferable to pass it into a Pseudo Random Number Generator (PRNG) as a seed value.

The timer resolution is 18.2 times a second. That isn't a very high resolution and likely calling Int 1ah/ah=0 one after the other will result in the same number being returned or the second call having a higher chance of returning a higher value than the first. This can be resolved by creating a PRNG (like a simple LCG) and use the timer value once to seed it. For each value you need - you query the PRNG for the next value not the system time.

A simple LCG based PRNG can be found in this related Stackoverflow Answer. Based on that answer you can create an srandsystime function to seed the PRNG with the timer ticks, and a rand() function that returns the next value from the PRNG. The code below demonstrates setting the seed once, and then displaying two random values between 1 and 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

Footnotes:

  • 1To get a random number in the range lower to upper (inclusive) you can use this general formula:

    rndvalue = (rand() % (upper-lower+1)) + lower;

  • Deficiency: converting the random value from the PRNG to a number between 1 and 10 still suffers from modulo bias.

  • I use Watcom's register calling convention (Page 12 for a description) when developing 16-bit assembly routines in general. That can be tailored to one's own needs.

  • This particular LCG PRNG has a period of about 65536 before the pattern repeats. This should be enough for most simple tasks.

Strontian answered 2/12, 2017 at 22:55 Comment(5)
The Int 21/ah=2 comment doesn't match the code. I think you mean int 1a/ah=0. (And BTW, xor-zeroing a partial register like ah has no benefit on most CPUs. mov ah,0 is probably best on Haswell/Skylake, but xor ax,ax might have advantages on Core2 or maybe Sandybridge. I don't think xor ah,ah is ever a win, because the only CPUs that handle xor-zeroing without an execution unit (unlike the also-2-byte mov ah,2) are Sandybridge-family. On HSW xor ah,ah is not even dep-breaking. Probably also SnB itself.Margeret
@peterCordes That was a remnant from a copy and paste I didn't intend. The previous version was someone else's that was even worse at mov ah, 0. It was intended to be AX and yes the comment is incorrect.Strontian
Actually re-reading my previous comment, xor ax,ax is probably good, not mov ah,0, on Haswell. It avoids writing AH separately, which causes a front-end stall (of 1 cycle) when inserting a merge uop if you read AX. xor ax,ax is not dep-breaking or a zeroing idiom on HSW, but it's very good for Core2.Margeret
If you are doing real mode the usual option is to go for size over speed. I would never have coded mov ax, 0 in the old days if size was the issue (and often it was). You'd also see people use pusha popa to push everything and pop everything (poor performance) if the resulting code was smaller.Strontian
mov ah,0 is 2 bytes, but yes 3-byte mov ax,0 looks wrong to me, too, even though modern CPUs might actually favour it for speed over size. There's a 2-byte mov r8, imm8 using opcodes B0-B7, like the mov r16/32, imm16/32 using opcodes B8-BF. Separate from the 3+ byte mov r/m8, imm8.Margeret
A
3

A standard PC system is configured to have the PIT channel 0 fire 18.2 times per second. Each time it fires, an interrupt occurs, the BIOS counts how many times this happened since the system booted and stores this number in the BIOS data area at address 0040:006c. You can use this value as a “random” seed value for a PRNG for your program:

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

Since this value changes so often, it should appear to be random. I recommend you to shuffle it around a little so it doesn't increment with every call. (i.e. seed a PRNG and run it a few iterations).

I am not sure if Emu8086 emulates this correctly, but on a PC it should work.

Alcaic answered 2/12, 2017 at 17:23 Comment(13)
IDK why this was downvoted; looks like a sane way to get some randomness without using any software-interrupt (int) instructions. I added some details about using the time as a seed for a random number generator to make a sequence of increasing values into a really random sequence.Margeret
In EMU8086 address 46ch always has 0 because the int 70h (IRQ0) support doesn't exist. As well EMU8086 doesn't have support for the int 1ah interrupts (the ones that support the system time interface)Strontian
@MichaelPetch That's a problem indeed. Deleting my answer.Alcaic
@MichaelPetch I did not think so. However, my answer is clearly not helpful to OP as he is expected to write a program for EMU8086 where this approach clearly fails.Alcaic
@MichaelPetch This is probably an exercise for class and OP is supposed to write his own random number generator, which is why no interrupts may be used.Alcaic
Found a solution for you. I didn't try every scenario previously and missed something. Although there is nothing at address 46ch in EMU8086, and there is no CMOS system clock access there is in fact one int 1ah routine that does work - that is int 1ah/ah=0 which returns the clock ticks since midnight (what you'd expect from reading 46ch in a normal environment). That means that DX could be used as a source of a changing value.Strontian
I'm guessing since EMU8086 doesn't emulate traditional external interrupts that 46ch never gets updated (remains 0). They probably programmed int 1ah/ah=0 to return some timer value from the EMU8086 host directly. Might explain why you can't do the low memory method but the int 1ah/ah=0 is allowed.Strontian
I suspect writing the random number generator (like you said) is the task, seeding it a separate issue and probably not the intent of the assignment.Strontian
@MichaelPetch OP's requirements state that his program should give a different number on every execution, so seeding is an issue. I wonder if you could do some TSR tricks to reach the desired result.Alcaic
If he's allowed to use int 1ah/ah=0 then the value in DX from that call will potentially give him a different number each time. Doing it without interrupts - not sure how he'd pull this off. I was playing around in EMU8086 and it seems the value in CX:DX pretty much is derived from the host Windows timer.Strontian
@MichaelPetch I mean, you could look up the vector in the IDT and replicate the effect of int 1ah with other instructions.Alcaic
@MichaelPetch I don't mean to hook the IRQ. I mean to manually jump to the int 1ah vector so technically, no interrupts are used.Alcaic
@MichaelPetch That's the point why I am always asking, what the exact restrictions are. OP has not answered to any of my comments and has been somewhat vague for the entire time.Alcaic

© 2022 - 2024 — McMap. All rights reserved.