3

I have a piece of code that calls a BIOS text output function, INT 0x10, AH=0x13:

static void __stdcall print_raw(uint8_t row, uint8_t col, uint8_t attr,
                               uint16_t length, char const *text)
{
    __asm__ __volatile__ (
        "xchgw %%bp,%%si\n\t"
        "int $0x10\n\t"
        "xchgw %%bp,%%si\n\t"
        :
        : "d" (row | (col << 8)), "c" (length), "b" (attr), "a" (0x13 << 8), "S" (text)
    );
}

I have another function which can print a number on the screen:

void print_int(uint32_t n)
{
    char buf[12];
    char *p;
    p = buf + 12;
    do {
        *(--p) = '0' + (n % 10);
        n /= 10;
    } while (p > buf && n != 0);

    print_raw(1, 0, 0x0F, (buf + 12) - p, p);
}

I spent significant time trying to figure out why nothing was coming up on the screen. I dug into it and looked at the code generated for print_int:

    .globl  print_int
    .type   print_int, @function
print_int:
.LFB7:
    .cfi_startproc
    pushl   %ebp
    .cfi_def_cfa_offset 8
    .cfi_offset 5, -8
    movl    $10, %ecx
    movl    %esp, %ebp
    .cfi_def_cfa_register 5
    pushl   %esi
    pushl   %ebx
    .cfi_offset 6, -12
    .cfi_offset 3, -16
    leal    -8(%ebp), %esi
    leal    -20(%ebp), %ebx
    subl    $16, %esp
    movl    8(%ebp), %eax
.L4:
    xorl    %edx, %edx
    decl    %esi
    divl    %ecx
    testl   %eax, %eax
    je  .L6
    cmpl    %ebx, %esi
    ja  .L4
.L6:
    leal    -8(%ebp), %ecx
    movl    $4864, %eax
    movb    $15, %bl
    movl    $1, %edx
    subl    %esi, %ecx
#APP
# 201 "bootsect.c" 1
    xchgw %bp,%si
    int $0x10
    xchgw %bp,%si

# 0 "" 2
#NO_APP
    addl    $16, %esp
    popl    %ebx
    .cfi_restore 3
    popl    %esi
    .cfi_restore 6
    popl    %ebp
    .cfi_restore 5
    .cfi_def_cfa 4, 4
    ret

If you look closely at the loop at L4, you'll see that it never stores anything into buf. It has omitted the instruction! it just divides it down and never stores any characters into the buffer.

doug65536
  • 6,562
  • 3
  • 43
  • 53
  • Most importantly, you can't call BIOS functions from 32-bit protected mode code directly. You can only call them from 16-bit real mode code or from virtual 8086 mode. – Alexey Frunze Oct 22 '16 at 08:59
  • @AlexeyFrunze This is running in real mode. – doug65536 Oct 22 '16 at 10:31
  • With extended segment limits or not? – Alexey Frunze Oct 22 '16 at 11:21
  • I wonder what happens when the compiler generates jump tables (for switch statements). The code must be crashing. – Alexey Frunze Oct 22 '16 at 11:23
  • @AlexeyFrunze No, it works. The linker resolves everything to addresses low enough to make sense. It all has to fit within 64KB (unless you use inline assembly and change the segment registers briefly). The `.code16gcc` directive tells the assembler to automatically use addr16 and data16 prefixes accordingly to make the code work in 16-bit mode. This is just bootstrapping code, it doesn't have to do much. You need a custom linker script to make these low addresses happen. – doug65536 Oct 22 '16 at 13:30
  • @AlexeyFrunze It doesn't generate true 16-bit code, it generates 32 bit code that runs in real mode. The addresses happen to be low enough to be within the 0xFFFF implicit segment limit in real mode. It still uses 32 bit addressing modes and 32 bit registers a lot. But instead of producing a prefix for 16 bit addressing modes and data, it produces a prefix for 32 bit addressing modes and data. – doug65536 Oct 22 '16 at 13:38
  • You may be interested in my [Smaller C](https://github.com/alexfru/SmallerC) compiler. – Alexey Frunze Oct 23 '16 at 05:27

1 Answers1

3

The optimizer can cause this sort of incorrect code when using __asm__ statements. You need to be very careful about your constraints. In this case, the compiler didn't "see" that I was accessing the memory through the pointer in esi, as specified in "S" (text) input constraint.

The solution is to add a "memory" clobber to the clobber section of the __asm__ statement:

__asm__ __volatile__ (
    "xchgw %%bp,%%si\n\t"
    "int $0x10\n\t"
    "xchgw %%bp,%%si\n\t"
    :
    : "d" (row | (col << 8)), "c" (length), "b" (attr), "a" (0x13 << 8), "S" (text)
    : "memory"
);

This tells the compiler that you depend upon memory values, and you may change memory values, so it should be paranoid about making sure memory is up to date before the assembly statement executes, and to make sure not to rely on any memory values it may have cached in registers. It is necessary to prevent the compiler from eliding the stores to buf in my code.

doug65536
  • 6,562
  • 3
  • 43
  • 53
  • I'm kinda surprised that compiles. You are both using ("S") and clobbering `esi`, which normally gives an error. And since `si` doesn't actually appear to get changed here, I'm not sure why you clobber it. What's more, it should be possible to use [local register variables](https://gcc.gnu.org/onlinedocs/gcc/Local-Register-Variables.html) to set `bp` and save yourself a register and 2 instructions. FWIW. – David Wohlferd Oct 22 '16 at 09:08
  • @DavidWohlferd ah good notice, I guess it is because Doug's code is 16-bit assembly and compiler doesn't realize that "S" and "esi" essentially points to the same thing... – Antti Haapala -- Слава Україні Oct 22 '16 at 09:40
  • @DavidWohlferd Yeah, I think I can remove the "esi" clobber, I added that when I was trying to figure out why it didn't work, and it ended up staying there accidentally. – doug65536 Oct 22 '16 at 10:30
  • 1
    As for the 2 `xchgw`, I expect you could do something like `register char *foo asm ("bp") = text; __asm__ __volatile__ ("int $0x10" : : "d" (row | (col << 8)), "c" (length), "b" (attr), "a" (0x13 << 8), "r" (foo) : "memory");`. Keeping your asm to a minimum tends to produce more efficient code and fewer bugs. – David Wohlferd Oct 22 '16 at 22:23
  • BTW, there's now a canonical Q&A for this: [How can I indicate that the memory \*pointed\* to by an inline ASM argument may be used?](https://stackoverflow.com/q/56432259) – Peter Cordes May 10 '22 at 20:21