mã máy x86 32 bit (số nguyên 32 bit): 17 byte.
(cũng xem các phiên bản khác bên dưới, bao gồm 16 byte cho 32 bit hoặc 64 bit, với quy ước gọi DF = 1.)
Người gọi chuyển các đối số trong các thanh ghi, bao gồm một con trỏ đến cuối bộ đệm đầu ra (như câu trả lời C của tôi ; xem nó để biện minh và giải thích thuật toán.) Nội bộ của glibc _itoa
thực hiện điều này , vì vậy nó không chỉ được sử dụng cho golf-code. Các thanh ghi chuyển arg gần với x86-64 System V, ngoại trừ chúng ta có một arg trong EAX thay vì EDX.
Khi trả về, EDI trỏ đến byte đầu tiên của chuỗi C kết thúc 0 trong bộ đệm đầu ra. Thanh ghi giá trị trả về thông thường là EAX / RAX, nhưng trong ngôn ngữ lắp ráp, bạn có thể sử dụng bất kỳ quy ước gọi nào thuận tiện cho một chức năng. ( xchg eax,edi
ở cuối sẽ thêm 1 byte).
Người gọi có thể tính toán độ dài rõ ràng nếu nó muốn, từ buffer_end - edi
. Nhưng tôi không nghĩ rằng chúng ta có thể biện minh cho việc bỏ qua bộ kết thúc trừ khi hàm thực sự trả về cả con trỏ bắt đầu + con trỏ kết thúc hoặc con trỏ + chiều dài. Điều đó sẽ tiết kiệm 3 byte trong phiên bản này, nhưng tôi không nghĩ nó hợp lý.
- EAX = n = số để giải mã. (Dành cho
idiv
. Các đối số khác không phải là toán hạng ngầm.)
- EDI = kết thúc bộ đệm đầu ra (phiên bản 64 bit vẫn sử dụng
dec edi
, do đó phải ở mức 4GiB thấp)
- ESI / RSI = bảng tra cứu, còn gọi là LUT. không bị tắc nghẽn.
- ECX = chiều dài của bảng = cơ sở. không bị tắc nghẽn.
nasm -felf32 ascii-compress-base.asm -l /dev/stdout | cut -b -30,$((30+10))-
(Chỉnh sửa bằng tay để thu nhỏ bình luận, đánh số dòng là lạ.)
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
Thật đáng ngạc nhiên khi phiên bản đơn giản nhất về cơ bản không có sự đánh đổi tốc độ / kích thước là nhỏ nhất, nhưng std
/ cld
tốn 2 byte để sử dụng stosb
theo thứ tự giảm dần và vẫn tuân theo quy ước gọi DF = 0 chung. (Và STOS giảm sau khi lưu trữ, khiến con trỏ chỉ một byte quá thấp khi thoát khỏi vòng lặp, khiến chúng ta phải trả thêm byte để xử lý.)
Phiên bản:
Tôi đã đưa ra 4 thủ thuật thực hiện khác nhau đáng kể (sử dụng mov
tải / lưu trữ đơn giản (ở trên), sử dụng lea
/ movsb
(gọn gàng nhưng không tối ưu), sử dụng xchg
/ xlatb
/ stosb
/ xchg
và một thủ thuật xâm nhập vào vòng lặp với một lệnh hack chồng chéo. Xem mã bên dưới) . Cái cuối cùng cần một dấu 0
trong bảng tra cứu để sao chép như là bộ kết thúc chuỗi đầu ra, vì vậy tôi đang tính đó là byte 1. Tùy thuộc vào 32/64 bit (1 byte inc
hay không) và liệu chúng ta có thể giả sử người gọi đặt DF = 1 ( stosb
giảm dần) hay bất cứ điều gì, các phiên bản khác nhau được (gắn cho) ngắn nhất.
DF = 1 để lưu trữ theo thứ tự giảm dần làm cho nó trở thành chiến thắng cho xchg / stosb / xchg, nhưng người gọi thường không muốn điều đó; Nó cảm thấy như giảm tải công việc cho người gọi một cách khó để biện minh. (Không giống như các thanh ghi giá trị chuyển tiếp và trả về giá trị tùy chỉnh, thường không tốn một người gọi asm cho bất kỳ công việc bổ sung nào.) Nhưng trong mã 64 bit, cld
/ scasb
hoạt động như inc rdi
, tránh cắt ngắn con trỏ đầu ra thành 32 bit, vì vậy đôi khi bất tiện trong việc bảo toàn DF = 1 trong các chức năng 64-bit. . (Con trỏ tới mã / dữ liệu tĩnh là 32 bit trong các tệp thực thi không phải PIE x86-64 trên Linux và luôn trong Linux x32 ABI, do đó, một phiên bản x86-64 sử dụng con trỏ 32 bit có thể sử dụng được trong một số trường hợp.) sự tương tác này làm cho nó thú vị để xem xét các kết hợp khác nhau của các yêu cầu.
- IA32 với DF = 0 trên quy ước gọi vào / ra: 17B (
nostring
) .
- IA32: 16B (với quy ước DF = 1:
stosb_edx_arg
hoặc skew
) ; hoặc với DF = dontcare, hãy đặt nó: 16 + 1Bstosb_decode_overlap
hoặc 17Bstosb_edx_arg
- x86-64 với các con trỏ 64 bit và DF = 0 trong quy ước gọi vào / ra: 17 + 1 byte (
stosb_decode_overlap
) , 18B ( stosb_edx_arg
hoặc skew
)
x86-64 với con trỏ 64 bit, xử lý DF khác: 16B (DF = 1 skew
) , 17B ( nostring
với DF = 1, sử dụng scasb
thay vì dec
). 18B ( stosb_edx_arg
bảo toàn DF = 1 với 3 byte inc rdi
).
Hoặc nếu chúng tôi cho phép trả về một con trỏ về 1 byte trước chuỗi, 15B ( stosb_edx_arg
không có inc
ở cuối). Tất cả được thiết lập để gọi lại và mở rộng một chuỗi khác vào bộ đệm với cơ sở / bảng khác nhau ... Nhưng điều đó sẽ có ý nghĩa hơn nếu chúng ta không lưu trữ một kết thúc 0
, và bạn có thể đặt thân hàm trong một vòng lặp để nó thực sự là một vấn đề riêng biệt.
x86-64 với con trỏ đầu ra 32 bit, quy ước gọi DF = 0: không cải thiện con trỏ đầu ra 64 bit, nhưng nostring
hiện tại có mối quan hệ 18B ( ).
- x86-64 với con trỏ đầu ra 32 bit: không cải thiện so với các phiên bản con trỏ 64 bit tốt nhất, vì vậy 16B (DF = 1
skew
). Hoặc để thiết lập DF = 1 và rời khỏi nó, 17B cho skew
với std
nhưng không phải cld
. Hoặc 17 + 1B cho stosb_decode_overlap
với inc edi
khi kết thúc thay vì cld
/ scasb
.
Với quy ước gọi DF = 1: 16 byte (IA32 hoặc x86-64)
Yêu cầu DF = 1 trên đầu vào, để nó được đặt. Hoàn toàn hợp lý , ít nhất là trên cơ sở từng chức năng. Thực hiện tương tự như phiên bản trên, nhưng với xchg để lấy phần còn lại vào / ra AL trước / sau XLATB (tra cứu bảng với R / EBX làm cơ sở) và STOSB ( *output-- = al
).
Với DF bình thường = 0 trên ước nhập / xuất cảnh, các std
/ cld
/ scasb
phiên bản là 18 byte cho mã 32 và 64-bit, và 64-bit sạch (làm việc với một con trỏ đầu ra 64-bit).
Lưu ý rằng các đối số đầu vào nằm trong các thanh ghi khác nhau, bao gồm RBX cho bảng (for xlatb
). Cũng lưu ý rằng vòng lặp này bắt đầu bằng cách lưu trữ AL và kết thúc với char cuối cùng chưa được lưu trữ (do đó mov
ở cuối). Vì vậy, vòng lặp là "sai lệch" so với những người khác, do đó tên.
;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
Một phiên bản không bị lệch tương tự làm lu mờ EDI / RDI và sau đó sửa nó.
; 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
Tôi đã thử một phiên bản thay thế của cái này với lea esi, [rbx+rdx]
/ movsb
như thân vòng lặp bên trong. (RSI được đặt lại mỗi lần lặp, nhưng RDI giảm). Nhưng nó không thể sử dụng xor-zero / stos cho bộ kết thúc, vì vậy nó lớn hơn 1 byte. (Và nó không sạch 64 bit cho bảng tra cứu mà không có tiền tố REX trên LEA.)
LUT với độ dài rõ ràng và dấu kết thúc 0: 16 + 1 byte (32 bit)
Phiên bản này đặt DF = 1 và để nguyên như vậy. Tôi đang đếm byte LUT bổ sung cần thiết như một phần của tổng số byte.
Thủ thuật hay ở đây là có cùng một byte giải mã hai cách khác nhau . Chúng tôi rơi vào giữa vòng lặp với số dư = cơ sở và thương số = số đầu vào và sao chép số 0 kết thúc vào vị trí.
Lần đầu tiên thông qua chức năng, 3 byte đầu tiên của vòng lặp được tiêu thụ dưới dạng các byte cao của một dist32 cho LEA. LEA đó sao chép cơ sở (mô-đun) sang EDX, idiv
tạo ra phần còn lại cho các lần lặp lại sau.
Byte thứ 2 idiv ebp
là FD
, là mã opcode cho std
lệnh mà hàm này cần để làm việc. (Đây là một khám phá may mắn. Tôi đã xem xét điều này div
trước đó, nó phân biệt với idiv
việc sử dụng các /r
bit trong ModRM. Byte thứ hai div epb
giải mã cmc
là vô hại nhưng không hữu ích. Nhưng idiv ebp
chúng ta thực sự có thể loại bỏ std
từ trên xuống của hàm.)
Lưu ý các thanh ghi đầu vào là một lần nữa: EBP cho cơ sở.
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
Thủ thuật giải mã chồng chéo này cũng có thể được sử dụng với cmp eax, imm32
: chỉ mất 1 byte để chuyển tiếp 4 byte một cách hiệu quả, chỉ có các cờ ghi đè. (Điều này thật tồi tệ đối với hiệu năng trên các CPU đánh dấu ranh giới chỉ dẫn trong bộ đệm L1i, BTW.)
Nhưng ở đây, chúng tôi đang sử dụng 3 byte để sao chép một thanh ghi và nhảy vào vòng lặp. Điều đó thường sẽ mất 2 + 2 (Mov + jmp) và sẽ cho phép chúng tôi nhảy vào vòng lặp ngay trước STOS thay vì trước XLATB. Nhưng sau đó chúng ta cần một STD riêng, và nó sẽ không thú vị lắm.
Hãy thử trực tuyến! (với một _start
người gọi sử dụng sys_write
kết quả)
Tốt nhất là gỡ lỗi để chạy nó bên dưới strace
, hoặc hexdump đầu ra, vì vậy bạn có thể thấy xác minh rằng có một bộ \0
kết thúc ở đúng nơi, v.v. Nhưng bạn có thể thấy điều này thực sự hoạt động và tạo ra AAAAAACHOO
đầu vào của
num equ 698911
table: db "CHAO"
%endif
tablen equ $ - table
db 0 ; "terminator" needed by ascii_compress_stosb_decode_overlap
(Trên thực tế xxAAAAAACHOO\0x\0\0...
, vì chúng ta đã chuyển từ 2 byte trước đó trong bộ đệm ra một độ dài cố định. Vì vậy, chúng ta có thể thấy rằng hàm đã ghi các byte mà nó được cho là và không bước vào bất kỳ byte nào mà nó không nên có. con trỏ bắt đầu được truyền cho hàm là x
ký tự cuối cùng thứ 2 , được theo sau bởi các số không.)