x86 รหัสเครื่อง 32 บิต (จำนวนเต็ม 32 บิต): 17 ไบต์
(ดูรุ่นอื่น ๆ ด้านล่างรวมถึง 16 ไบต์สำหรับ 32- บิตหรือ 64- บิตโดยใช้การประชุมที่เรียกว่า DF = 1)
ผู้เรียกผ่าน args ในการลงทะเบียนรวมถึงตัวชี้ไปยังจุดสิ้นสุดของบัฟเฟอร์ส่งออก (เช่นคำตอบ C ของฉัน ; ดูมันสำหรับเหตุผลและคำอธิบายของอัลกอริทึม.) ภายใน glibc ของ_itoa
ทำเช่นนี้จึงไม่เพียง รีจิสเตอร์ arg-pass ใกล้เคียงกับ x86-64 System V ยกเว้นว่าเรามี arg ใน EAX แทน EDX
เมื่อกลับมา EDI ชี้ไปที่ไบต์แรกของสตริง C ที่สิ้นสุด 0 ในบัฟเฟอร์บัฟเฟอร์ การลงทะเบียนค่าส่งคืนตามปกติคือ EAX / RAX แต่ในภาษาแอสเซมบลีคุณสามารถใช้สิ่งที่เรียกประชุมสิ่งที่สะดวกสำหรับฟังก์ชั่น ( xchg eax,edi
ในตอนท้ายจะเพิ่ม 1 ไบต์)
buffer_end - edi
โทรสามารถคำนวณความยาวอย่างชัดเจนถ้ามันอยากจาก แต่ฉันไม่คิดว่าเราสามารถปรับข้ามเทอร์มิเนเตอร์ได้เว้นแต่ฟังก์ชันจะส่งคืนพอยน์เตอร์ + พอยน์เตอร์หรือพอยน์เตอร์ + ความยาว นั่นจะประหยัดได้ 3 ไบต์ในรุ่นนี้ แต่ฉันไม่คิดว่ามันสมเหตุสมผล
- EAX = n = หมายเลขที่จะถอดรหัส (สำหรับ
idiv
. args อื่นไม่ใช่ตัวถูกดำเนินการโดยนัย)
- EDI = จุดสิ้นสุดของบัฟเฟอร์เอาต์พุต (ยังคงใช้รุ่น 64 บิต
dec edi
ดังนั้นจะต้องอยู่ในระดับต่ำ 4GiB)
- ESI / RSI = ตารางค้นหา, aka LUT ไม่อุดตัน
- ECX = ความยาวของตาราง = ฐาน ไม่อุดตัน
nasm -felf32 ascii-compress-base.asm -l /dev/stdout | cut -b -30,$((30+10))-
(มือแก้ไขเพื่อย่อความคิดเห็นการกำหนดหมายเลขบรรทัดนั้นแปลก)
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
เป็นเรื่องน่าแปลกใจที่รุ่นที่ง่ายที่สุดที่ไม่มีการแลกเปลี่ยนความเร็ว / ขนาดนั้นเล็กที่สุด แต่std
/ cld
ราคา 2 ไบต์เพื่อใช้stosb
ในการเรียงลำดับจากมากไปน้อยและยังคงเป็นไปตาม DF = 0 แบบแผนทั่วไป (และ STOS ลดลงหลังจากการจัดเก็บปล่อยให้ตัวชี้ชี้หนึ่งไบต์ต่ำเกินไปเมื่อออกจากลูปทำให้เราต้องเพิ่มไบต์พิเศษเพื่อหลีกเลี่ยง)
รุ่น:
ฉันมาพร้อมกับเทคนิคการใช้งานที่แตกต่างกัน 4 อย่าง (โดยใช้mov
load / store แบบง่าย ๆ(ด้านบน), การใช้lea
/ movsb
(เรียบร้อย แต่ไม่เหมาะสม), การใช้xchg
/ xlatb
/ stosb
/ xchg
และสิ่งหนึ่งที่เข้าสู่ลูปด้วยแฮ็คคำแนะนำที่ทับซ้อนกันดูรหัสด้านล่าง) . อันสุดท้ายจำเป็นต้องมีการติดตาม0
ในตารางการค้นหาเพื่อคัดลอกเป็นเทอร์มินัลสตริงผลลัพธ์ดังนั้นฉันจึงนับว่าเป็น 1 ไบต์ ขึ้นอยู่กับ 32 / 64- บิต (1 ไบต์inc
หรือไม่) และไม่ว่าเราจะสามารถสันนิษฐานได้ว่าผู้โทรตั้งค่า DF = 1 ( stosb
มากไปหาน้อย) หรืออะไรก็ตามรุ่นที่ต่างกันสั้นที่สุด
DF = 1 เพื่อจัดเก็บตามลำดับจากมากไปน้อยทำให้มันชนะสำหรับ xchg / stosb / xchg แต่ผู้โทรมักจะไม่ต้องการสิ่งนั้น รู้สึกเหมือนกำลังถ่ายงานกับผู้โทรด้วยวิธีที่ยากต่อการจัดชิดขอบ (ซึ่งแตกต่างจากการลงทะเบียน arg-through และ return-value แบบกำหนดเองซึ่งโดยทั่วไปแล้วจะไม่เสียค่าใช้จ่ายสำหรับ asm caller ใด ๆ เพิ่มเติม) แต่ในรหัส 64- บิตcld
/ scasb
ทำงานเป็นinc rdi
หลีกเลี่ยงการตัดทอนตัวชี้ออกเป็น 32 บิตดังนั้นบางครั้ง ไม่สะดวกในการเก็บรักษา DF = 1 ในฟังก์ชั่น 64 บิต . (ตัวชี้ไปยังรหัสคงที่ / ข้อมูลเป็นแบบ 32 บิตใน x86-64 executable-PIE ที่ไม่ใช่ PIE บน Linux และใน Linux x32 ABI เสมอดังนั้นรุ่น x86-64 ที่ใช้ตัวชี้แบบ 32 บิตนั้นสามารถใช้ได้ในบางกรณี) อย่างไรก็ตาม การโต้ตอบนี้ทำให้น่าสนใจในการดูชุดค่าผสมที่แตกต่างกัน
- IA32 กับ DF = 0 ในรายการ / ออกจากการเรียกประชุม: 17B (
nostring
)
- IA32: 16B (ด้วยการประชุม DF = 1:
stosb_edx_arg
หรือskew
) ; หรือ DF ที่เข้ามา = dontcare ปล่อยให้มันตั้ง: 16 + 1Bstosb_decode_overlap
หรือ 17Bstosb_edx_arg
- x86-64 พร้อมพอยน์เตอร์ 64- บิตและ DF = 0 สำหรับการโทรเข้า / ออก: 17 + 1 ไบต์ (
stosb_decode_overlap
) , 18B ( stosb_edx_arg
หรือskew
)
x86-64 พร้อมพอยน์เตอร์ 64- บิต, การจัดการ DF อื่น ๆ : 16B (DF = 1 skew
) , 17B ( nostring
ด้วย DF = 1, ใช้scasb
แทนdec
) 18B ( stosb_edx_arg
รักษา DF = 1 ด้วย 3 ไบต์inc rdi
)
หรือถ้าเราอนุญาตให้ส่งกลับตัวชี้ไปที่ 1 ไบต์ก่อนสตริง15B ( stosb_edx_arg
โดยไม่inc
สิ้นสุด) ทั้งหมดตั้งค่าให้เรียกอีกครั้งและขยายสายอักขระอื่นลงในบัฟเฟอร์ด้วยฐาน / ตารางที่แตกต่างกัน ... แต่นั่นจะสมเหตุสมผลมากกว่าถ้าเราไม่ได้หยุดการจัดเก็บ0
อย่างใดอย่างหนึ่งและคุณอาจวางฟังก์ชันของฟังก์ชันไว้ในลูป ปัญหาแยกต่างหาก
x86-64 พร้อมตัวชี้เอาต์พุต 32 บิต, DF = 0 แบบแผนการเรียก: ไม่มีการปรับปรุงใด ๆ กับตัวชี้เอาต์พุต 64- บิต แต่ 18B ( nostring
) เชื่อมโยงกันในขณะนี้
- x86-64 พร้อมตัวชี้เอาต์พุต 32 บิต: ไม่มีการปรับปรุงใด ๆ กับตัวชี้เวอร์ชัน 64 บิตที่ดีที่สุดดังนั้น 16B (DF = 1
skew
) หรือเพื่อตั้งค่า DF = 1 และปล่อยให้เป็น 17B skew
ด้วยหรือstd
ไม่cld
ก็ได้ หรือ 17 + 1B สำหรับstosb_decode_overlap
ที่มีinc edi
ในตอนท้ายแทน/cld
scasb
ด้วย DF = 1 แผนการโทร: 16 ไบต์ (IA32 หรือ x86-64)
ต้องการ DF = 1 ในอินพุตปล่อยให้ตั้งค่าไว้ อย่างน้อยน่าจะเป็นอย่างน้อยบนพื้นฐานต่อฟังก์ชั่น ทำสิ่งเดียวกับเวอร์ชั่นด้านบน แต่ใช้ xchg เพื่อรับส่วนที่เหลือเข้า / ออก AL ก่อน / หลัง XLATB (ค้นหาตารางด้วย R / EBX เป็นฐาน) และ STOSB ( *output-- = al
)
ด้วย DF ปกติ = 0 ในการประชุมเข้า / ออก/ / รุ่นคือ 18 ไบต์สำหรับรหัส 32 และ 64 บิตและ 64 บิตที่สะอาด (ทำงานร่วมกับตัวชี้การส่งออก 64 บิต)std
cld
scasb
โปรดทราบว่าอินพุต args อยู่ในการลงทะเบียนที่แตกต่างกันรวมถึง RBX สำหรับตาราง (สำหรับxlatb
) นอกจากนี้ทราบว่าวงนี้จะเริ่มต้นด้วยการจัดเก็บ AL, และจบลงด้วยถ่านที่ผ่านมาไม่ได้เก็บไว้เลย (เพราะฉะนั้นmov
ที่สิ้นสุด) ดังนั้นลูปคือ "เบ้" สัมพันธ์กับชื่ออื่นดังนั้นชื่อ
;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
รุ่นที่ไม่มีการเอียงคล้ายกันนี้จะใช้ EDI / RDI มากกว่าและแก้ไขได้
; 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
ฉันลองรุ่นอื่นของสิ่งนี้ด้วย lea esi, [rbx+rdx]
/ movsb
เป็นตัววนรอบด้านใน (RSI ถูกรีเซ็ตทุกการทำซ้ำ แต่ลดลง RDI) แต่มันไม่สามารถใช้ xor-zero / stos สำหรับเทอร์มิเนเตอร์ดังนั้นจึงมีขนาดใหญ่กว่า 1 ไบต์ (และไม่สะอาด 64 บิตสำหรับตารางการค้นหาโดยไม่มีคำนำหน้า REX ใน LEA)
LUT ที่มีความยาวอย่างชัดเจนและ 0 terminator: 16 + 1 bytes (32- บิต)
รุ่นนี้ตั้งค่า DF = 1 และปล่อยให้เป็นเช่นนั้น ฉันกำลังนับไบต์พิเศษ LUT ที่ต้องการเป็นส่วนหนึ่งของจำนวนไบต์ทั้งหมด
เคล็ดลับเย็นที่นี่จะมีไบต์เดียวกันถอดรหัสสองวิธีที่แตกต่างกัน เราตกลงไปในช่วงกลางของลูปที่มีเศษเหลือ = ฐานและ quotient = จำนวนอินพุตและคัดลอก 0 เทอร์มิเนเตอร์เข้าที่
ในครั้งแรกที่ผ่านฟังก์ชั่น 3 ไบต์แรกของลูปจะถูกใช้เป็นไบต์สูงของ disp32 สำหรับ LEA หน่วยงาน LEA นั้นทำการคัดลอกฐาน (โมดูลัส) ไปยัง EDX idiv
สร้างส่วนที่เหลือสำหรับการทำซ้ำในภายหลัง
ไบต์ที่สองของidiv ebp
คือFD
ซึ่งเป็น opcode สำหรับstd
คำสั่งที่ฟังก์ชันนี้จำเป็นต้องใช้ (นี่คือการค้นพบที่โชคดีฉันได้ดูสิ่งนี้div
ก่อนหน้านี้ซึ่งแยกตัวเองจากการidiv
ใช้/r
บิตใน ModRM ไบต์ที่ 2 ของการdiv epb
ถอดรหัสเป็นcmc
ซึ่งไม่เป็นอันตราย แต่ไม่เป็นประโยชน์ แต่idiv ebp
จริง ๆ แล้วเราสามารถลบสิ่งที่std
อยู่ด้านบนออก ของฟังก์ชัน)
หมายเหตุการลงทะเบียนอินพุตนั้นต่างกันอีกครั้ง: EBP สำหรับฐาน
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
เคล็ดลับการถอดรหัสที่ทับซ้อนกันนี้ยังสามารถใช้กับcmp eax, imm32
: ใช้เวลาเพียง 1 ไบต์เพื่อข้ามไปข้างหน้าอย่างมีประสิทธิภาพ 4 ไบต์เพียงธงอุดตัน (นี่เป็นเรื่องที่แย่มากสำหรับประสิทธิภาพของ CPU ที่ทำเครื่องหมายขอบเขตการเรียนการสอนในแคช L1i, BTW)
แต่ที่นี่เราใช้ 3 ไบต์เพื่อคัดลอกรีจิสเตอร์และกระโดดเข้าไปในลูป ปกติแล้วจะใช้เวลา 2 + 2 (mov + jmp) และให้เรากระโดดเข้าไปในลูปก่อนหน้า STOS แทนก่อน XLATB แต่เราต้องการ STD แยกต่างหากและมันก็ไม่น่าสนใจมาก
ลองออนไลน์! (กับ_start
ผู้โทรที่ใช้sys_write
กับผลลัพธ์)
เป็นการดีที่สุดสำหรับการดีบั๊กเพื่อให้ทำงานภายใต้strace
หรือลบ hexdump เอาท์พุทดังนั้นคุณสามารถดูการตรวจสอบว่ามีเทอร์\0
มินัลในตำแหน่งที่ถูกต้องและอื่น ๆ แต่คุณสามารถดูว่ามันใช้งานได้จริงและผลิตAAAAAACHOO
เพื่อป้อนเข้า
num equ 698911
table: db "CHAO"
%endif
tablen equ $ - table
db 0 ; "terminator" needed by ascii_compress_stosb_decode_overlap
(อันที่จริงแล้วxxAAAAAACHOO\0x\0\0...
เพราะเราทิ้งจาก 2 ไบต์ก่อนหน้านี้ในบัฟเฟอร์ออกไปจนถึงความยาวคงที่ดังนั้นเราจะเห็นได้ว่าฟังก์ชั่นเขียนไบต์ที่มันควรจะเป็นและไม่ได้เหยียบไบต์ใด ๆ ที่มันไม่ควร start-pointer ที่ส่งผ่านไปยังฟังก์ชันคือx
อักขระตัวที่ 2 ซึ่งตามด้วยศูนย์)