x86 32 비트 머신 코드 (32 비트 정수) : 17 바이트
(또한 DF = 1 호출 규칙이있는 32 비트 또는 64 비트 용 16 바이트를 포함하여 아래의 다른 버전도 참조하십시오.)
발신자는 포인터를 포함한 레지스터 인수, 통과 최종 출력 버퍼의 (같은 내 C의 대답을 , 칭의와 알고리즘의 설명을 참조하십시오.) 의 glibc의 내부 _itoa
이 작업을 수행 그것은 단지 코드 골프를 위해 고안하지 그래서. arg-passing 레지스터는 EDX 대신 EAX에 arg가 있다는 점을 제외하고 x86-64 System V에 가깝습니다.
리턴시 EDI는 출력 버퍼에서 0으로 끝나는 C 문자열의 첫 번째 바이트를 가리 킵니다. 일반적인 반환 값 레지스터는 EAX / RAX이지만 어셈블리 언어에서는 함수에 편리한 호출 규칙을 사용할 수 있습니다. ( xchg eax,edi
끝에 1 바이트가 추가됩니다).
호출자는 원하는 경우에서 명시 적 길이를 계산할 수 있습니다 buffer_end - edi
. 그러나 함수가 실제로 시작 + 끝 포인터 또는 포인터 + 길이를 모두 반환하지 않는 한 종료자를 생략하는 것을 정당화 할 수 있다고 생각하지 않습니다. 그것은이 버전에서 3 바이트를 절약 할 것이지만 그것이 정당하다고 생각하지 않습니다.
- EAX = n = 디코딩 할 숫자입니다. (의 경우
idiv
. 다른 인수는 암시 적 피연산자가 아닙니다.)
- EDI = 출력 버퍼 끝 (64 비트 버전은 여전히을 사용
dec edi
하므로 낮은 4GiB에 있어야 함)
- ESI / RSI = 룩업 테이블, 일명 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는 저장 후 감소 하여 루프 종료시 포인터가 1 바이트를 너무 낮게 유지하면 추가 바이트가 필요합니다.)
버전 :
나는 4 가지의 상당히 다른 구현 트릭 ( mov
위의 간단한 로드 / 저장 (위), lea
/ movsb
(최초이지만 최적은 아님) 사용, xchg
/ xlatb
/ stosb
/ 사용) xchg
및 겹치는 명령 해킹으로 루프에 들어가는 트릭을 생각해 냈습니다 . 아래 코드 참조) . 마지막 0
은 출력 문자열 종결 자로 복사하기 위해 조회 테이블에서 후행 이 필요 하므로 +1 바이트로 계산합니다. 32/64 비트 (1 바이트 inc
)에 따라 호출자가 DF = 1 ( stosb
내림차순) 또는 다른 버전을 가장 짧게 설정한다고 가정 할 수 있는지 여부 에 따라 다릅니다.
DF = 1을 내림차순으로 저장하면 xchg / stosb / xchg에 승리하지만 호출자는 종종 원하지 않습니다. 작업을 정당화하기 어려운 방식으로 호출자에게 오프로드하는 것처럼 느껴집니다. (일반적으로 asm 호출자는 추가 작업 비용이 들지 않는 사용자 정의 arg-passing 및 return-value 레지스터와 달리) 64 비트 코드에서 cld
/ 는 출력 포인터를 32 비트로 자르지 않도록 scasb
로 작동합니다. inc rdi
64 비트 클린 기능에서 DF = 1을 유지하는 것은 불편합니다. . 정적 코드 / 데이터에 대한 포인터는 Linux의 x86-64 비 PIE 실행 파일에서 32 비트이며 항상 Linux x32 ABI에서 32 비트 포인터를 사용하는 x86-64 버전을 사용할 수 있습니다. 이러한 상호 작용을 통해 서로 다른 요구 사항 조합을 살펴볼 수 있습니다.
- 시작 / 종료 호출 규칙에서 DF = 0 인 IA32 : 17B (
nostring
) .
- IA32 : 16B (DF = 1 규칙 :
stosb_edx_arg
또는 skew
) ; 또는 들어오는 DF = dontcare로 설정 한 상태로 유지 : 16 + 1Bstosb_decode_overlap
또는 17Bstosb_edx_arg
- 64 비트 포인터가있는 x86-64 및 시작 / 종료 호출 규칙에서 DF = 0 : 17 + 1 바이트 (
stosb_decode_overlap
) , 18B ( stosb_edx_arg
또는 skew
)
64 비트 포인터가있는 x86-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
중 하나, 그리고 정말 그래서 당신은 루프 내부의 함수 본문을 넣을 수 별도의 문제.
32 비트 출력 포인터가있는 x86-64, DF = 0 호출 규칙 : 64 비트 출력 포인터보다 개선되지 않았지만 18B ( nostring
)는 현재 연결되어 있습니다.
- 32 비트 출력 포인터가있는 x86-64 : 최상의 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를 사용하여 XLATB (기본으로 R / EBX를 사용하여 테이블 조회) 및 STOSB ( *output-- = al
) 이전 / 이후에 AL의 나머지를 가져오고 나가는 xchg를 사용합니다 .
시작 / 종료 규칙에 일반적인 DF = 0 인 경우 std
, cld
// scasb
버전은 32 비트 및 64 비트 코드의 경우 18 바이트이고 64 비트는 깨끗합니다 (64 비트 출력 포인터와 함께 작동).
입력 인수는 테이블에 대한 RBX (for 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 바이트 더 큽니다. (그리고 LEA에 REX 접두사가없는 조회 테이블의 경우 64 비트가 아닙니다.)
명시 적 길이 와 0 종결 자를 갖는 LUT : 16 + 1 바이트 (32 비트)
이 버전은 DF = 1을 설정하고 그대로 둡니다. 총 바이트 수의 일부로 필요한 여분의 LUT 바이트를 계산합니다.
여기서 멋진 트릭 은 동일한 바이트가 두 가지 다른 방식으로 디코딩되는 것 입니다. 우리는 나머지 = base 및 quotient = 입력 번호로 루프 중간에 들어가고 0 종결자를 제자리에 복사합니다.
함수를 통해 처음으로 루프의 처음 3 바이트는 LEA에 대한 disp32의 상위 바이트로 사용됩니다. LEA는베이스 (모듈러스)를 EDX에 복사하고 idiv
나중의 반복을 위해 나머지를 생성합니다.
의 2 바이트 idiv ebp
IS FD
대한 오피 코드이며, std
일 기능이 요구하는 명령. (이 행운의 발견은 내가 함께이보고되었다.이었다 div
에서 자신을 구별하는, 이전 idiv
은 Using /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
. 4 바이트를 효과적으로 앞당기려면 1 바이트 만 소요되며 클로버 링 플래그 만 있습니다. (BTW L1i 캐시에서 명령 경계를 표시하는 CPU의 성능에는 끔찍합니다.)
그러나 여기서는 3 바이트를 사용하여 레지스터를 복사하고 루프로 점프합니다. 보통 2 + 2 (mov + jmp)가 걸리며 XLATB가 아닌 STOS 바로 앞의 루프로 넘어갑니다. 그러나 우리는 별도의 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 바이트에서 덤핑하고 때문에 고정 길이 출력 버퍼. 우리는 함수가 그것으로 생각하고 있었다 바이트를 쓴 것을 볼 수 있도록 하지 않았다 어떤이가하지 말았어야 바이트에 단계.를 함수에 전달 된 시작 포인터는 마지막 두 번째 x
문자이며 그 뒤에 0이옵니다.)