4 ms·
I love using a variant on that trick in real mode code: print: pop si .loop: lodsb jz .end mov ah, 0x0E xor bh,
by messe 3y ago
I love using a variant on that trick in real mode code:
print:
pop si
.loop:
lodsb
jz .end
mov ah, 0x0E
xor bh, bh
int 10h
jmp .loop
.end:
push si
ret
I’m writing this from memory, so there may be an off by one error in the above code.
It’s used like this, with a null terminated string, rather than a hardcoded length:
call print
db "hello, world", 0
This can even be transformed into something like
puts "hello, world"
with the aid of NASM macros. I can’t recall where I saw this trick originally. Maybe some FreeDOS or GRUB code.
- stevekemp 3y agoIf first saw this in virus-code from the 80s, where you'd have code to get the current location: call next next: pop ax I've used the same approach for printing "inline" strings myself, though in my case I tend to be working with CP/M and there the string are terminated with "$".
- EvanAnderson 3y agoThis is exactly what I thought of. Learning x86 assembler in the context of reverse engineering MS-DOS made this trick seem perfectly normal (as did the idea of writing position independent code).
- stevekemp 3y agoA later comment in this discussion reminds me that this was called "calculating the delta-offset".
- EvanAnderson 3y agoYep. That's the terminology I'd expect to see in a 40Hex! >smile<
- akoboldfrying 3y agoCute! And the time overhead vs. the usual stack-based parameter passing convention is roughly zero, since even though the callee has to "push si" at the end, the caller needs zero instructions to pass the argument, instead of the usual one.
- messe 3y agoToo late for me to edit, but my code is missing a "test al, al" after "lodsb".
- amluto 3y agoThe fact that code like this would get acceptable performance is amazing! By modern standards, there’s maybe a few cycles (assuming no cache misses) in the loop body plus that INT instruction. That’s maybe 20k cycles for the round trip (read IDT and GDT, go through the whole awful ucode flow, and jump to kernel, then do the work, and the do IRET, which is, again, amazingly slow). Fortunately, I’m pretty sure the CPUs that were intended to run this code were rather more efficient at interrupts (in terms of cycles) than modern x86 monsters. Intel is at least trying to fix this with FRED.
- messe 3y ago> That’s maybe 20k cycles for the round trip (read IDT and GDT, go through the whole awful ucode flow, and jump to kernel, then do the work, and the do IRET, which is, again, amazingly slow). For comparison to the protected-mode version you've outlined: in real-mode, there's no GDT (all segments are 64 kilobytes in length, with their base address given by the value of the segment register shifted left by 4 bits), and the IDT is actually the IVT, a 1024-byte list of 256 segment:offset addresses that begins at memory address zero. There's no switching from user mode to kernel mode. And all IRET has to do is pop flags, segment and offset from the stack, and jump to it. In the code above, the biggest bottleneck would be probably be the (VGA)BIOS's implementation of the print character function, which was usually dog slow, as it would look up the current cursor position by querying VGA registers using the x86 port IO instructions, which were quite slow even at the time, and would have to do so for each character. I wouldn't call it fast, but it's good enough for displaying a few short messages onscreen in an assembly-language program, for example a bootloader.