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 _itoarobi 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,edina 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/ cldkosztuje 2 bajty, stosbaby 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 movload / store (powyżej), używając lea/ movsb(schludny, ale nie optymalny), używając xchg/ xlatb/ stosb/ xchgi takiego, który wchodzi do pętli z nakładającym się instrukcją. Zobacz kod poniżej) . Ostatni potrzebuje 0znaku 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 inclub nie) oraz od tego, czy możemy założyć, że zestawy wywołujące DF = 1 ( stosbmaleją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/ scasbdział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_arglub 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_arglub skew)
x86-64 z 64-bitowymi wskaźnikami, inna obsługa DF: 16B (DF = 1 skew) , 17B ( nostringz DF = 1, używając scasbzamiast dec). 18B ( stosb_edx_argzachowanie 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_argbez inckoń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 nostringpowią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 skewz, stdale nie cld. Lub 17 + 1B dla stosb_decode_overlapz inc edina 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).stdcldscasb
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 movna 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]/ movsbjako 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, idivprodukuje resztę do późniejszych iteracji.
Drugi bajt idiv ebpto FD, który jest kodem op dla stdinstrukcji, że ta funkcja musi działać. (To było szczęśliwe odkrycie. Patrzyłem na to divwcześniej, co odróżnia się od idivużywania /rbitów w ModRM. Drugi bajt div epbdekodowania jako cmc, który jest nieszkodliwy, ale nie jest pomocny. Ale za pomocą idiv ebptego możemy faktycznie usunąć stdgó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 _startdzwoniącym, który wykorzystuje sys_writewynik)
Najlepiej jest debugować, aby uruchomić go poniżej stracelub zrzucić dane wyjściowe, aby zobaczyć, czy \0w odpowiednim miejscu znajduje się terminator i tak dalej. Ale możesz zobaczyć, że to faktycznie działa i produkować AAAAAACHOOza 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 xznakiem, po którym następowały zera.)