Mẹo chơi gôn trong mã máy x86 / x64


27

Tôi nhận thấy rằng không có câu hỏi nào như vậy, vì vậy đây là:

Bạn có lời khuyên chung cho việc chơi golf trong mã máy? Nếu mẹo chỉ áp dụng cho một môi trường nhất định hoặc quy ước gọi điện, vui lòng chỉ định điều đó trong câu trả lời của bạn.

Xin vui lòng chỉ một mẹo cho mỗi câu trả lời (xem tại đây ).

Câu trả lời:


11

mov- ngay lập tức là đắt tiền cho hằng số

Điều này có thể rõ ràng, nhưng tôi vẫn sẽ đặt nó ở đây. Nói chung, bạn nên suy nghĩ về biểu diễn mức bit của một số khi bạn cần khởi tạo một giá trị.

Đang khởi tạo eaxvới 0:

b8 00 00 00 00          mov    $0x0,%eax

nên được rút ngắn ( đối với hiệu suất cũng như kích thước mã ) thành

31 c0                   xor    %eax,%eax

Đang khởi tạo eaxvới -1:

b8 ff ff ff ff          mov    $-1,%eax

có thể rút ngắn thành

31 c0                   xor    %eax,%eax
48                      dec    %eax

hoặc là

83 c8 ff                or     $-1,%eax

Hay nói chung hơn, bất kỳ giá trị mở rộng đăng nhập 8 bit nào cũng có thể được tạo thành 3 byte với push -12(2 byte) / pop %eax(1 byte). Điều này thậm chí hoạt động cho các thanh ghi 64 bit không có tiền tố REX bổ sung; push/ poptoán hạng mặc định-size = 64.

6a f3                   pushq  $0xfffffffffffffff3
5d                      pop    %rbp

Hoặc được cung cấp một hằng số đã biết trong một thanh ghi, bạn có thể tạo một hằng số gần đó bằng cách sử dụng lea 123(%eax), %ecx(3 byte). Điều này rất hữu ích nếu bạn cần một thanh ghi zero hằng số; xor-zero (2 byte) + lea-disp8(3 byte).

31 c0                   xor    %eax,%eax
8d 48 0c                lea    0xc(%eax),%ecx

Xem thêm Đặt tất cả các bit trong thanh ghi CPU thành 1 hiệu quả


Ngoài ra, để khởi tạo một thanh ghi có giá trị nhỏ (8 bit) khác 0: use eg push 200; pop edx- 3 byte để khởi tạo.
anatolyg

2
BTW để khởi tạo một thanh ghi để -1, sử dụng dec, ví dụ nhưxor eax, eax; dec eax
anatolyg

