• Please review our updated Terms and Rules here

DOS 16 bit register usage

alank2

Veteran Member
Joined
Aug 3, 2016
Messages
2,264
Location
USA
I'm using Borland C and using the build in assembler BASM, but have some general questions.

If a function s not declared something else, is it _cdecl by default?

I read about calling conventions here:
https://en.wikipedia.org/wiki/X86_calling_conventions#cdecl

It says that: Registers EAX, ECX, and EDX are caller-saved, and the rest are callee-saved.

This would imply that BX/EBX should be callee saved. But, a stepping through a function that Borland C compiles shows it using BX in the function without any concern for changing it.

Then, from the chapter 12 of the Borland C++ 3.1 programming guide:

Inline assembly code can freely u~e SI or DI as scratch registers. If
you use SI or DI in inline assembly code, the compiler won't use
these registers for register variables.

It doesn't mention using AX, BX, CX, DX, at all. Is this because it is common knowledge? Why mention SI/DI specifically here and not others?

Then I found this:
https://www.agner.org/optimize/calling_conventions.pdf

It shows on page 10 that AX, BX, CX, DX, ES, and something called ST(0)-ST(7) are scratch registers.

Which ones should be saved if used? Maybe there is a place in the Borland documentation I am missing this.
 
It's a matter of practicality. You can't really do much in x86 code without using AX, BX, CX or DX. So they're fair game to use. Consider that AX is used as an accumulator for lots of things; DX is an extension of AX (consider register use in 16-bit MUL and DIV instructions) and CX is used as an iteration or loop counter (consider LOOP, as well as REPxx instructions). Saving those would make no sense. Of course, BP is used as the stack frame base and changes every time you call another function.

SI and DI are special, as are the segment registers. So save those if you change them. I've also seen some text that some compilers rely on the setting of the direction flag (e.g. CLD is assumed).

Otherwise, everything else should be fair game.
 
The discrepancy is because there was a change when compilers moved from 16 to 32 bits. In 16-bit code, BX is caller-saved but in 32-bit code EBX is callee-saved.
 
According to Michael Abrash's Zen of Assembly, also read elsewhere (can't remember where) and also confirmed, at least, by my experience: the only register that needs to be saved while inlining assembly on Borland/Turbo C is DS, just because global variables rely on it and the entire system could hang and/or show unexpected results. If you don't use variables on the inline assembly snippet, you don't even need to push DS. So it's safe to use any other general use register, namely AX, BX, CX, DX, and segment SI, ES, DI without needing to push them. Maybe it's not a good idea messing with critical registers as CS, BP, IP and SP... I think all of this also applies to other C compilers such as MS and Watcom, but haven't tested.

If calling an external assembly procedure, the BP must be preserved and restored upon return. If using variables, also DS.

Code:
my_procedure proc  
    push     bp                       ; preserve caller registers
    mov      bp,sp
    push     ds

    .... HERE COMES THE CODE

    pop      ds
    pop      bp
    ret
my_procedure endp
 
I ask because I am messing around with direct DOS calls and using segread and int86x seemed pretty cumbersome.

Is there anything I could do better in the following? I am using an approach of setting local variables and then referencing them in the C sections, but maybe there is a better way?

Code:
uint8_t FileOpen(uint8_t AFileNo, char *AFilename, uint8_t AMode)
{
  uint16_t ui1;

  //dos open
  asm mov ah, 0x3d
  asm mov al, 0x00
  asm mov dx, word ptr AFilename
  #if defined(__COMPACT__) || defined(__LARGE__) || defined(__HUGE__)
    asm push ds
    asm mov ds, word ptr AFilename+2
    asm int 0x21
    asm pop ds
  #else
    asm int 0x21
  #endif
  asm mov ui1, ax
  asm jc error

  //success
  fileio[AFileNo].handle=ui1;
  return 1;

  //error
  error:
  fileerror=ui1;
  return 0;
}

uint8_t FileReadWrite(uint8_t AWrite, uint8_t AFileNo, uint8_t *ABuffer, uint16_t *ASize)
{
  uint16_t ui1, ui2;

  ui1=fileio[AFileNo].handle;
  ui2=*ASize;
  if (AWrite)
    AWrite=0x40;
  else AWrite=0x3f;

  //dos read/write
  asm mov ah, AWrite
  asm mov bx, ui1
  asm mov cx, ui2
  asm mov dx, word ptr ABuffer
  #if defined(__COMPACT__) || defined(__LARGE__) || defined(__HUGE__)
    asm push ds
    asm mov ds, word ptr ABuffer+2
    asm int 0x21
    asm pop ds
  #else
    asm int 0x21
  #endif
  asm mov ui1, ax
  asm jc error

  //success
  *ASize=ui1;
  return 1;

  //error
  error:
  fileerror=ui1;
  return 0;
}
 
That's pretty close to optimal. Consider that the INT 21h open and read calls will occupy many, many more cycles than the code calling them.
 
About the only thing that jumps out is that you can use "lds dx,AfileName" in the open call for the large data cases. Save a couple of cycles, but it won't make any practical difference.
 
Back
Top