Displaying Time in Assembly
Asked Answered
S

3

4

Hello im trying to display the actual time hours/minutes/seconds this is my code sample:

MOV AH, 2Ch
INT 21h

MOV AH, 0Eh

MOV AL, CH
INT 10h

MOV AL, 3Ah
INT 10h

MOV AL, CL
INT 10h

MOV AL, 3Ah
INT 10h

MOV AL, DH
INT 10h

ret

Here you can se what the console is displaying

enter image description here

Spall answered 10/5, 2016 at 5:16 Comment(3)
was it 2:14:02? :)Roar
yes how can i get an output in numbers? :sSpall
I've added emu8086 to the tags given that the screen output you posted is for that emulator.Hadji
S
4

See the tag wiki for the instruction set reference manual, and many good links to reference material and tutorials.


It takes enough code to split up an integer into ASCII digits that you should factor it out into a function.

This is an optimized and bugfixed version of @hobbs's print2Digits function. (I also bugfixed the version in his answer, so it's correct too, but left the optimizations for this one).

print2Digits:
    ;; input in AL (0-99).  (Or preferably already zero-extended to AX so we can omit CBW)
    ;; clobbers AX and DX
    cbw             ; zero AH.  Sign-extending AL does the job because AL is only allowed to be 0-99.

    mov   dl, 10
    div   dl        ; quotient in AL(first (high) digit), remainder in AH(second (low) digit)

    add   ax, 0x3030  ; add '0' to al and ah at the same time.
    mov   dl, ah      ; save the 2nd digit

    mov   ah, 0x0E   ; BIOS call #: print single character
    int   0x10       ; print high digit first.  Doesn't clobber anything, so AH still holds 0x0E after
    mov   al, dl
    int   0x10       ; print the low digit 2nd
    ret

Since we used div to split an integer into two base10 digits, we need ah to be zero. i.e. for the dividend to be in AX, not just AL with possible garbage in AH. We could save the cbw or mov ah,0 if the caller did movzx ax, ch or something to zero ah.

(Except that 8086 doesn't have movzx, so you'd actually want xor ax,ax / mov al, ch.)

There's a DOS system call for printing a whole string, so you could store characters into a small buffer and print them all at once, like I do in this AMD64 Linux FizzBuzz. See also How do I print an integer in Assembly Level Programming without printf from the c library? for a more general int->string in a buffer function, or other multi-digit number links in the x86 tag wiki


It's also possible to use aam to divide AL (instead of AX) by 10, avoiding the need to zero AH first. It's slightly faster than div r8 on current Intel and AMD CPUs. However, it puts the results in the opposite registers from div, which means extra instructions after the aam. This balances out the saving on the mov dl, 10 and cbw.

print2Digits:
    ;; input in AL (0-99).  (Ignores AH because we use AAM instead of div)
    ;; clobbers AX and DX
    aam               ; like `div` by 10, but with the outputs reversed, and input from AL only
          ;; quotient in AH (high digit), remainder in AL(low digit).  (Opposite to div)

    add   ax, 0x3030  ; add '0' to al and ah at the same time.
    mov   dl, al      ; save the low digit
    mov   al, ah      ; print high digit first

    mov   ah, 0x0E    ; BIOS call #: print single character
    int   0x10        ; print first digit.  Doesn't clobber anything, so AH still holds 0x0E after
    mov   al, dl
    int   0x10        ; print second digit
    ret

Even if we wanted to store to a string (and make one call to a print-string function or system call), we'd have to swap al and ah before storing AX to memory (e.g. xchg al,ah, or more efficiently on modern hardware but requiring 186: rol ax,8). div produces them in the right order inside AX.


For 386 where 32bit address-size is available, we can save one instruction:

lea   dx, [eax + 0x3030]   ; need a 32bit addressing mode to use eax as a source reg.  Adds '0' to both digits at once, with a different destination.
mov   al, dh               ; then get ready to print the high byte first

The lea needs an address-size prefix and a 2-byte mod/rm, and a 32bit displacement, so it loses badly on code-size, but it does save one instruction.

Using lea to read from eax after div writes ax will probably be faster on Sandybridge-family CPUs, esp. Haswell and later, but on Intel pre-SnB, the partial register stall will make it better to use the pure 16bit version with separate add and mov instructions.

Of course if you actually cared about performance, you'd use a multiplicative inverse instead of actually dividing by 10. And you usually wouldn't be writing 16-bit code that makes legacy BIOS calls either!

