x86 32-bit machine code, 24 21 bytes
changelog: -3 bytes: replace standard add/cmp/jbe/add with a DAS hack by @peter ferrie
64-bit: still 24 bytes. Long mode removed the DAS opcode.
16-bit mode: the default operand-size is 16-bit but the problem spec is inherently 32-bit. Including hard-coded 8 hex digits.
Byte-reverse with bswap
then manual int->hex in standard order (most-significant nibble first, writing hex digits to a char output buffer in ascending order.) This avoids needing to unroll the loop to switch order between nibbles within a byte vs. across bytes.
Callable as void lehex(char buf[8] /*edi*/, uint32_t x /*esi*/);
like x86-64 System V, except this doesn't work in 64-bit mode. (It needs the output pointer in EDI for stosb
. The input number can be in any register other than ECX or EAX.)
1 lehex:
2 00000000 0FCE bswap esi
3 00000002 6A08 push 8 ; 8 hex digits
4 00000004 59 pop ecx
5 .loop: ;do{
6 00000005 C1C604 rol esi, 4 ; rotate high nibble to the bottom
7
8 00000008 89F0 mov eax, esi
9 0000000A 240F and al, 0x0f ; isolate low nibble
10 0000000C 3C0A cmp al, 10 ; set CF according to digit <= 9
11 0000000E 1C69 sbb al, 0x69 ; read CF, set CF and conditionally set AF
12 00000010 2F das ; magic, which happens to work
13
14 00000011 AA stosb ; *edi++ = al
15 00000012 E2F1 loop .loop ; }while(--ecx)
16
17 00000014 C3 ret
size = 0x15 = 21 bytes.
TIO FASM 32-bit x86 test case with an asm caller that uses a write
system call to write the output after calling it twice to append 2 strings into a buffer. Tests all hex digits 0..F, including 9 and A at the boundary between numeral vs. letter.
The DAS
hack - x86 has a half-carry flag, for carry out of the low nibble. Useful for packed-BCD stuff like the DAS instruction, intended for use after subtracting two 2-digit BCD integers. With the low nibble of AL being outside the 0-9 range, we're definitely abusing it here.
Notice the if (old_AL > 99H) or (old_CF = 1)
THEN AL ← AL − 60H;
part of the Operation section in the manual; sbb always sets CF here so that part always happens. That and the ASCII range for upper-case letters is what motivates the choice of sbb al, 0x69
cmp 0xD, 0xA
doesn't set CF
- sbb
0xD - 0x69
wraps to AL=0xA4
as input to DAS. (And sets CF, clears AF)
- no AL -= 6 in the first part of DAS (because 4 > 9 is false and AF=0)
- AL -= 0x60 in the second part, leaving
0x44
, the ASCII code for 'D'
vs. a numeral:
cmp 0x3, 0xA
sets CF
- sbb
3 - 0x69 - 1
= AL = 0x99 and sets CF and AF
- AL -= 6 in the first part of DAS (9 > 9 is false but AF is set), leaving 0x93
- AL -= 0x60 in the second part, leaving 0x33, the ASCII code for
'3'
.
Subtracting 0x6a
in SBB will set AF for every digit <= 9 so all the numerals follow the same logic. And leave it cleared for every alphabetic hex digit. i.e. correctly exploiting the 9 / A split handling of DAS.
Normally (for performance) you'd use a lookup table for a scalar loop, or possibly a branchless 2x lea
and cmp/cmov
conditional add. But 2-byte al, imm8
instructions are a big win for code-size.
x86-64 version version: just the part that's different, between and al, 0xf
and stosb
.
;; x86-64 int -> hex in 8 bytes
10 0000000C 0430 add al, '0'
11 0000000E 3C39 cmp al, '9'
12 00000010 7602 jbe .digit
13 00000012 0427 add al, 'a'-10 - '0' ; al = al>9 ? al+'a'-10 : al+'0'
14 .digit:
Notice that the add al, '0'
always runs, and the conditional add only adds the difference between 'a'-10
and '0'
, to make it just an if
instead of if
/else
.
Tested and works, using the same main
caller as my C answer, which uses char buf[8]
and printf("%.8s\n", buf)
.