@anatolyg: 200 là một ví dụ tồi, nó không phù hợp với dấu hiệu mở rộng8. Nhưng có, push imm8/ pop reglà 3 byte và thật tuyệt vời đối với các hằng 64 bit trên x86-64, trong đó dec/ inclà 2 byte. Và push r64/ pop 64(2 byte) thậm chí có thể thay thế 3 byte mov r64, r64(3 byte bằng REX). Xem thêm Đặt tất cả các bit trong thanh ghi CPU thành 1 một cách hiệu quả cho những thứ như đã lea eax, [rcx-1]cho một giá trị đã biết trong eax(ví dụ: nếu cần một thanh ghi 0 một hằng số khác, chỉ cần sử dụng LEA thay vì đẩy / bật
Peter Cordes

10

Trong rất nhiều trường hợp, các lệnh dựa trên bộ tích lũy (nghĩa là các lệnh lấy (R|E)AXtoán hạng đích) ngắn hơn 1 byte so với các lệnh trong trường hợp chung; xem câu hỏi này trên StackOverflow.


Thông thường những al, imm8trường hợp hữu ích nhất là những trường hợp đặc biệt, như or al, 0x20/ sub al, 'a'/cmp al, 'z'-'a' / ja .non_alphabeticlà 2 byte mỗi, thay vì 3. Sử dụng alcho dữ liệu ký tự cũng cho phép lodsbvà / hoặc stosb. Hoặc sử dụng alđể kiểm tra một cái gì đó về byte thấp của EAX, như lodsd/ test al, 1/ setnz clmake cl = 1 hoặc 0 cho số lẻ / chẵn. Nhưng trong trường hợp hiếm hoi mà bạn cần 32-bit ngay lập tức, thì chắc chắn op eax, imm32, như trong câu trả lời chính của tôi
Peter Cordes

8

Chọn quy ước gọi của bạn để đặt args nơi bạn muốn họ.

Ngôn ngữ của câu trả lời của bạn là asm (thực tế là mã máy), vì vậy hãy coi nó như một phần của chương trình được viết bằng asm, không phải C-comp-for-x86. Chức năng của bạn không phải dễ dàng gọi từ C với bất kỳ quy ước gọi tiêu chuẩn nào. Tuy nhiên, đó là một phần thưởng tuyệt vời nếu nó không làm bạn tốn thêm byte nào.

Trong một chương trình asm thuần túy, việc một số chức năng của người trợ giúp sử dụng quy ước gọi điện là thuận tiện cho họ và cho người gọi của họ. Các chức năng như vậy ghi lại quy ước gọi của họ (đầu vào / đầu ra / clobbers) với các bình luận.

Trong cuộc sống thực, ngay cả các chương trình asm cũng (tôi nghĩ) có xu hướng sử dụng các quy ước gọi phù hợp cho hầu hết các chức năng (đặc biệt là trên các tệp nguồn khác nhau), nhưng bất kỳ chức năng quan trọng nào cũng có thể làm điều gì đó đặc biệt. Trong môn đánh gôn, bạn đang tối ưu hóa crap từ một chức năng duy nhất, vì vậy rõ ràng nó rất quan trọng / đặc biệt.


Để kiểm tra chức năng của bạn từ một chương trình C, có thể viết một trình bao bọc đặt args vào đúng vị trí, lưu / khôi phục bất kỳ thanh ghi bổ sung nào bạn ghi lại và đặt giá trị trả về e/raxnếu nó chưa có.


Giới hạn của những gì hợp lý: bất cứ điều gì không đặt ra gánh nặng vô lý cho người gọi:

  • ESP / RSP phải được bảo toàn cuộc gọi; regs số nguyên khác là trò chơi công bằng. (RBP và RBX thường được bảo toàn cuộc gọi trong các quy ước thông thường, nhưng bạn có thể ghi đè cả hai.)
  • Bất kỳ arg trong bất kỳ thanh ghi nào (ngoại trừ RSP) đều hợp lý, nhưng yêu cầu người gọi sao chép cùng một arg vào nhiều thanh ghi thì không.
  • Yêu cầu DF (cờ hướng chuỗi cho lods/ stos/ v.v.) phải rõ ràng (hướng lên) trên cuộc gọi / ret là bình thường. Để nó không được xác định trong cuộc gọi / ret sẽ ổn. Yêu cầu xóa nó hoặc đặt vào mục nhập nhưng sau đó để nó được sửa đổi khi bạn quay lại sẽ là lạ.

  • Trả về các giá trị FP trong x87 st0là hợp lý, nhưng trả lại st3bằng rác trong thanh ghi x87 khác thì không. Người gọi sẽ phải dọn sạch ngăn xếp x87. Ngay cả việc quay lại st0với các thanh ghi ngăn xếp cao hơn không trống cũng sẽ bị nghi ngờ (trừ khi bạn trả về nhiều giá trị).

  • Chức năng của bạn sẽ được gọi với call, [rsp]địa chỉ trả lại của bạn cũng vậy . Bạn có thể tránh call/ retbật x86 bằng cách sử dụng đăng ký liên kết như lea rbx, [ret_addr]/ jmp functionvà quay lại jmp rbx, nhưng điều đó không "hợp lý". Điều đó không hiệu quả như cuộc gọi / ret, vì vậy đó không phải là thứ bạn có thể tìm thấy trong mã thực.
  • Ghi đè bộ nhớ không giới hạn trên RSP là không hợp lý, nhưng việc ghi đè chức năng của bạn lập luận trên ngăn xếp được cho phép trong các quy ước gọi thông thường. x64 Windows yêu cầu 32 byte không gian bóng phía trên địa chỉ trả về, trong khi x86-64 System V cung cấp cho bạn vùng đỏ 128 byte bên dưới RSP, vì vậy một trong hai điều đó là hợp lý. (Hoặc thậm chí là một vùng đỏ lớn hơn nhiều, đặc biệt là trong một chương trình độc lập hơn là chức năng.)

Các trường hợp đường biên: viết một hàm tạo ra một chuỗi trong một mảng, với 2 phần tử đầu tiên là hàm args . Tôi đã chọn để người gọi lưu trữ bắt đầu chuỗi vào mảng và chỉ cần truyền một con trỏ đến mảng. Điều này chắc chắn uốn cong các yêu cầu của câu hỏi. Tôi coi lấy args đóng gói vào xmm0cho movlps [rdi], xmm0, mà cũng sẽ là một quy ước gọi lạ.


Trả về một boolean trong FLAGS (mã điều kiện)

Các cuộc gọi hệ thống OS X thực hiện điều này ( CF=0có nghĩa là không có lỗi): Được coi là thực hành xấu khi sử dụng các thanh ghi cờ làm giá trị trả về boolean? .

Bất kỳ điều kiện nào có thể được kiểm tra với một JCC là hoàn toàn hợp lý, đặc biệt nếu bạn có thể chọn một điều kiện có liên quan đến ngữ nghĩa đối với vấn đề. (ví dụ: hàm so sánh có thể đặt cờ vì vậy jnesẽ được thực hiện nếu chúng không bằng nhau).


Yêu cầu các đối số hẹp (như a char) là dấu hoặc không được mở rộng thành 32 hoặc 64 bit.

Điều này không phải là không hợp lý; sử dụng movzxhoặc movsx để tránh làm chậm đăng ký một phần là bình thường trong asm x86 hiện đại. Trong thực tế, clang / LLVM đã tạo mã phụ thuộc vào phần mở rộng không có giấy tờ đối với quy ước gọi của Hệ thống V x86-64: hẹp hơn 32 bit là ký hiệu hoặc 0 được mở rộng đến 32 bit bởi người gọi .

Bạn có thể ghi lại / mô tả phần mở rộng thành 64 bit bằng cách viết uint64_thoặcint64_t trong nguyên mẫu của bạn nếu bạn muốn. ví dụ: vì vậy bạn có thể sử dụng một looplệnh, sử dụng toàn bộ 64 bit RCX trừ khi bạn sử dụng tiền tố kích thước địa chỉ để ghi đè kích thước xuống 32 bit ECX (thực sự, kích thước địa chỉ không phải kích thước toán hạng).

Lưu ý rằng đó longchỉ là loại 32 bit trong Windows 64 bit ABI và Linux x32 ABI ; uint64_tkhông rõ ràng và ngắn hơn để gõ hơn unsigned long long.


Các quy ước gọi điện hiện có:

  • Windows 32-bit __fastcall, đã được đề xuất bởi một câu trả lời khác : số nguyên args trong ecxedx.

  • x86-64 Hệ thống V : vượt qua rất nhiều đối số trong các thanh ghi và có rất nhiều thanh ghi bị chặn mà bạn có thể sử dụng mà không cần tiền tố REX. Quan trọng hơn, nó thực sự được chọn để cho phép trình biên dịch nội tuyếnmemcpy hoặc bộ nhớ rep movsbmột cách dễ dàng: 6 đối số / con trỏ đầu tiên được truyền trong RDI, RSI, RDX, RCX, R8, R9.

    Nếu chức năng của bạn sử dụng lodsd/ stosdbên trong một vòng lặp chạy rcxthời gian (với loophướng dẫn), bạn có thể nói "có thể gọi được từ C như int foo(int *rdi, const int *rsi, int dummy, uint64_t len)với quy ước gọi Hệ thống V x86-64". ví dụ: nhiễm sắc thể .

  • GCC 32 bit regparm: Số nguyên lập luận trong EAX , ECX, EDX, trả về EAX (hoặc EDX: EAX). Có đối số đầu tiên trong cùng một thanh ghi với giá trị trả về cho phép một số tối ưu hóa, như trường hợp này với một người gọi ví dụ và một nguyên mẫu với thuộc tính hàm . Và tất nhiên AL / EAX là đặc biệt cho một số hướng dẫn.

  • Linux x32 ABI sử dụng các con trỏ 32 bit ở chế độ dài, do đó bạn có thể lưu tiền tố REX khi sửa đổi một con trỏ ( ví dụ trường hợp sử dụng ). Bạn vẫn có thể sử dụng kích thước địa chỉ 64 bit, trừ khi bạn có số nguyên âm 32 bit được mở rộng bằng 0 trong một thanh ghi (vì vậy nó sẽ là một giá trị không dấu lớn nếu bạn đã làm[rdi + rdx] ).

    Lưu ý rằng push rsp/ pop raxlà 2 byte và tương đương với mov rax,rsp, vì vậy bạn vẫn có thể sao chép các thanh ghi 64 bit đầy đủ trong 2 byte.


Khi các thách thức yêu cầu trả về một mảng, bạn có nghĩ quay trở lại stack là hợp lý không? Tôi nghĩ đó là những gì trình biên dịch sẽ làm khi trả về một cấu trúc theo giá trị.
qwr

@qwr: không, các quy ước gọi chính thống chuyển một con trỏ ẩn đến giá trị trả về. (Một số quy ước thông qua / trả lại các cấu trúc nhỏ trong sổ đăng ký). C / C ++ trả về cấu trúc theo giá trị dưới mui xe và xem phần cuối của Làm thế nào để các đối tượng hoạt động trong x86 ở cấp độ lắp ráp? . Lưu ý rằng việc truyền mảng (cấu trúc bên trong) sẽ sao chép chúng vào ngăn xếp cho x86-64 SysV: Kiểu dữ liệu C11 nào là một mảng theo AMD64 ABI , nhưng Windows x64 vượt qua con trỏ không phải là const.
Peter Cordes

Vậy bạn nghĩ sao về hợp lý hay không? Bạn có đếm x86 theo quy tắc này codegolf.meta.stackexchange.com/a/8507/17360
qwr

1
@qwr: x86 không phải là "ngôn ngữ dựa trên ngăn xếp". x86 là máy đăng ký có RAM , không phải máy xếp . Một máy stack giống như ký hiệu đánh bóng ngược, giống như các thanh ghi x87. fld / fld / faddp. Ngăn xếp cuộc gọi của x86 không phù hợp với mô hình đó: tất cả các quy ước gọi thông thường đều khiến RSP không được sửa đổi hoặc bật các đối số ret 16; họ không bật địa chỉ trả về, đẩy một mảng, sau đó push rcx/ ret. Người gọi sẽ phải biết kích thước mảng hoặc đã lưu RSP ở đâu đó bên ngoài ngăn xếp để tìm chính nó.
Peter Cordes

Gọi đẩy địa chỉ của lệnh sau khi cuộc gọi trong ngăn xếp jmp đến chức năng được gọi; ret bật địa chỉ từ ngăn xếp và jmp đến địa chỉ đó
RosLuP

7

Sử dụng mã hóa dạng ngắn trong trường hợp đặc biệt cho AL / AX / EAX và các dạng ngắn khác và các lệnh đơn byte

Các ví dụ giả định chế độ 32/64 bit, trong đó kích thước toán hạng mặc định là 32 bit. Một tiền tố kích thước toán hạng thay đổi hướng dẫn thành AX thay vì EAX (hoặc đảo ngược ở chế độ 16 bit).

  • inc/decmột thanh ghi (khác 8 bit): inc eax/ dec ebp. (Không phải x86-64:0x4x byte opcode được tái sử dụng làm tiền tố REX, vì vậyinc r/m32 là mã hóa duy nhất.)

    8-bit inc bllà 2 byte, bằng cách sử dụng inc r/m8opcode + ModR / M operand mã hóa . Vì vậy, sử dụng inc ebxđể tăng bl, nếu nó an toàn. (ví dụ: nếu bạn không cần kết quả ZF trong trường hợp các byte trên có thể khác không).

  • scasd: e/rdi+=4, yêu cầu các thanh ghi trỏ vào bộ nhớ có thể đọc được. Đôi khi hữu ích ngay cả khi bạn không quan tâm đến kết quả FLAGS (như cmp eax,[rdi]/ rdi+=4). Và ở chế độ 64 bit, scasbcó thể hoạt động dưới dạng 1 byteinc rdi , nếu lodsb hoặc stosb không hữu ích.

  • xchg eax, r32: đây là nơi 0x90 NOP đến từ : xchg eax,eax. Ví dụ: sắp xếp lại 3 thanh ghi với hai xchghướng dẫn trong một vòng lặp cdq/ cho GCD theo 8 byte trong đó hầu hết các hướng dẫn là byte đơn, bao gồm cả lạm dụng / thay vì /idivinc ecxlooptest ecx,ecxjnz

  • cdq: đăng nhập mở rộng EAX vào EDX: EAX, tức là sao chép bit cao của EAX sang tất cả các bit của EDX. Để tạo số 0 với số không âm, hoặc lấy 0 / -1 để thêm / phụ hoặc mặt nạ với. Bài học lịch sử x86: cltqso vớimovslq , và cả AT & T so với Intel ghi nhớ cho điều này và các vấn đề liên quan cdqe.

  • lodsb / d : thích mov eax, [rsi]/ rsi += 4không có cờ ghi chú. (Giả sử DF là rõ ràng, mà các quy ước gọi tiêu chuẩn yêu cầu khi nhập chức năng.) Ngoài ra stosb / d, đôi khi là scas, và hiếm khi hơn Mov / cmps.

  • push/ pop reg. ví dụ: ở chế độ 64 bit, push rsp/ pop rdilà 2 byte, nhưng mov rdi, rspcần tiền tố REX và là 3 byte.

xlatbtồn tại, nhưng hiếm khi hữu ích. Một bảng tra cứu lớn là điều cần tránh. Tôi cũng chưa bao giờ tìm thấy việc sử dụng cho AAA / DAA hoặc các hướng dẫn đóng gói BCD hoặc 2-ASCII khác.

1 byte lahf/ sahfhiếm khi hữu ích. Bạn có thể lahf / and ah, 1thay thế setc ah, nhưng nó thường không hữu ích.

Và đối với CF cụ thể, sẽ có sbb eax,eax0 / -1, hoặc thậm chí không có tài liệu nhưng được hỗ trợ phổ biến 1 byte salc(đặt AL từ Carry)sbb al,alkhông ảnh hưởng đến cờ. (Đã xóa trong x86-64). Tôi đã sử dụng SALC trong Thử thách đánh giá cao người dùng # 1: Dennis ♦ .

1 byte cmc/ clc/ stc(flip ("bổ sung"), xóa hoặc đặt CF) hiếm khi hữu ích, mặc dù tôi đã tìm thấy cách sử dụng đểcmc bổ sung độ chính xác mở rộng với các khối cơ sở 10 ^ 9. Để thiết lập / xóa CF vô điều kiện, thường sắp xếp để điều đó xảy ra như một phần của hướng dẫn khác, ví dụ: xor eax,eaxxóa CF cũng như EAX. Không có hướng dẫn tương đương cho các cờ điều kiện khác, chỉ DF (hướng chuỗi) và IF (ngắt). Cờ mang là đặc biệt cho rất nhiều hướng dẫn; ca làm việc đặt nó, adc al, 0có thể thêm nó vào AL trong 2 byte và tôi đã đề cập trước đó là SALC không có giấy tờ.

std/ cldhiếm khi có vẻ đáng giá . Đặc biệt là trong mã 32 bit, tốt hơn là chỉ sử dụng dectrên một con trỏ và movtoán hạng nguồn hoặc bộ nhớ cho một lệnh ALU thay vì đặt DF so lodsb/ stosbđi xuống thay vì lên trên. Thông thường nếu bạn cần hướng xuống, bạn vẫn có một con trỏ khác đi lên, vì vậy bạn cần nhiều hơn một stdcldtrong toàn bộ chức năng để sử dụng lods/ stoscho cả hai. Thay vào đó, chỉ cần sử dụng các hướng dẫn chuỗi cho hướng lên. (Các quy ước gọi tiêu chuẩn đảm bảo DF = 0 khi nhập chức năng, do đó bạn có thể cho rằng miễn phí mà không cần sử dụng cld.)


Lịch sử 8086: tại sao các bảng mã này tồn tại

Trong nguyên bản 8086, AX là rất đặc biệt: hướng dẫn thích lodsb/ stosb, cbw, mul/div và những người khác sử dụng nó ngầm. Tất nhiên đó vẫn là trường hợp; x86 hiện tại đã không giảm bất kỳ opcodes nào của 8086 (ít nhất là không phải bất kỳ trong số các tài liệu chính thức). Nhưng các CPU sau này đã thêm các hướng dẫn mới đưa ra các cách tốt hơn / hiệu quả hơn để thực hiện mọi việc mà không cần sao chép hoặc hoán đổi chúng sang AX trước. (Hoặc đến EAX ở chế độ 32 bit.)

ví dụ 8086 thiếu các bổ sung sau này như movsx/movzx để tải hoặc di chuyển + gia hạn đăng nhập hoặc 2 và 3 toán hạng imul cx, bx, 1234không tạo ra kết quả nửa cao và không có bất kỳ toán hạng ngầm nào.

Ngoài ra, nút cổ chai chính của 8086 là tìm nạp lệnh, vì vậy tối ưu hóa kích thước mã rất quan trọng đối với hiệu suất trước đó . Nhà thiết kế ISA của 8086 (Stephen Morse) đã dành rất nhiều không gian mã hóa opcode cho các trường hợp đặc biệt cho AX / AL, bao gồm các opcode đích (E) AX / AL đặc biệt cho tất cả các hướng dẫn ALU src tức thời cơ bản , chỉ cần opcode + ngay lập tức không có byte ModR / M. 2 byte add/sub/and/or/xor/cmp/test/... AL,imm8hoặc AX,imm16hoặc (ở chế độ 32 bit) EAX,imm32.

Nhưng không có trường hợp đặc biệt nào EAX,imm8, vì vậy mã hóa ModR / M thông thường add eax,4ngắn hơn.

Giả định là nếu bạn sẽ làm việc trên một số dữ liệu, bạn sẽ muốn nó trong AX / AL, vì vậy việc hoán đổi một thanh ghi với AX là điều bạn có thể muốn làm, thậm chí có thể thường xuyên hơn sao chép một đăng ký vào AX với mov.

Mọi thứ về mã hóa lệnh 8086 đều hỗ trợ mô hình này, từ các hướng dẫn như lodsb/w cho đến tất cả các mã hóa trường hợp đặc biệt để thực hiện với EAX cho đến việc sử dụng ngầm của nó ngay cả để nhân / chia.


Đừng mang đi; nó không tự động là một chiến thắng để hoán đổi mọi thứ thành EAX, đặc biệt nếu bạn cần sử dụng trực tiếp với các thanh ghi 32 bit thay vì 8 bit. Hoặc nếu bạn cần xen kẽ các hoạt động trên nhiều biến trong các thanh ghi cùng một lúc. Hoặc nếu bạn đang sử dụng hướng dẫn với 2 thanh ghi, thì không thể thực hiện được.

Nhưng hãy luôn ghi nhớ: tôi có đang làm bất cứ điều gì ngắn hơn trong EAX / AL không? Tôi có thể sắp xếp lại để tôi có cái này trong AL không, hoặc tôi hiện đang tận dụng lợi thế của AL tốt hơn với những gì tôi đã sử dụng nó cho.

Kết hợp các hoạt động 8 bit và 32 bit một cách tự do để tận dụng bất cứ khi nào an toàn để làm như vậy (bạn không cần thực hiện đăng ký đầy đủ hoặc bất cứ điều gì).


cdqlà hữu ích cho divnhu cầu zeroed edxtrong nhiều trường hợp.
qwr

1
@qwr: đúng, bạn có thể lạm dụng cdqtrước khi chưa ký divnếu bạn biết cổ tức của mình dưới 2 ^ 31 (tức là không âm khi được coi là đã ký) hoặc nếu bạn sử dụng nó trước khi đặt thành eaxgiá trị lớn. Thông thường (bên ngoài mã-golf) bạn sẽ sử dụng cdqlàm thiết lập cho idivxor edx,edxtrước đódiv
Peter Cordes

5

Sử dụng fastcallquy ước

nền tảng x86 có nhiều quy ước gọi . Bạn nên sử dụng những người vượt qua các tham số trong sổ đăng ký. Trên x86_64, một vài tham số đầu tiên được truyền vào các thanh ghi, vì vậy không có vấn đề gì ở đó. Trên nền tảng 32 bit, quy ước gọi mặc định (cdecl ) truyền các tham số trong ngăn xếp, điều này không tốt cho việc chơi gôn - truy cập các tham số trên ngăn xếp yêu cầu các hướng dẫn dài.

Khi sử dụng fastcalltrên nền tảng 32 bit, 2 tham số đầu tiên thường được truyền vào ecxedx. Nếu chức năng của bạn có 3 tham số, bạn có thể xem xét triển khai nó trên nền tảng 64 bit.

Nguyên mẫu hàm C cho fastcallquy ước (lấy từ ví dụ này trả lời ):

extern int __fastcall SwapParity(int value);                 // MSVC
extern int __attribute__((fastcall)) SwapParity(int value);  // GNU   

Hoặc sử dụng quy ước gọi hoàn toàn tùy chỉnh , bởi vì bạn đang viết bằng asm thuần túy, không nhất thiết phải viết mã để được gọi từ C. Trả lại booleans trong FLAGS thường thuận tiện.
Peter Cordes

5

Trừ -128 thay vì thêm 128

0100 81C38000      ADD     BX,0080
0104 83EB80        SUB     BX,-80

Samely, thêm -128 thay vì trừ 128


1
Tất nhiên, điều này cũng hoạt động theo hướng khác: thêm -128 thay vì phụ 128. Thực tế thú vị: trình biên dịch biết tối ưu hóa này và cũng thực hiện tối ưu hóa liên quan đến việc chuyển < 128thành <= 127để giảm cường độ của toán hạng ngay lập tức cmphoặc gcc luôn thích sắp xếp lại so sánh để giảm cường độ ngay cả khi nó không -129 so với -128.
Peter Cordes

4

Tạo 3 số 0 bằng mul(sau đó inc/ decđể có +1 / -1 cũng như 0)

Bạn có thể zero eax và edx bằng cách nhân với số 0 trong thanh ghi thứ ba.

xor   ebx, ebx      ; 2B  ebx = 0
mul   ebx           ; 2B  eax=edx = 0

inc   ebx           ; 1B  ebx=1

sẽ dẫn đến EAX, EDX và EBX đều bằng 0 chỉ trong bốn byte. Bạn có thể zero EAX và EDX trong ba byte:

xor eax, eax
cdq

Nhưng từ điểm bắt đầu đó, bạn không thể có được một thanh ghi số 0 ở một byte nữa hoặc một thanh ghi +1 hoặc -1 trong 2 byte khác. Thay vào đó, sử dụng kỹ thuật mul.

Ví dụ về trường hợp sử dụng: ghép các số Fibonacci trong hệ nhị phân .

Lưu ý rằng sau khi LOOPvòng lặp kết thúc, ECX sẽ bằng 0 và có thể được sử dụng để zero EDX và EAX; bạn không phải luôn tạo số 0 đầu tiên xor.


1
Điều này hơi khó hiểu. Bạn có thể mở rộng?
NoOneIsHãy

@NoOneIsHãy tôi tin rằng anh ấy muốn đặt ba thanh ghi thành 0, bao gồm EAX và EDX.
NieDzejkob

4

Các thanh ghi CPU và cờ đang ở trạng thái khởi động đã biết

Chúng ta có thể giả định rằng CPU ở trạng thái mặc định đã biết và được ghi lại dựa trên nền tảng và HĐH.

Ví dụ:

DOS http://www.fysnet.net/yourhelp.htmlm

ELF Linux x86 http://asm.sourceforge.net/articles/startup.html


1
Code Golf Rules nói rằng mã của bạn phải hoạt động trên ít nhất một lần thực hiện. Linux chọn không tất cả các reg (trừ RSP) và ngăn xếp trước khi bước vào một quy trình không gian người dùng mới, mặc dù các tài liệu ABI của hệ thống i386 và x86-64 nói rằng chúng "không xác định" khi vào _start. Vì vậy, đúng là trò chơi công bằng để tận dụng điều đó nếu bạn đang viết một chương trình thay vì một chức năng. Tôi đã làm như vậy trong Extreme Fibonacci . (Trong một tệp thực thi được liên kết động, ld.so chạy trước khi nhảy tới _startkhông để lại rác trong sổ đăng ký, nhưng tĩnh chỉ là mã của bạn.)
Peter Cordes

3

Để thêm hoặc trừ 1, hãy sử dụng một byte inchoặc dechướng dẫn nhỏ hơn so với hướng dẫn thêm và đa bội.


Lưu ý rằng chế độ 32 bit có 1 byte inc/dec r32với số thanh ghi được mã hóa trong opcode. Vậy inc ebxlà 1 byte, nhưng inc bllà 2. Vẫn nhỏ hơn add bl, 1tất nhiên, đối với các thanh ghi khác al. Cũng lưu ý rằng inc/ decđể CF không thay đổi, nhưng cập nhật các cờ khác.
Peter Cordes

1
2 cho +2 & -2 trong x86
l4m2

3

lea cho môn toán

Đây có lẽ là một trong những điều đầu tiên người ta tìm hiểu về x86, nhưng tôi để nó ở đây như một lời nhắc nhở. leacó thể được sử dụng để thực hiện phép nhân với 2, 3, 4, 5, 8 hoặc 9 và thêm phần bù.

Ví dụ: để tính toán ebx = 9*eax + 3trong một lệnh (ở chế độ 32 bit):

8d 5c c0 03             lea    0x3(%eax,%eax,8),%ebx

Đây là không có bù:

8d 1c c0                lea    (%eax,%eax,8),%ebx

Ồ Tất nhiên, leacó thể được sử dụng để làm toán như ebx = edx + 8*eax + 3tính toán lập chỉ mục mảng.


1
Có lẽ đáng nói đến đó lea eax, [rcx + 13]là phiên bản không có tiền tố thêm cho chế độ 64 bit. Kích thước toán hạng 32 bit (cho kết quả) và kích thước địa chỉ 64 bit (cho đầu vào).
Peter Cordes

3

Các lệnh vòng lặp và chuỗi nhỏ hơn các chuỗi lệnh thay thế. Hầu hết các hữu ích là loop <label>đó là nhỏ hơn so với chuỗi hướng dẫn hai dec ECXjnz <label>, và lodsbnhỏ hơn mov al,[esi]inc si.


2

mov nhỏ vào các thanh ghi thấp hơn khi áp dụng

Nếu bạn đã biết các bit trên của một thanh ghi là 0, bạn có thể sử dụng một lệnh ngắn hơn để di chuyển ngay lập tức vào các thanh ghi thấp hơn.

b8 0a 00 00 00          mov    $0xa,%eax

đấu với

b0 0a                   mov    $0xa,%al

Sử dụng push/ popcho các bit trên từ 8 đến 0

Tín dụng cho Peter Cordes. xor/ movlà 4 byte, nhưng push/ popchỉ là 3!

6a 0a                   push   $0xa
58                      pop    %eax

mov al, 0xalà tốt nếu bạn không cần nó mở rộng đến mức đầy đủ. Nhưng nếu bạn làm như vậy, xor / Mov là 4 byte so với 3 khi đẩy im8 / pop hoặc leatừ một hằng số đã biết khác. Điều này có thể hữu ích khi kết hợp với mul0 thanh ghi trong 4 byte hoặc cdq, nếu bạn cần rất nhiều hằng số.
Peter Cordes

Trường hợp sử dụng khác sẽ dành cho các hằng số từ [0x80..0xFF], không thể biểu thị dưới dạng im8 mở rộng. Hoặc nếu bạn đã biết các byte trên, ví dụ mov cl, 0x10sau một looplệnh, bởi vì cách duy nhất loopđể không nhảy là khi nó được thực hiện rcx=0. (Tôi đoán bạn đã nói điều này, nhưng ví dụ của bạn sử dụng một xor). Bạn thậm chí có thể sử dụng byte thấp của một thanh ghi cho một thứ khác, miễn là thứ khác đưa nó về 0 (hoặc bất cứ thứ gì) khi bạn hoàn thành. ví dụ: chương trình Fibonacci của tôi giữ -1024ở ebx và sử dụng bl.
Peter Cordes

@PeterCordes Tôi đã thêm kỹ thuật đẩy / bật của bạn
qwr

Có lẽ nên đi vào câu trả lời hiện có về hằng số, nơi anatolyg đã đề xuất nó trong một bình luận . Tôi sẽ chỉnh sửa câu trả lời đó. IMO, bạn nên làm lại cái này để đề xuất sử dụng kích thước toán hạng 8 bit cho nhiều thứ hơn (ngoại trừ xchg eax, r32), ví dụ mov bl, 10/ dec bl/ jnzđể mã của bạn không quan tâm đến các byte cao của RBX.
Peter Cordes

@PeterCordes hmm. Tôi vẫn không chắc chắn về việc khi nào nên sử dụng toán hạng 8 bit, vì vậy tôi không chắc nên đưa câu trả lời nào vào đó.
qwr

2

các FLAGS được thiết lập sau nhiều hướng dẫn

Sau nhiều hướng dẫn số học, Cờ mang theo (không dấu) và Cờ tràn (đã ký) được đặt tự động ( thông tin thêm ). Cờ ký hiệu và Cờ không được đặt sau nhiều phép toán số học và logic. Điều này có thể được sử dụng để phân nhánh có điều kiện.

Thí dụ:

d1 f8                   sar    %eax

ZF được thiết lập theo hướng dẫn này, vì vậy chúng ta có thể sử dụng nó để phân nhánh.


Khi nào bạn đã sử dụng cờ chẵn lẻ? Bạn biết đó là xor ngang của 8 bit thấp của kết quả, phải không? (Bất kể kích thước toán hạng, PF chỉ được đặt từ 8 bit thấp ; xem thêm ). Không phải số chẵn / số lẻ; để kiểm tra ZF sau test al,1; bạn thường không nhận được nó miễn phí. (Hoặc and al,1để tạo một số nguyên 0/1 tùy thuộc vào số lẻ / chẵn.)
Peter Cordes

Dù sao, nếu câu trả lời này cho biết "sử dụng các cờ đã được đặt theo các hướng dẫn khác để tránh test/ cmp", thì đó sẽ là cơ bản cho người mới bắt đầu x86, nhưng vẫn đáng để nâng cấp.
Peter Cordes

@PeterCordes Huh, tôi dường như đã hiểu nhầm cờ chẵn lẻ. Tôi vẫn đang làm việc trên câu trả lời khác của tôi. Tôi sẽ chỉnh sửa câu trả lời. Và như bạn có thể nói, tôi là người mới bắt đầu nên những lời khuyên cơ bản giúp ích.
qwr

2

Sử dụng vòng lặp do-while thay vì vòng lặp while

Đây không phải là x86 cụ thể nhưng là một mẹo lắp ráp người mới bắt đầu áp dụng rộng rãi. Nếu bạn biết một vòng lặp while sẽ chạy ít nhất một lần, hãy viết lại vòng lặp dưới dạng vòng lặp do-while, với kiểm tra điều kiện vòng lặp ở cuối, thường lưu một lệnh nhảy 2 byte. Trong trường hợp đặc biệt, bạn thậm chí có thể sử dụng loop.


2
Liên quan: Tại sao các vòng lặp luôn được biên dịch như thế này? giải thích tại sao do{}while()thành ngữ looping tự nhiên trong lắp ráp (đặc biệt là cho hiệu quả). Cũng lưu ý rằng 2 byte jecxz/ jrcxztrước một vòng lặp hoạt động rất tốt với loopviệc xử lý "trường hợp cần chạy 0 lần" "hiệu quả" (trên các CPU hiếm khi loopkhông chậm). jecxzcũng có thể sử dụng bên trong vòng lặp để thực hiện awhile(ecx){} , với jmpở phía dưới.
Peter Cordes

@PeterCordes đó là một câu trả lời bằng văn bản. Tôi muốn tìm một cách sử dụng để nhảy vào giữa một vòng lặp trong một chương trình golf mã.
qwr

Sử dụng goto jmp và thụt lề ... Vòng lặp theo dõi
RosLuP

2

Sử dụng bất cứ quy ước gọi nào đều thuận tiện

System V x86 sử dụng ngăn xếp và System V x86-64 sử dụng rdi, rsi, rdx, rcx, vv cho các thông số đầu vào, và raxnhư giá trị trả về, nhưng nó là hoàn toàn hợp lý để sử dụng quy ước gọi của riêng bạn. __fastcall sử dụng ecxedxlàm tham số đầu vào, và các trình biên dịch / HĐH khác sử dụng các quy ước riêng của chúng . Sử dụng ngăn xếp và bất cứ điều gì đăng ký làm đầu vào / đầu ra khi thuận tiện.

Ví dụ: Bộ đếm byte lặp lại , sử dụng quy ước gọi thông minh cho giải pháp 1 byte.

Meta: Viết đầu vào vào thanh ghi , Viết đầu ra vào thanh ghi

Các tài nguyên khác: Ghi chú của Agner Fog về các quy ước gọi điện


1
Cuối cùng tôi cũng có ý định đăng câu trả lời của riêng mình cho câu hỏi này về việc tạo ra các quy ước gọi vốn, và những gì hợp lý và không hợp lý.
Peter Cordes

@PeterCordes không liên quan, cách tốt nhất để in trong x86 là gì? Cho đến nay tôi đã tránh được những thách thức đòi hỏi phải in. DOS có vẻ như nó có các ngắt hữu ích cho I / O nhưng tôi chỉ dự định viết câu trả lời 32/64 bit. Cách duy nhất tôi biết là int 0x80yêu cầu một loạt các thiết lập.
qwr

Vâng, int 0x80trong mã 32 bit, hoặc mã syscall64 bit, để gọi sys_write, là cách tốt duy nhất. Đó là những gì tôi đã sử dụng cho Extreme Fibonacci . Trong mã 64 bit __NR_write = 1 = STDOUT_FILENO, vì vậy bạn có thể mov eax, edi. Hoặc nếu các byte trên của EAX bằng 0, mov al, 4trong mã 32 bit. Bạn cũng có thể call printfhoặc puts, tôi đoán và viết câu trả lời "x86 asm cho Linux + glibc". Tôi nghĩ thật hợp lý khi không tính không gian nhập PLT hoặc GOT hoặc mã thư viện.
Peter Cordes

1
Tôi sẽ có xu hướng yêu cầu người gọi vượt qua a char*bufvà tạo chuỗi trong đó, với định dạng thủ công. ví dụ như thế này (được tối ưu hóa một cách vụng về tốc độ) như là FizzBuzz , nơi tôi đã đưa dữ liệu chuỗi vào thanh ghi và sau đó lưu trữ nó mov, bởi vì các chuỗi có độ dài ngắn và cố định.
Peter Cordes

1

Sử dụng di chuyển có điều kiện CMOVccvà bộSETcc

Đây là một lời nhắc nhở cho bản thân tôi, nhưng các hướng dẫn tập hợp có điều kiện tồn tại và các hướng dẫn di chuyển có điều kiện tồn tại trên bộ xử lý P6 (Pentium Pro) hoặc mới hơn. Có nhiều hướng dẫn dựa trên một hoặc nhiều cờ được đặt trong EFLAGS.


1
Tôi đã tìm thấy sự phân nhánh thường nhỏ hơn. Có một số trường hợp phù hợp tự nhiên, nhưng cmovcó opcode 2 byte ( 0F 4x +ModR/M) nên tối thiểu 3 byte. Nhưng nguồn là r / m32, vì vậy bạn có thể tải có điều kiện trong 3 byte. Khác với sự phân nhánh, setcchữu ích trong nhiều trường hợp hơn cmovcc. Tuy nhiên, hãy xem xét toàn bộ tập lệnh, không chỉ các hướng dẫn cơ bản 386. (. Mặc dù SSE2 và hướng dẫn BMI / BMI2 rất lớn mà họ hiếm khi hữu ích rorx eax, ecx, 32là 6 byte, dài hơn mov + ROR đẹp cho hiệu suất, không golf trừ POPCNT hoặc PDEP tiết kiệm nhiều isns.)
Peter Cordes

@PeterCordes cảm ơn, tôi đã thêm setcc.
qwr

1

Tiết kiệm jmp byte bằng cách sắp xếp vào if / then thay vì if / then / other

Điều này chắc chắn là rất cơ bản, chỉ cần nghĩ rằng tôi sẽ đăng bài này như một cái gì đó để suy nghĩ khi chơi golf. Ví dụ, xem xét mã đơn giản sau đây để giải mã ký tự chữ số thập lục phân:

    cmp $'A', %al
    jae .Lletter
    sub $'0', %al
    jmp .Lprocess
.Lletter:
    sub $('A'-10), %al
.Lprocess:
    movzbl %al, %eax
    ...

Điều này có thể được rút ngắn bằng hai byte bằng cách để trường hợp "sau đó" rơi vào trường hợp "khác":

    cmp $'A', %al
    jb .digit
    sub $('A'-'0'-10), %eax
.digit:
    sub $'0', %eax
    movzbl %al, %eax
    ...

Bạn thường làm điều này một cách bình thường khi tối ưu hóa hiệu suất, đặc biệt là khi subđộ trễ thêm trên đường dẫn quan trọng cho một trường hợp không phải là một phần của chuỗi phụ thuộc mang theo vòng lặp (như ở đây khi mỗi chữ số đầu vào là độc lập cho đến khi hợp nhất các đoạn 4 bit ). Nhưng dù sao tôi cũng đoán +1. BTW, ví dụ của bạn có một tối ưu hóa bị bỏ lỡ riêng biệt: nếu cuối cùng bạn sẽ cần một movzxcái cuối cùng thì sub $imm, %alkhông sử dụng EAX để tận dụng mã hóa 2 byte không modrm của op $imm, %al.
Peter Cordes

Ngoài ra, bạn có thể loại bỏ cmpbằng cách làm sub $'A'-10, %al; jae .was_alpha; add $('A'-10)-'0'. (Tôi nghĩ rằng tôi đã có logic đúng). Lưu ý rằng 'A'-10 > '9'vì vậy không có sự mơ hồ. Trừ đi sự hiệu chỉnh cho một chữ cái sẽ bao bọc một chữ số thập phân. Vì vậy, điều này là an toàn nếu chúng tôi giả sử đầu vào của chúng tôi là hex hợp lệ, giống như của bạn.
Peter Cordes

0

Bạn có thể tìm nạp các đối tượng tuần tự từ ngăn xếp bằng cách đặt esi thành đặc biệt và thực hiện một chuỗi lodsd / xchg reg, eax.


Tại sao điều này tốt hơn pop eax/ pop edx/ ...? Nếu bạn cần để chúng trên ngăn xếp, bạn có thể pushquay lại tất cả chúng sau khi khôi phục ESP, vẫn là 2 byte cho mỗi đối tượng mà không cần mov esi,esp. Hay ý bạn là đối với các đối tượng 4 byte trong mã 64 bit, nơi popsẽ nhận được 8 byte? BTW, bạn thậm chí có thể sử dụng popđể lặp qua bộ đệm với hiệu suất tốt hơn lodsd, ví dụ: để bổ sung độ chính xác mở rộng trong Extreme Fibonacci
Peter Cordes

nó chính xác hơn hữu ích sau khi một "es esi, [đặc biệt là kích thước của địa chỉ ret]", sẽ loại trừ sử dụng pop trừ khi bạn có đăng ký dự phòng.
perr ferrie

Oh, cho chức năng args? Khá hiếm khi bạn muốn có nhiều đối số hơn là có các thanh ghi hoặc bạn muốn người gọi để lại một trong bộ nhớ thay vì chuyển tất cả chúng trong các thanh ghi. (Tôi có câu trả lời nửa vời về việc sử dụng các quy ước gọi điện tùy chỉnh, trong trường hợp một trong các quy ước gọi đăng ký tiêu chuẩn không phù hợp hoàn hảo.)
Peter Cordes

cdecl thay vì fastcall sẽ để lại các tham số trên ngăn xếp và thật dễ dàng để có nhiều tham số. Xem github.com/peterferrie/tinycrypt, ví dụ.
perr ferrie

0

Đối với codegolf và ASM: Hướng dẫn sử dụng chỉ sử dụng các thanh ghi, đẩy pop, giảm thiểu bộ nhớ thanh ghi hoặc bộ nhớ ngay lập tức


0

Để sao chép một thanh ghi 64 bit, sử dụng push rcx; pop rdxthay vì 3 byte mov.
Kích thước toán hạng mặc định của đẩy / pop là 64 bit mà không cần tiền tố REX.

  51                      push   rcx
  5a                      pop    rdx
                vs.
  48 89 ca                mov    rdx,rcx

(Tiền tố kích thước toán hạng có thể ghi đè kích thước đẩy / pop thành 16 bit, nhưng kích thước toán hạng đẩy / pop 32 bit không được mã hóa ở chế độ 64 bit ngay cả với REX.W = 0.)

Nếu một hoặc cả hai thanh ghi là r8.. r15, hãy sử dụng movvì đẩy và / hoặc pop sẽ cần tiền tố REX. Trường hợp xấu nhất điều này thực sự mất nếu cả hai đều cần tiền tố REX. Rõ ràng là bạn thường nên tránh r8..r15 dù sao trong mã golf.


Bạn có thể giữ nguồn của mình dễ đọc hơn trong khi phát triển với macro NASM này . Chỉ cần nhớ rằng nó bước trên 8 byte bên dưới RSP. (Trong vùng màu đỏ trong x86-64 System V). Nhưng trong điều kiện bình thường, nó là sự thay thế thả xuống cho 64-bit mov r64,r64hoặcmov r64, -128..127

    ; mov  %1, %2       ; use this macro to copy 64-bit registers in 2 bytes (no REX prefix)
%macro MOVE 2
    push  %2
    pop   %1
%endmacro

Ví dụ:

   MOVE  rax, rsi            ; 2 bytes  (push + pop)
   MOVE  rbp, rdx            ; 2 bytes  (push + pop)
   mov   ecx, edi            ; 2 bytes.  32-bit operand size doesn't need REX prefixes

   MOVE  r8, r10             ; 4 bytes, don't use
   mov   r8, r10             ; 3 bytes, REX prefix has W=1 and the bits for reg and r/m being high

   xchg  eax, edi            ; 1 byte  (special xchg-with-accumulator opcodes)
   xchg  rax, rdi            ; 2 bytes (REX.W + that)

   xchg  ecx, edx            ; 2 bytes (normal xchg + modrm)
   xchg  rcx, rdx            ; 3 bytes (normal REX + xchg + modrm)

Một xchgphần của ví dụ là bởi vì đôi khi bạn cần lấy một giá trị vào EAX hoặc RAX và không quan tâm đến việc giữ bản sao cũ. Push / pop không giúp bạn thực sự trao đổi, mặc dù.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.