Santoro answered 10/5, 2016 at 6:54 Comment(5)
Given that that is this is an 8086 tagged question, and that the emulator (emu8086) is limited to 8086 plus a few 80186 instructions I recommend at least uncommenting the 16-bit code and comment out the 32-bit code. emu8086 and the 8086 processor have no 32-bit general purpose registers.Hadji
@MichaelPetch: thanks, I didn't read the tags, only flipped down to read the answers after seeing the question was trivial. Fixed.Santoro
Thanks helped a lot!Spall
@PeterCordes I edited your answer. The aam instruction leaves the quotient in AH and the remainder in AL.Perfuse
@Peter Cordes: "[NEC] V20 always assumes that AAD/AAM/AAS argument is 10." vcfed.org/forum/archive/index.php/t-34519.htmlTantalus
R
4

You need a print routine to print bytes as numbers, instead of writing them directly to the screen as characters. Luckily since you only have to deal with values between 0 and 59, and since you want leading zeroes, the problem is pretty simple. Assuming value to be printed in AX:

print2Digits:
    ;; input in AX (0-99)
    ;; clobbers AX and DX, save them if needed
    MOV   DL, 0Ah ; divide by: 10
    DIV   DL      ; first digit in AL (quotient), second digit in AH (remainder)
    MOV   DX, AX  ; save the digits
    ADD   AL, 30h ; ASCII '0'
    MOV   AH, 0Eh ; set up print
    INT   10h     ; print first digit.
    MOV   AL, DH  ; retrieve second digit
    ADD   AL, 30h
    INT   10h     ; print it
    RET
Roar answered 10/5, 2016 at 5:37 Comment(10)
I don't understand very much, in wich lines do i have to call the funciont print2digits? (also when i write move AX, CH i get wrong parameters.)Spall
Fun fact, the obsolete AAM instruction is a shortcut for dividing al by 10. It's like div by an immediate divisor, with the input only from al instead of ax. It's actually slightly faster than div r8 on Haswell. You could optimize your code by doing add ax, 0x3030 after div, to convert both digits to ASCII with on insn. There's a problem, though: DI is a 16bit register, so you're actually doing div r16, dividing dx:ax by di. And mov di, ah won't assemble.Santoro
edx or ecx are good choices for scratch registers, being call-clobbered in most ABIs. They have byte components that are accessible outside of 64bit mode. (In 64bit mode, DIL is the low 8 of RDI, but it requires a REX prefix to access).Santoro
I got a bit carried away with my edit, let me know if you'd rather I put my changes in my own answer.Santoro
@PeterCordes the first part of the edit is fine. The second part is your work, so you should put it in your own answer and get credit for it. Also, it uses protected mode registers so I think it should be separate :)Roar
32bit registers are separate from protected mode, even though they were both introduced with 386. You can use them in 16bit mode with an operand-size prefix. My code should assemble just fine with BITS 16. (Fun fact which I just discovered, 286 had something called Protected Mode. The usual meaning is the 386 version, though, with the default operand-size being 32bit.)Santoro
Thanks helped a lot!Spall
@PeterCordes The edit you've made to this answer will display the numbers with inverted digits! After a normal div the first digit must come from the quotient in AL.Perfuse
@Roar I edited the answer. Digits came out inverted!Perfuse
@SepRoland: oops! I was thinking little-endian smallest-digit-first, not printing order. I may also have gotten myself confused playing with aam, since I had been thinking it was just like div, but actually it puts the quotient in ah. Thanks, now to fix at least the comments in my own answer.Santoro
S
4

See the tag wiki for the instruction set reference manual, and many good links to reference material and tutorials.


It takes enough code to split up an integer into ASCII digits that you should factor it out into a function.

This is an optimized and bugfixed version of @hobbs's print2Digits function. (I also bugfixed the version in his answer, so it's correct too, but left the optimizations for this one).

print2Digits:
    ;; input in AL (0-99).  (Or preferably already zero-extended to AX so we can omit CBW)
    ;; clobbers AX and DX
    cbw             ; zero AH.  Sign-extending AL does the job because AL is only allowed to be 0-99.

    mov   dl, 10
    div   dl        ; quotient in AL(first (high) digit), remainder in AH(second (low) digit)

    add   ax, 0x3030  ; add '0' to al and ah at the same time.
    mov   dl, ah      ; save the 2nd digit

    mov   ah, 0x0E   ; BIOS call #: print single character
    int   0x10       ; print high digit first.  Doesn't clobber anything, so AH still holds 0x0E after
    mov   al, dl
    int   0x10       ; print the low digit 2nd
    ret

