Displaying characters with DOS or BIOS

All of the forementioned functions are unique in what they accomplish, but
at first the abundance does seem somewhat exagerated.

  • Int 21h AH=02h Write Character To Standard Output
    This function interprets the character codes 7 (Beep), 8 (Backspace), 9 (Tab),
    10 (Linefeed), and 13 (Carriage return). All other character codes are
    displayed.
    Backspace is nondestructive meaning that the cursor moves one position to the
    left without erasing what is underneath. Backspacing stops at the left edge
    of the screen.
    Tabs are expanded by this function. Tab expansion is the process of replacing
    ASCII 9 by a series of one or more spaces (ASCII 32) until the cursor reaches
    a column position that is a multiple of 8.
    Linefeed moves the cursor one line down, scrolling the screen if required.
    Carriage return moves the cursor to the far left of the screen.

  • Int 21h AH=06h Direct Console Output
    Very similar to function 02h, but not well suited for general use as it is
    not possible to output character 255. FYI a legal character in FAT filenames.
    It would seem that it solely exists to avoid ctrlC/
    ctrlBreak checking.

  • Int 21h AH=09h Write String To Standard Output
    The string version of function 02h, but with the inability to output character
    36 since that one is used as the string terminator. This is a major drawback
    since character 36 ($) not only is a well-known currency symbol, but also
    a legal character in FAT filenames.

  • Int 21h AH=40h Write To File Or Device
    When used with predefined handle 1 this function outputs to STDOUT which
    defaults to the screen. So here’s one more possibility for displaying
    characters. Note however that it does not rely on a string terminating
    character but rather a length. Certainly the most complete output function.
    It interprets what needs to be interpreted, it does not exclude certain
    characters and it allows to catch redirection errors although the latter involves the use of an Int 24h handler.

  • Int 10h AH=09h Write Character And Attribute At Cursor Position
    None of the character codes gets interpreted, they’re all displayed on screen.
    In text mode the provided attribute byte produces both foreground- and
    background colors, but in graphics mode you can only get a foreground color.
    The cursor position does not change. Pity! 1

  • Int 10h AH=0Ah Write Character Only At Cursor Position
    Similar to function 09h in text mode but omitting the attribute byte.
    In graphics mode this function is identical to function 09h.

  • Int 10h AH=0Eh Teletype Output
    This function interprets the character codes 7 (Beep), 8 (Backspace),
    10 (Linefeed), and 13 (Carriage return). All other character codes are
    displayed. Too bad this function does not expand tabs!

  • Int 10h AH=13h Write String
    To some degree this is the string version of function 0Eh. However for
    general use the lack of tab expansion is certainly a limitation.
    And why so many parameters? 2


Which function to choose then entirely depends on what kind of program you’re
writing. Basically there’s a choice between a console application and a full
screen application. Tools like CHKDSK.EXE or TREE.COM are console applications.
Programs like QBASIC.EXE or NE.COM are full screen applications.

A console application:

  • does not care about using color
  • writes its output on the screen in a linear fashion
  • does not hinder the OS’s capability for output redirection
  • usually performs a single task
  • often terminates in the blink of an eye

A screen oriented application:

  • benefits greatly from using the right amount of color
  • wanders over the screen and writes what it wants where it wants it
  • needs not to worry about output redirection as the above will soon enough
    render such output unreadable
  • often performs (too) many tasks
  • goes on until you decide it’s time to exit

Console Application

Only the DOS output functions offer the required/recommended redirection
capability. Output function 02h is perfect. Even though it lacks its own error
reporting for when an error (very unlikely) should occur while output is
redirected, the default critical error message of “Abort, Retry, Fail?”
doesn’t look too much out of place. (Had this been a full screen application,
the same message would have disrupted the screen enormously.)

; IN (ds:si) OUT ()
WriteStringDOS:
      pusha
      jmps    .b
.a:   mov     dl,al
      mov     ah,02h
      int     21h             ;DOS.DisplayCharacter -> AL
.b:   lodsb
      test    al,al
      jnz     .a
      popa
      ret

Sometimes however you’ll want to display a temporary item like:

  • a prompt of some kind (“– More –“, “Strike a key when ready…”, etc. )
  • a running counter/percentage
  • a progression bar

In order to avoid messing up any redirected output, it’s best to not use DOS
output functions on these temporary items. Better use the WriteStringBIOS
code that comes next.

Full Screen Application

Now output redirection is your enemy, so don’t use any of the DOS output functions.
If you don’t need the color then next code snippet is for you. It basically
adds tab expansion to the BIOS Teletype function.

