x86 32-bitowy kod maszynowy (32-bitowe liczby całkowite): 17 bajtów.
(zobacz także inne wersje poniżej, w tym 16 bajtów dla wersji 32-bitowej lub 64-bitowej, z konwencją wywoływania DF = 1).
Dzwoniący przekazuje argumenty do rejestrów, w tym wskaźnik do końca bufora wyjściowego (jak moja odpowiedź C ; zobacz to w celu wyjaśnienia i wyjaśnienia algorytmu) . Wewnętrzny glibc _itoa
robi to , więc nie jest on przeznaczony tylko do gry w golfa kodu. Rejestry przekazujące arg są zbliżone do x86-64 System V, z tym wyjątkiem, że mamy arg w EAX zamiast EDX.
Po powrocie EDI wskazuje na pierwszy bajt łańcucha C zakończonego na 0 w buforze wyjściowym. Zwykle rejestrem wartości zwracanej jest EAX / RAX, ale w języku asemblera można użyć dowolnej konwencji wywoływania dogodnej dla funkcji. ( xchg eax,edi
na końcu dodaje 1 bajt).
Dzwoniący może obliczyć jawną długość, jeśli chce, z buffer_end - edi
. Ale nie sądzę, abyśmy mogli usprawiedliwić pominięcie terminatora, chyba że funkcja faktycznie zwróci zarówno wskaźniki początkowe + końcowe, jak i wskaźnik + długość. Pozwoliłoby to zaoszczędzić 3 bajty w tej wersji, ale nie sądzę, aby było to uzasadnione.
- EAX = n = liczba do dekodowania. (Dla
idiv
. Pozostałe argumenty nie są domyślnymi operandami.)
- EDI = koniec bufora wyjściowego (wersja 64-bitowa nadal używa
dec edi
, więc musi być w niskim 4GiB)
- ESI / RSI = tablica odnośników, inaczej LUT. nie zatkany.
- ECX = długość stołu = podstawa. nie zatkany.
nasm -felf32 ascii-compress-base.asm -l /dev/stdout | cut -b -30,$((30+10))-
(Ręcznie edytowane w celu zmniejszenia komentarzy, numeracja linii jest dziwna).
32-bit: 17 bytes ; 64-bit: 18 bytes
; same source assembles as 32 or 64-bit
3 %ifidn __OUTPUT_FORMAT__, elf32
5 %define rdi edi
6 address %define rsi esi
11 machine %endif
14 code %define DEF(funcname) funcname: global funcname
16 bytes
22 ;;; returns: pointer in RDI to the start of a 0-terminated string
24 ;;; clobbers:; EDX (tmp remainder)
25 DEF(ascii_compress_nostring)
27 00000000 C60700 mov BYTE [rdi], 0
28 .loop: ; do{
29 00000003 99 cdq ; 1 byte shorter than xor edx,edx / div
30 00000004 F7F9 idiv ecx ; edx=n%B eax=n/B
31
32 00000006 8A1416 mov dl, [rsi + rdx] ; dl = LUT[n%B]
33 00000009 4F dec edi ; --output ; 2B in x86-64
34 0000000A 8817 mov [rdi], dl ; *output = dl
35
36 0000000C 85C0 test eax,eax ; div/idiv don't write flags in practice, and the manual says they're undefined.
37 0000000E 75F3 jnz .loop ; }while(n);
38
39 00000010 C3 ret
0x11 bytes = 17
40 00000011 11 .size: db $ - .start
Zaskakujące jest to, że najprostsza wersja zasadniczo bez kompromisów prędkości / rozmiaru jest najmniejsza, ale std
/ cld
kosztuje 2 bajty, stosb
aby przejść w kolejności malejącej i nadal przestrzegać wspólnej konwencji wywoływania DF = 0. (A STOS zmniejsza się po przechowywaniu, pozostawiając wskaźnik wskazujący jeden bajt za niski na wyjściu z pętli, co kosztuje dodatkowe bajty do obejścia.)
Wersje:
Wymyśliłem 4 znacząco różne sztuczki implementacyjne (używając prostego mov
load / store (powyżej), używając lea
/ movsb
(schludny, ale nie optymalny), używając xchg
/ xlatb
/ stosb
/ xchg
i takiego, który wchodzi do pętli z nakładającym się instrukcją. Zobacz kod poniżej) . Ostatni potrzebuje 0
znaku końca w tablicy odnośników, aby skopiować jako terminator łańcucha wyjściowego, więc liczę to jako +1 bajt. W zależności od wersji 32/64-bitowej (1-bajtowej inc
lub nie) oraz od tego, czy możemy założyć, że zestawy wywołujące DF = 1 ( stosb
malejąco) lub cokolwiek innego, różne wersje są (powiązane) najkrótsze.
DF = 1 do przechowywania w kolejności malejącej sprawia, że wygrana dla xchg / stosb / xchg, ale dzwoniący często tego nie chce; Wydaje się, że przenoszenie pracy na osobę dzwoniącą jest trudne do uzasadnienia. (W przeciwieństwie do niestandardowych rejestrów przechodzących argumenty i zwracających wartość, które zwykle nie kosztują dodatkowego programu wywołującego asm). Ale w 64-bitowym kodzie cld
/ scasb
działa inc rdi
, unikając obcięcia wskaźnika wyjściowego do 32-bitowego, więc czasami niewygodne jest zachowanie DF = 1 w 64-bitowych funkcjach czyszczenia. . (Wskaźniki do statycznego kodu / danych są 32-bitowe w plikach wykonywalnych innych niż PIE x86-64 w Linuksie i zawsze w ABI Linux x32, więc w niektórych przypadkach można używać wersji x86-64 używającej 32-bitowych wskaźników.) ta interakcja sprawia, że interesujące jest spojrzenie na różne kombinacje wymagań.
- IA32 z DF = 0 na konwencji wywoływania wejścia / wyjścia: 17B (
nostring
) .
- IA32: 16B (z konwencją DF = 1:
stosb_edx_arg
lub skew
) ; lub z przychodzącym DF = dontcare, pozostawiając ustawione: 16 + 1Bstosb_decode_overlap
lub 17Bstosb_edx_arg
- x86-64 z 64-bitowymi wskaźnikami i DF = 0 w konwencji wywoływania wejścia / wyjścia: 17 + 1 bajtów (
stosb_decode_overlap
) , 18B ( stosb_edx_arg
lub skew
)
x86-64 z 64-bitowymi wskaźnikami, inna obsługa DF: 16B (DF = 1 skew
) , 17B ( nostring
z DF = 1, używając scasb
zamiast dec
). 18B ( stosb_edx_arg
zachowanie DF = 1 z 3 bajtami inc rdi
).
Lub jeśli pozwolimy na powrót wskaźnika do 1 bajtu przed ciągiem, 15B ( stosb_edx_arg
bez inc
końca). Wszystko gotowe do ponownego wywołania i rozwinięcia kolejnego łańcucha do bufora z inną bazą / tabelą ... Ale to miałoby większy sens, gdybyśmy nie zachowali zakończenia 0
, a ty możesz umieścić ciało funkcji w pętli, więc to naprawdę osobny problem.
x86-64 z 32-bitowym wskaźnikiem wyjściowym, DF = 0 konwencja wywoływania: brak poprawy w stosunku do 64-bitowego wskaźnika wyjściowego, ale nostring
powiązanie 18B ( ) teraz.
- x86-64 z 32-bitowym wskaźnikiem wyjściowym: brak poprawy w stosunku do najlepszych 64-bitowych wersji wskaźnika, więc 16B (DF = 1
skew
). Lub ustawić DF = 1 i pozostawić, 17B dla skew
z, std
ale nie cld
. Lub 17 + 1B dla stosb_decode_overlap
z inc edi
na końcu zamiast cld
/ scasb
.
Z konwencją wywoływania DF = 1: 16 bajtów (IA32 lub x86-64)
Wymaga DF = 1 na wejściu, pozostawia to ustawienie. Mało prawdopodobne , przynajmniej na podstawie funkcji. Robi to samo co powyższa wersja, ale z xchg, aby uzyskać resztę do / z AL przed / po XLATB (wyszukiwanie tabeli z R / EBX jako bazą) i STOSB ( *output-- = al
).
Z normalnym DF = 0 na konwencji wejścia / wyjścia, / / wersja ma 18 bajtów dla kodu 32 i 64-bitowych i 64-bitowych jest czyste (współpracuje z 64-bitowego wskaźnika wyjściowego).std
cld
scasb
Zauważ, że argumenty wejściowe znajdują się w różnych rejestrach, w tym RBX dla tabeli (dla xlatb
). Zauważ też, że ta pętla zaczyna się od zapisania AL, a kończy na ostatnim znaku, który nie został jeszcze zapisany (stąd mov
na końcu). Pętla jest więc „wypaczona” w stosunku do innych, stąd nazwa.
;DF=1 version. Uncomment std/cld for DF=0
;32-bit and 64-bit: 16B
157 DEF(ascii_compress_skew)
158 ;;; inputs
159 ;; O in RDI = end of output buffer
160 ;; I in RBX = lookup table for xlatb
161 ;; n in EDX = number to decode
162 ;; B in ECX = length of table = modulus
163 ;;; returns: pointer in RDI to the start of a 0-terminated string
164 ;;; clobbers:; EDX=0, EAX=last char
165 .start:
166 ; std
167 00000060 31C0 xor eax,eax
168 .loop: ; do{
169 00000062 AA stosb
170 00000063 92 xchg eax, edx
171
172 00000064 99 cdq ; 1 byte shorter than xor edx,edx / div
173 00000065 F7F9 idiv ecx ; edx=n%B eax=n/B
174
175 00000067 92 xchg eax, edx ; eax=n%B edx=n/B
176 00000068 D7 xlatb ; al = byte [rbx + al]
177
178 00000069 85D2 test edx,edx
179 0000006B 75F5 jnz .loop ; }while(n = n/B);
180
181 0000006D 8807 mov [rdi], al ; stosb would move RDI away
182 ; cld
183 0000006F C3 ret
184 00000070 10 .size: db $ - .start
Podobna nieskrzywiona wersja przekracza EDI / RDI, a następnie go naprawia.
; 32-bit DF=1: 16B 64-bit: 17B (or 18B for DF=0)
70 DEF(ascii_compress_stosb_edx_arg) ; x86-64 SysV arg passing, but returns in RDI
71 ;; O in RDI = end of output buffer
72 ;; I in RBX = lookup table for xlatb
73 ;; n in EDX = number to decode
74 ;; B in ECX = length of table
75 ;;; clobbers EAX,EDX, preserves DF
76 ; 32-bit mode: a DF=1 convention would save 2B (use inc edi instead of cld/scasb)
77 ; 32-bit mode: call-clobbered DF would save 1B (still need STD, but INC EDI saves 1)
79 .start:
80 00000040 31C0 xor eax,eax
81 ; std
82 00000042 AA stosb
83 .loop:
84 00000043 92 xchg eax, edx
85 00000044 99 cdq
86 00000045 F7F9 idiv ecx ; edx=n%B eax=n/B
87
88 00000047 92 xchg eax, edx ; eax=n%B edx=n/B
89 00000048 D7 xlatb ; al = byte [rbx + al]
90 00000049 AA stosb ; *output-- = al
91
92 0000004A 85D2 test edx,edx
93 0000004C 75F5 jnz .loop
94
95 0000004E 47 inc edi
96 ;; cld
97 ;; scasb ; rdi++
98 0000004F C3 ret
99 00000050 10 .size: db $ - .start
16 bytes for the 32-bit DF=1 version
Próbowałem alternatywnej wersji tego z lea esi, [rbx+rdx]
/ movsb
jako wewnętrzną pętlę. (RSI jest resetowane przy każdej iteracji, ale zmniejsza się RDI). Ale nie może użyć xor-zero / stos do terminatora, więc jest o 1 bajt większy. (I to nie jest 64-bitowe czyszczenie dla tabeli odnośników bez prefiksu REX na LEA.)
LUT z jawną długością i terminatorem 0: 16 + 1 bajtów (32-bit)
Ta wersja ustawia DF = 1 i pozostawia to w ten sposób. Liczę dodatkowy wymagany bajt LUT jako część całkowitej liczby bajtów.
Fajną sztuczką jest tutaj dekodowanie tych samych bajtów na dwa różne sposoby . Wchodzimy w środek pętli z resztą = podstawa i iloraz = liczba wejściowa i kopiujemy terminator 0 na swoje miejsce.
Za pierwszym razem przez funkcję pierwsze 3 bajty pętli są zużywane jako wysokie bajty disp32 dla LEA. To, że LEA kopiuje bazę (moduł) do EDX, idiv
produkuje resztę do późniejszych iteracji.
Drugi bajt idiv ebp
to FD
, który jest kodem op dla std
instrukcji, że ta funkcja musi działać. (To było szczęśliwe odkrycie. Patrzyłem na to div
wcześniej, co odróżnia się od idiv
używania /r
bitów w ModRM. Drugi bajt div epb
dekodowania jako cmc
, który jest nieszkodliwy, ale nie jest pomocny. Ale za pomocą idiv ebp
tego możemy faktycznie usunąć std
górę funkcji).
Zauważ, że rejestry wejściowe są jeszcze raz różne: EBP dla bazy.
103 DEF(ascii_compress_stosb_decode_overlap)
104 ;;; inputs
105 ;; n in EAX = number to decode
106 ;; O in RDI = end of output buffer
107 ;; I in RBX = lookup table, 0-terminated. (first iter copies LUT[base] as output terminator)
108 ;; B in EBP = base = length of table
109 ;;; returns: pointer in RDI to the start of a 0-terminated string
110 ;;; clobbers: EDX (=0), EAX, DF
111 ;; Or a DF=1 convention allows idiv ecx (STC). Or we could put xchg after stos and not run IDIV's modRM
112 .start:
117 ;2nd byte of div ebx = repz. edx=repnz.
118 ; div ebp = cmc. ecx=int1 = icebp (hardware-debug trap)
119 ;2nd byte of idiv ebp = std = 0xfd. ecx=stc
125
126 ;lea edx, [dword 0 + ebp]
127 00000040 8D9500 db 0x8d, 0x95, 0 ; opcode, modrm, 0 for lea edx, [rbp+disp32]. low byte = 0 so DL = BPL+0 = base
128 ; skips xchg, cdq, and idiv.
129 ; decode starts with the 2nd byte of idiv ebp, which decodes as the STD we need
130 .loop:
131 00000043 92 xchg eax, edx
132 00000044 99 cdq
133 00000045 F7FD idiv ebp ; edx=n%B eax=n/B;
134 ;; on loop entry, 2nd byte of idiv ebp runs as STD. n in EAX, like after idiv. base in edx (fake remainder)
135
136 00000047 92 xchg eax, edx ; eax=n%B edx=n/B
137 00000048 D7 xlatb ; al = byte [rbx + al]
138 .do_stos:
139 00000049 AA stosb ; *output-- = al
140
141 0000004A 85D2 test edx,edx
142 0000004C 75F5 jnz .loop
143
144 %ifidn __OUTPUT_FORMAT__, elf32
145 0000004E 47 inc edi ; saves a byte in 32-bit. Makes DF call-clobbered instead of normal DF=0
146 %else
147 cld
148 scasb ; rdi++
149 %endif
150
151 0000004F C3 ret
152 00000050 10 .size: db $ - .start
153 00000051 01 db 1 ; +1 because we require an extra LUT byte
# 16+1 bytes for a 32-bit version.
# 17+1 bytes for a 64-bit version that ends with DF=0
Ta nakładająca się sztuczka dekodowania może być również używana z cmp eax, imm32
: zajmuje tylko 1 bajt, aby skutecznie przeskoczyć do przodu o 4 bajty, tylko flagi clobbering. (Jest to straszne z punktu widzenia wydajności procesorów, które wyznaczają granice instrukcji w pamięci podręcznej L1i, BTW).
Ale tutaj używamy 3 bajtów, aby skopiować rejestr i przejść do pętli. Zwykle zajęłoby to 2 + 2 (mov + jmp) i pozwoliłoby nam wskoczyć do pętli tuż przed STOS zamiast przed XLATB. Ale wtedy potrzebowalibyśmy osobnego STD i nie byłoby to zbyt interesujące.
Wypróbuj online! (z _start
dzwoniącym, który wykorzystuje sys_write
wynik)
Najlepiej jest debugować, aby uruchomić go poniżej strace
lub zrzucić dane wyjściowe, aby zobaczyć, czy \0
w odpowiednim miejscu znajduje się terminator i tak dalej. Ale możesz zobaczyć, że to faktycznie działa i produkować AAAAAACHOO
za wkład
num equ 698911
table: db "CHAO"
%endif
tablen equ $ - table
db 0 ; "terminator" needed by ascii_compress_stosb_decode_overlap
(W rzeczywistości xxAAAAAACHOO\0x\0\0...
, ponieważ zrzucamy z bufora 2 bajty wcześniej do stałej długości. Widzimy więc, że funkcja zapisała bajty, które powinna, i nie nadepnęła na bajty, których nie powinna. wskaźnik początkowy przekazany do funkcji był drugim przedostatnim x
znakiem, po którym następowały zera.)