Since we used div to split an integer into two base10 digits, we need ah to be zero. i.e. for the dividend to be in AX, not just AL with possible garbage in AH. We could save the cbw or mov ah,0 if the caller did movzx ax, ch or something to zero ah.

(Except that 8086 doesn't have movzx, so you'd actually want xor ax,ax / mov al, ch.)

There's a DOS system call for printing a whole string, so you could store characters into a small buffer and print them all at once, like I do in this AMD64 Linux FizzBuzz. See also How do I print an integer in Assembly Level Programming without printf from the c library? for a more general int->string in a buffer function, or other multi-digit number links in the x86 tag wiki


It's also possible to use aam to divide AL (instead of AX) by 10, avoiding the need to zero AH first. It's slightly faster than div r8 on current Intel and AMD CPUs. However, it puts the results in the opposite registers from div, which means extra instructions after the aam. This balances out the saving on the mov dl, 10 and cbw.

print2Digits:
    ;; input in AL (0-99).  (Ignores AH because we use AAM instead of div)
    ;; clobbers AX and DX
    aam               ; like `div` by 10, but with the outputs reversed, and input from AL only
          ;; quotient in AH (high digit), remainder in AL(low digit).  (Opposite to div)

    add   ax, 0x3030  ; add '0' to al and ah at the same time.
    mov   dl, al      ; save the low digit
    mov   al, ah      ; print high digit first

    mov   ah, 0x0E    ; BIOS call #: print single character
    int   0x10        ; print first digit.  Doesn't clobber anything, so AH still holds 0x0E after
    mov   al, dl
    int   0x10        ; print second digit
    ret

Even if we wanted to store to a string (and make one call to a print-string function or system call), we'd have to swap al and ah before storing AX to memory (e.g. xchg al,ah, or more efficiently on modern hardware but requiring 186: rol ax,8). div produces them in the right order inside AX.


For 386 where 32bit address-size is available, we can save one instruction:

lea   dx, [eax + 0x3030]   ; need a 32bit addressing mode to use eax as a source reg.  Adds '0' to both digits at once, with a different destination.
mov   al, dh               ; then get ready to print the high byte first

The lea needs an address-size prefix and a 2-byte mod/rm, and a 32bit displacement, so it loses badly on code-size, but it does save one instruction.

Using lea to read from eax after div writes ax will probably be faster on Sandybridge-family CPUs, esp. Haswell and later, but on Intel pre-SnB, the partial register stall will make it better to use the pure 16bit version with separate add and mov instructions.

Of course if you actually cared about performance, you'd use a multiplicative inverse instead of actually dividing by 10. And you usually wouldn't be writing 16-bit code that makes legacy BIOS calls either!

Santoro answered 10/5, 2016 at 6:54 Comment(5)
Given that that is this is an 8086 tagged question, and that the emulator (emu8086) is limited to 8086 plus a few 80186 instructions I recommend at least uncommenting the 16-bit code and comment out the 32-bit code. emu8086 and the 8086 processor have no 32-bit general purpose registers.Hadji
@MichaelPetch: thanks, I didn't read the tags, only flipped down to read the answers after seeing the question was trivial. Fixed.Santoro
Thanks helped a lot!Spall
@PeterCordes I edited your answer. The aam instruction leaves the quotient in AH and the remainder in AL.Perfuse
@Peter Cordes: "[NEC] V20 always assumes that AAD/AAM/AAS argument is 10." vcfed.org/forum/archive/index.php/t-34519.htmlTantalus
D
2

You are trying to display the values (time) in CH, CL and DH registers as aschi characters.

Lets say your CH contains the value 15h.

Now when you put this value in to AL and call int 10h, it displays the aschi character that matches the value 15h. If you want to show the decimal digits for 15h, then you will have to call a display routine which would break the value in the register and extracts the digits in it to display - not just the aschi representation in that value.

Lets say you have decimal 85 in a register. Now you need to print on screen "8" and "5". So you have to convert value 85 in to to aschi-56("8") and aschii-53("5"). Then put them in registers one after another and call int 10 twice to display "85".

hobbs answer shows how to do that.

Here is another tutorial

Duty answered 10/5, 2016 at 6:34 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.