; IN (ds:si) OUT ()
WriteStringBIOS:
      pusha
      mov     bx,0007h        ;Display page 0, Color if graphics mode
      jmps    .b
.a:   cmp     al,9
      je      .Tab
      mov     ah,0Eh
      int     10h             ;BIOS.Teletype
.b:   lodsb
      test    al,al
      jnz     .a
      popa
      ret
.Tab: mov     ax,0E20h        ;Start displaying space(s)
      int     10h             ;BIOS.Teletype
      mov     ah,03h
      int     10h             ;BIOS.GetCursor -> CX DX
      test    dl,7
      jnz     .Tab            ;Column not yet multiple of 8
      jmps    .b

Most of the time a bit of color will work wonders. Following code snippets use
BIOS function 09h for outputting the colored character and BIOS function 0Eh to
advance the cursor. A good combination that keeps things simple.

Use the first one in text video mode:

; IN (bl,ds:si) OUT ()
WriteStringWithAttributeTVM:
      pusha
      mov     bh,0            ;Display page 0
      jmps    .d
.a:   cmp     al,9
      je      .Tab
      cmp     al,13
      ja      .b
      mov     cx,1101_1010_0111_1111b
      bt      cx,ax
      jnc     .c              ;7,8,10,13 don't need the color
.b:   mov     cx,1
      mov     ah,09h
      int     10h             ;BIOS.WriteCharacterAndAttribute
.c:   mov     ah,0Eh
      int     10h             ;BIOS.Teletype
.d:   lodsb
      test    al,al
      jnz     .a
      popa
      ret
.Tab: mov     cx,1            ;Start displaying colored space(s)
      mov     ax,0920h        ;ASCII 20h is space character
      int     10h             ;BIOS.WriteCharacterAndAttribute
      mov     ah,0Eh
      int     10h             ;BIOS.Teletype
      mov     ah,03h
      int     10h             ;BIOS.GetCursor -> CX DX
      test    dl,7
      jnz     .Tab            ;Column not yet multiple of 8
      jmps    .d

Use the second one in 16 color graphics video mode. It’s a bit more involved
since BIOS refuses to draw background colors.

; IN (bl,ds:si) OUT ()
WriteStringWithAttributeGVM:
      pusha
      mov     bh,0            ;Display page 0
      mov     bp,bx
      jmps    .d
.a:   cmp     al,9
      je      .Tab
      cmp     al,13
      ja      .b
      mov     cx,1101_1010_0111_1111b
      bt      cx,ax
      jnc     .c              ;7,8,10,13 don't need the color
.b:   push    ax
      mov     cx,1
      mov     bx,bp
      shr     bl,4            ;Get background color (high nibble)
      mov     ax,09DBh        ;ASCII DBh is full block character
      int     10h             ;BIOS.WriteCharacterAndAttribute
      xor     bx,bp           ;Anticipate upcoming 'xor'
      and     bl,15           ;Get foreground color (low nibble)
      or      bl,128          ;Have BIOS 'xor' it
      pop     ax
.c:   mov     ah,0Eh
      int     10h             ;BIOS.Teletype
.d:   lodsb
      test    al,al
      jnz     .a
      popa
      ret
.Tab: mov     cx,1            ;Start displaying colored space(s)
      mov     bx,bp
      shr     bl,4            ;Get background color
      mov     ax,0EDBh        ;ASCII DBh is full block character
      int     10h             ;BIOS.Teletype
      mov     ah,03h
      int     10h             ;BIOS.GetCursor -> CX DX
      test    dl,7
      jnz     .Tab            ;Column not yet multiple of 8
      jmps    .d

In summary

  • For a console application the WriteStringDOS and WriteStringBIOS
    procedures are more than adequate3.
  • For a full screen application the WriteStringWithAttributeTVM and
    WriteStringWithAttributeGVM procedures equally deliver3
    4.
  • Neither DOS nor BIOS are sufficiently equipped to handle the graphics video
    modes. Either write your own graphics routines (not a trivial task!) or use
    a 3rd party graphics library.

1 Long delayed feature request: Making the cursor advance upon
receiving a replication count of zero.
2 Retorical, not an actual question.
3 Unless you select a video mode for which the BIOS has no TTY
support. eg. Many BIOSes can’t scroll when in a VESA video mode. I even came
across a BIOS that is unable to write characters with function 09h on the legacy graphics video mode 12h!
4 Writing directly in the video memory is possible but requires more
effort.

Leave a Comment