Kiat untuk bermain golf dalam kode mesin x86 / x64

27

Saya perhatikan bahwa tidak ada pertanyaan seperti itu, jadi ini dia:

Apakah Anda memiliki tips umum untuk bermain golf dalam kode mesin? Jika tip hanya berlaku untuk lingkungan tertentu atau konvensi pemanggilan, harap sebutkan itu dalam jawaban Anda.

Harap hanya satu tip per jawaban (lihat di sini ).

ბიმო
sumber

Jawaban:

11

mov-menengah adalah mahal untuk konstanta

Ini mungkin jelas, tetapi saya masih akan menaruhnya di sini. Secara umum, terbayar untuk memikirkan representasi bit-level dari angka ketika Anda perlu menginisialisasi nilai.

Inisialisasi eaxdengan 0:

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

harus dipersingkat ( untuk kinerja serta ukuran kode ) menjadi

31 c0                   xor    %eax,%eax

Inisialisasi eaxdengan -1:

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

dapat disingkat menjadi

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

atau

83 c8 ff                or     $-1,%eax

Atau lebih umum, setiap nilai sign-extended 8-bit dapat dibuat dalam 3 byte dengan push -12(2 byte) / pop %eax(1 byte). Ini bahkan berfungsi untuk register 64-bit tanpa awalan REX ekstra; push/ popukuran operan standar = 64.

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

Atau diberikan konstanta yang diketahui dalam register, Anda dapat membuat konstanta terdekat lainnya menggunakan lea 123(%eax), %ecx(3 byte) Ini berguna jika Anda membutuhkan register yang memusatkan perhatian dan konstanta; xor-nol (2 byte) + lea-disp8(3 byte).

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

Lihat juga Atur semua bit dalam register CPU ke 1 secara efisien

ბიმო
sumber
Juga, untuk menginisialisasi register dengan nilai (8-bit) kecil selain 0: gunakan misalnya push 200; pop edx- 3 byte untuk inisialisasi.
anatolyg
2
BTW untuk menginisialisasi register ke -1, gunakan dec, misalnyaxor eax, eax; dec eax
anatolyg
@anatolyg: 200 adalah contoh yang buruk, tidak cocok dengan tanda-extended-imm8. Tapi ya, push imm8/ pop regadalah 3 byte, dan fantastis untuk konstanta 64-bit pada x86-64, di mana dec/ incadalah 2 byte. Dan push r64/ pop 64(2 byte) bahkan dapat menggantikan 3 byte mov r64, r64(3 byte dengan REX). Lihat juga Setel semua bit dalam register CPU ke 1 secara efisien untuk hal-hal seperti lea eax, [rcx-1]diberi nilai yang diketahui eax(misalnya jika perlu register nol dan konstanta lain, gunakan saja LEA alih-alih push / pop
Peter Cordes
10

Dalam banyak kasus, instruksi berbasis akumulator (yaitu yang mengambil (R|E)AXsebagai operan tujuan) adalah 1 byte lebih pendek dari instruksi kasus umum; lihat pertanyaan ini di StackOverflow.

Govind Parmar
sumber
Biasanya yang paling berguna adalah al, imm8kasus khusus, seperti or al, 0x20/ sub al, 'a'/ cmp al, 'z'-'a'/ ja .non_alphabeticmasing-masing 2 byte, bukan 3. Menggunakan aluntuk data karakter juga memungkinkan lodsbdan / atau stosb. Atau gunakan aluntuk menguji sesuatu tentang byte rendah EAX, seperti lodsd/ test al, 1/ setnz clmembuat cl = 1 atau 0 untuk odd / even. Tetapi dalam kasus yang jarang di mana Anda membutuhkan 32-bit segera, maka pasti op eax, imm32, seperti dalam jawaban kunci kroma saya
Peter Cordes
8

Pilih konvensi pemanggilan Anda untuk menempatkan args di tempat yang Anda inginkan.

Bahasa jawaban Anda adalah asm (sebenarnya kode mesin), jadi perlakukan itu sebagai bagian dari program yang ditulis dalam asm, bukan C-compile-for-x86. Fungsi Anda tidak harus mudah dipanggil dari C dengan konvensi panggilan standar apa pun. Itu bonus yang bagus jika tidak dikenakan biaya byte tambahan.

Dalam program asm murni, adalah normal bagi beberapa fungsi pembantu untuk menggunakan konvensi panggilan yang nyaman bagi mereka dan bagi pemanggil mereka. Fungsi-fungsi tersebut mendokumentasikan konvensi panggilan mereka (input / output / clobbers) dengan komentar.

Dalam kehidupan nyata, bahkan program AS pun cenderung (saya pikir) cenderung menggunakan konvensi pemanggilan yang konsisten untuk sebagian besar fungsi (terutama di seluruh file sumber yang berbeda), tetapi fungsi penting yang diberikan dapat melakukan sesuatu yang istimewa. Dalam kode-golf, Anda mengoptimalkan omong kosong dari satu fungsi tunggal, jadi jelas ini penting / spesial.


Untuk menguji fungsi Anda dari program C, dapat menulis pembungkus yang menempatkan args di tempat yang tepat, menyimpan / mengembalikan register tambahan yang Anda clobber, dan memasukkan nilai e/raxbaliknya jika belum ada di sana.


Batas-batas apa yang masuk akal: apa pun yang tidak membebani penelepon:

  • ESP / RSP harus dilestarikan dengan panggilan; reger integer lainnya adalah permainan yang adil. (RBP dan RBX biasanya dipelihara dengan panggilan dalam konvensi normal, tetapi Anda bisa mengalahkan keduanya.)
  • Arg apa pun dalam register apa pun (kecuali RSP) masuk akal, tetapi meminta penelepon untuk menyalin argumen yang sama ke beberapa register tidak.
  • Memerlukan DF (tanda arah string untuk lods/ stos/ dll.) Agar jelas (ke atas) saat panggilan / ret adalah normal. Membiarkannya tidak ditentukan pada panggilan / ret akan baik-baik saja. Membutuhkannya untuk dihapus atau ditetapkan pada entri tetapi kemudian membiarkannya dimodifikasi ketika Anda kembali akan aneh.

  • Mengembalikan nilai FP di x87 st0masuk akal, tetapi kembali st3dengan sampah di register x87 lain tidak. Penelepon harus membersihkan tumpukan x87. Bahkan kembali st0dengan register tumpukan tinggi yang tidak kosong juga akan dipertanyakan (kecuali jika Anda mengembalikan beberapa nilai).

  • Fungsi Anda akan dipanggil call, demikian [rsp]juga alamat pengirim Anda. Anda dapat menghindari call/ retpada x86 menggunakan register tautan seperti lea rbx, [ret_addr]/ jmp functiondan kembali dengan jmp rbx, tetapi itu tidak "masuk akal". Itu tidak seefisien panggilan / ret, jadi itu bukan sesuatu yang masuk akal Anda akan menemukan dalam kode nyata.
  • Memanjat memori tak terbatas di atas RSP tidak masuk akal, tetapi mengacaukan fungsi Anda pada stack diperbolehkan dalam kebiasaan panggilan biasa. x64 Windows membutuhkan 32 byte ruang bayangan di atas alamat pengirim, sementara x86-64 System V memberi Anda zona merah 128 byte di bawah RSP, jadi salah satunya masuk akal. (Atau bahkan zona merah yang jauh lebih besar, terutama dalam program yang berdiri sendiri daripada fungsi.)

Borderline cases: menulis fungsi yang menghasilkan urutan dalam array, mengingat 2 elemen pertama sebagai argumen fungsi . Saya memilih agar penelepon menyimpan awal urutan ke dalam array dan hanya meneruskan pointer ke array. Ini jelas menekuk persyaratan pertanyaan itu. Aku dianggap mengambil args dikemas ke dalam xmm0untuk movlps [rdi], xmm0, yang juga akan menjadi konvensi pemanggilan aneh.


Kembalikan boolean dalam FLAGS (kode kondisi)

Panggilan sistem OS X melakukan ini ( CF=0berarti tidak ada kesalahan): Apakah dianggap praktik yang buruk untuk menggunakan register bendera sebagai nilai pengembalian boolean? .

Segala kondisi yang dapat diperiksa dengan satu JCC sangat masuk akal, terutama jika Anda dapat memilih satu yang memiliki relevansi semantik dengan masalah tersebut. (misalnya fungsi membandingkan mungkin mengatur bendera sehingga jneakan diambil jika mereka tidak sama).


Membutuhkan arg yang sempit (seperti a char) untuk bertanda atau nol diperpanjang menjadi 32 atau 64 bit.

Ini tidak masuk akal; menggunakan movzxatau movsx untuk menghindari penurunan parsial register adalah normal dalam asm x86 modern. Bahkan dentang / LLVM sudah membuat kode yang tergantung pada ekstensi tidak berdokumen pada konvensi pemanggilan Sistem x86-64: args yang lebih sempit dari 32 bit bertanda atau nol diperluas menjadi 32 bit oleh pemanggil .

Anda dapat mendokumentasikan / mendeskripsikan ekstensi ke 64 bit dengan menulis uint64_tatau int64_tdalam prototipe Anda jika Anda mau. mis. sehingga Anda dapat menggunakan loopinstruksi, yang menggunakan seluruh 64 bit RCX kecuali Anda menggunakan awalan ukuran-alamat untuk menimpa ukuran ke ECX 32 bit (ya, ukuran alamat bukan ukuran operan).

Perhatikan bahwa longhanya tipe 32-bit pada Windows 64-bit ABI, dan Linux x32 ABI ; uint64_ttidak ambigu dan lebih pendek untuk diketik daripada unsigned long long.


Konvensi panggilan yang ada:

  • Windows 32-bit __fastcall, sudah disarankan oleh jawaban lain : integer args di ecxdan edx.

  • x86-64 Sistem V : melewatkan banyak argumen dalam register, dan memiliki banyak register yang berantakan yang dapat Anda gunakan tanpa awalan REX. Lebih penting lagi, itu sebenarnya dipilih untuk memungkinkan kompiler untuk inline memcpyatau memset dengan rep movsbmudah: 6 integer / pointer arg pertama dilewatkan dalam RDI, RSI, RDX, RCX, R8, R9.

    Jika fungsi Anda menggunakan lodsd/ stosddi dalam loop yang berjalan rcxkali (dengan loopinstruksi), Anda dapat mengatakan "callable from C seperti int foo(int *rdi, const int *rsi, int dummy, uint64_t len)dengan konvensi pemanggilan System V x86-64". contoh: chromakey .

  • GCC 32-bit regparm: Integer args dalam EAX , ECX, EDX, return dalam EAX (atau EDX: EAX). Memiliki arg pertama dalam register yang sama dengan nilai pengembalian memungkinkan beberapa optimasi, seperti kasus ini dengan contoh pemanggil dan prototipe dengan atribut fungsi . Dan tentu saja AL / EAX khusus untuk beberapa instruksi.

  • Linux x32 ABI menggunakan pointer 32-bit dalam mode panjang, sehingga Anda dapat menyimpan awalan REX saat memodifikasi pointer ( contoh case-use ). Anda masih dapat menggunakan ukuran alamat 64-bit, kecuali jika Anda memiliki bilangan bulat negatif 32-bit yang diperpanjang di register (jadi itu akan menjadi nilai besar yang tidak ditandatangani jika Anda melakukannya [rdi + rdx]).

    Perhatikan bahwa push rsp/ pop raxadalah 2 byte, dan setara dengan mov rax,rsp, sehingga Anda masih dapat menyalin register 64-bit penuh dalam 2 byte.

Peter Cordes
sumber
Ketika tantangan meminta untuk mengembalikan array, apakah menurut Anda mengembalikan pada stack itu masuk akal? Saya pikir itulah yang akan dilakukan kompiler ketika mengembalikan struct dengan nilai.
qwr
@ qwr: tidak, konvensi panggilan umum melewati pointer tersembunyi ke nilai kembali. (Beberapa konvensi melewati / mengembalikan struct kecil dalam register). C / C ++ mengembalikan struct dengan nilai di bawah kap , dan lihat akhir Bagaimana objek bekerja di x86 di tingkat perakitan? . Perhatikan bahwa array yang lewat (di dalam struct) benar-benar menyalinnya ke stack untuk x86-64 SysV: Jenis data C11 apa yang merupakan array menurut AMD64 ABI , tetapi Windows x64 melewatkan pointer non-const.
Peter Cordes
jadi apa yang menurut Anda masuk akal atau tidak? Apakah Anda menghitung x86 di bawah aturan ini codegolf.meta.stackexchange.com/a/8507/17360
qwr
1
@ qwr: x86 bukan "bahasa berbasis stack". x86 adalah mesin register dengan RAM , bukan mesin stack . Mesin stack seperti notasi pemoles terbalik, seperti register x87. fld / fld / faddp. stack-stack x86 tidak cocok dengan model itu: semua konvensi panggilan normal membiarkan RSP tidak dimodifikasi, atau memunculkan argumen dengan ret 16; mereka tidak memunculkan alamat pengirim, mendorong array, lalu push rcx/ ret. Penelepon harus mengetahui ukuran array atau telah menyimpan RSP di suatu tempat di luar tumpukan untuk menemukan sendiri.
Peter Cordes
Panggilan dorong alamat instruksi setelah panggilan di stack jmp berfungsi disebut; ret pop alamat dari stack dan jmp ke alamat itu
RosLuP
7

Gunakan penyandian bentuk pendek kasus khusus untuk AL / AX / EAX, dan formulir pendek lainnya dan instruksi byte tunggal

Contoh mengasumsikan mode 32/64-bit, di mana ukuran operan default adalah 32 bit. Awalan ukuran operan mengubah instruksi ke AX alih-alih EAX (atau terbalik dalam mode 16-bit).

  • inc/decregister (selain 8-bit): inc eax/ dec ebp. (Bukan x86-64: 0x4xbyte opcode digunakan kembali sebagai awalan REX, jadi inc r/m32adalah satu-satunya penyandian.)

    8-bit inc bladalah 2 byte, menggunakan inc r/m8opcode + ModR / M operand encoding . Jadi gunakan inc ebxuntuk menambah bl, jika aman. (mis. jika Anda tidak memerlukan hasil ZF dalam kasus di mana byte atas mungkin tidak nol).

  • scasd: e/rdi+=4, mensyaratkan bahwa register menunjuk ke memori yang dapat dibaca. Terkadang bermanfaat bahkan jika Anda tidak peduli dengan hasil FLAGS (seperti cmp eax,[rdi]/ rdi+=4). Dan dalam mode 64-bit, scasbdapat berfungsi sebagai 1-byteinc rdi , jika lodsb atau stosb tidak berguna.

  • xchg eax, r32: Ini adalah di mana 0x90 NOP berasal dari: xchg eax,eax. Contoh: mengatur ulang 3 register dengan dua xchginstruksi dalam cdq/ idivloop untuk GCD dalam 8 byte di mana sebagian besar instruksi adalah single-byte, termasuk penyalahgunaan inc ecx/ loopbukannya test ecx,ecx/jnz

  • cdq: tandatangani-rentangkan EAX ke dalam EDX: EAX, yaitu menyalin bit EAX yang tinggi ke semua bit EDX. Untuk membuat nol dengan diketahui non-negatif, atau untuk mendapatkan 0 / -1 untuk ditambahkan / sub atau topeng. pelajaran sejarah x86: cltqvs.movslq , dan juga AT&T vs Intel mnemonics untuk ini dan yang terkait cdqe.

  • lodsb / d : suka mov eax, [rsi]/ rsi += 4tanpa bendera yang rusak. (Dengan anggapan DF jelas, konvensi panggilan standar mana yang diperlukan pada entri fungsi.) Juga stosb / d, terkadang scas, dan lebih jarang bergerak / cmps.

  • push/ pop reg. misalnya dalam mode 64-bit, push rsp/ pop rdiadalah 2 byte, tetapi mov rdi, rspmembutuhkan awalan REX dan 3 byte.

xlatbada, tetapi jarang bermanfaat. Tabel pencarian besar adalah sesuatu yang harus dihindari. Saya juga tidak pernah menemukan penggunaan untuk AAA / DAA atau instruksi paket-BCD atau 2-ASCII lainnya.

1-byte lahf/ sahfjarang bermanfaat. Anda bisa lahf / and ah, 1sebagai alternatif setc ah, tetapi biasanya tidak berguna.

Dan untuk CF secara khusus, ada sbb eax,eaxuntuk mendapatkan 0 / -1, atau bahkan 1-byte yang tidak didokumentasikan tetapi didukung secara universal salc(atur AL dari Carry) yang secara efektif tidak sbb al,almempengaruhi flag. (Dihapus di x86-64). Saya menggunakan SALC dalam Tantangan Penghargaan Pengguna # 1: Dennis ♦ .

1-byte cmc/ clc/ stc(flip ("pelengkap"), jelas, atau set CF) jarang berguna, meskipun saya menemukan penggunaan untukcmc penambahan presisi-tinggi dengan basis 10 ^ 9 potongan. Untuk mengatur / menghapus CF tanpa syarat, biasanya mengatur agar itu terjadi sebagai bagian dari instruksi lain, misalnya xor eax,eaxmembersihkan CF dan juga EAX. Tidak ada instruksi yang setara untuk flag kondisi lain, hanya DF (arah string) dan IF (interupsi). Bendera carry khusus untuk banyak instruksi; shift mengaturnya, adc al, 0dapat menambahkannya ke AL ​​dalam 2 byte, dan saya sebutkan sebelumnya SALC tidak berdokumen.

stdSaya cldjarang terlihat sepadan . Khususnya dalam kode 32-bit, lebih baik gunakan saja decpada pointer dan movatau sumber memori operan ke instruksi ALU daripada mengatur DF begitu lodsb/ stosbturun ke bawah bukan ke atas. Biasanya jika Anda perlu ke bawah sama sekali, Anda masih memiliki pointer lain naik, jadi Anda akan membutuhkan lebih dari satu stddan cldseluruh fungsi untuk menggunakan lods/ stosuntuk keduanya. Sebagai gantinya, cukup gunakan instruksi string untuk arah ke atas. (Konvensi panggilan standar menjamin DF = 0 pada entri fungsi, sehingga Anda dapat menganggap itu gratis tanpa menggunakan cld.)


8086 sejarah: mengapa pengkodean ini ada

Di asli 8086, AX sangat istimewa: petunjuk suka lodsb/ stosb, cbw, mul/ divdan lain-lain menggunakannya secara implisit. Tentu saja masih demikian; x86 saat ini belum menjatuhkan opcodes 8086 (setidaknya tidak ada yang secara resmi didokumentasikan). Tetapi kemudian CPU menambahkan instruksi baru yang memberikan cara yang lebih baik / lebih efisien untuk melakukan sesuatu tanpa menyalin atau menukar mereka ke AX terlebih dahulu. (Atau ke EAX dalam mode 32-bit.)

misal 8086 tidak memiliki tambahan tambahan seperti movsx/ movzxuntuk memuat atau memindahkan + sign-extended, atau 2 dan 3 operan imul cx, bx, 1234yang tidak menghasilkan hasil setengah tinggi dan tidak memiliki operan implisit.

Juga, hambatan utama 8086 adalah instruksi-ambil, jadi mengoptimalkan ukuran kode penting untuk kinerja saat itu . Perancang ISA 8086 (Stephen Morse) menghabiskan banyak ruang pengkodean opcode pada case khusus untuk AX / AL, termasuk opcode khusus (E) AX / AL-tujuan untuk semua instruksi ALU- direct-src dasar , hanya opcode + direct tanpa byte ModR / M. 2-byte add/sub/and/or/xor/cmp/test/... AL,imm8atau AX,imm16atau (dalam mode 32-bit) EAX,imm32.

Tetapi tidak ada kasus khusus untuk EAX,imm8, sehingga pengkodean ModR / M reguler add eax,4lebih pendek.

Asumsinya adalah jika Anda akan mengerjakan beberapa data, Anda akan menginginkannya di AX / AL, jadi bertukar register dengan AX adalah sesuatu yang mungkin ingin Anda lakukan, mungkin bahkan lebih sering daripada menyalin register ke AX dengan mov.

Segala sesuatu tentang 8086 instruksi pengkodean mendukung paradigma ini, dari instruksi seperti lodsb/wuntuk semua pengkodean kasus khusus untuk segera dengan EAX hingga penggunaan implisitnya bahkan untuk penggandaan / pembagian.


Jangan terbawa; itu tidak otomatis menang untuk menukar semuanya ke EAX, terutama jika Anda perlu menggunakan segera dengan register 32-bit, bukan 8-bit. Atau jika Anda perlu interleave operasi pada beberapa variabel dalam register sekaligus. Atau jika Anda menggunakan instruksi dengan 2 register, tidak segera sama sekali.

Tetapi selalu ingat: apakah saya melakukan sesuatu yang lebih pendek di EAX / AL? Dapatkah saya mengatur ulang sehingga saya memiliki ini dalam AL, atau apakah saya saat ini mengambil keuntungan lebih baik dari AL dengan apa yang sudah saya gunakan untuk itu.

Campurkan operasi 8-bit dan 32-bit secara bebas untuk mengambil keuntungan kapan pun aman untuk melakukannya (Anda tidak perlu melakukan daftar lengkap atau apa pun).

Peter Cordes
sumber
cdqberguna untuk divkebutuhan yang memusatkan perhatian edxdalam banyak kasus.
qwr
1
@ qwr: benar, Anda dapat menyalahgunakan cdqsebelum tidak ditandatangani divjika Anda tahu dividen Anda di bawah 2 ^ 31 (yaitu non-negatif ketika diperlakukan sebagai ditandatangani), atau jika Anda menggunakannya sebelum menetapkan eaxke nilai berpotensi-besar. Biasanya (di luar kode-golf) Anda akan menggunakan cdqsebagai pengaturan untuk idiv, dan xor edx,edxsebelumdiv
Peter Cordes
5

Gunakan fastcallkonvensi

Platform x86 memiliki banyak konvensi panggilan . Anda harus menggunakan yang lulus parameter dalam register. Pada x86_64, beberapa parameter pertama dilewatkan dalam register, jadi tidak ada masalah di sana. Pada platform 32-bit, konvensi pemanggilan standar ( cdecl) melewati parameter dalam stack, yang tidak baik untuk bermain golf - mengakses parameter pada stack membutuhkan instruksi panjang.

Saat menggunakan fastcallplatform 32-bit, 2 parameter pertama biasanya diteruskan ecxdan edx. Jika fungsi Anda memiliki 3 parameter, Anda dapat mempertimbangkan untuk mengimplementasikannya pada platform 64-bit.

Prototipe fungsi C untuk fastcallkonvensi (diambil dari contoh jawaban ini ):

extern int __fastcall SwapParity(int value);                 // MSVC
extern int __attribute__((fastcall)) SwapParity(int value);  // GNU   
anatolyg
sumber
Atau gunakan konvensi pemanggilan kustom sepenuhnya , karena Anda menulis dalam asm murni, belum tentu menulis kode untuk dipanggil dari C. Mengembalikan booleans dalam BENDERA sering nyaman.
Peter Cordes
5

Kurangi -128 alih-alih tambahkan 128

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

Samely, tambahkan -128 bukannya kurangi 128

l4m2
sumber
1
Hal ini juga bekerja ke arah lain, tentu saja: tambahkan -128 bukan sub 128. Fun fakta: compiler tahu optimasi ini, dan juga melakukan optimasi terkait untuk mengubah < 128ke dalam <= 127untuk mengurangi besarnya operan langsung untuk cmp, atau gcc selalu lebih suka menata ulang membandingkan untuk mengurangi besarnya bahkan jika itu bukan -129 vs -128.
Peter Cordes
4

Buat 3 nol dengan mul(lalu inc/ decuntuk mendapatkan +1 / -1 dan juga nol)

Anda dapat nol eax dan edx dengan mengalikan dengan nol di register ketiga.

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

inc   ebx           ; 1B  ebx=1

akan menghasilkan EAX, EDX, dan EBX semuanya menjadi nol hanya dalam empat byte. Anda dapat mem-nolkan EAX dan EDX dalam tiga byte:

xor eax, eax
cdq

Tetapi dari titik awal itu Anda tidak bisa mendapatkan register zeroed 3 dalam satu byte lagi, atau register +1 atau -1 dalam 2 byte lainnya. Sebaliknya, gunakan teknik mul.

Contoh penggunaan-huruf: menggabungkan angka-angka Fibonacci dalam biner .

Perhatikan bahwa setelah LOOPloop selesai, ECX akan menjadi nol dan dapat digunakan untuk nol EDX dan EAX; Anda tidak selalu harus membuat nol pertama dengan xor.

Peter Ferrie
sumber
1
Ini agak membingungkan. Bisakah Anda berkembang?
NoOneIsHere
@NoOneIsHere Saya percaya dia ingin mengatur tiga register ke 0, termasuk EAX dan EDX.
NieDzejkob
4

Register dan flag CPU berada dalam status startup yang dikenal

Kita dapat mengasumsikan bahwa CPU dalam keadaan default yang dikenal dan didokumentasikan berdasarkan pada platform dan OS.

Sebagai contoh:

DOS http://www.fysnet.net/yhelhel.htm

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

640KB
sumber
1
Aturan Golf aturan mengatakan kode Anda harus bekerja pada setidaknya satu implementasi. Linux memilih untuk mem-nol semua regs (kecuali RSP) dan susun sebelum memasuki proses ruang pengguna yang baru, meskipun Sistem V i386 dan x86-64 ABI docs mengatakan mereka "tidak terdefinisi" saat masuk _start. Jadi ya itu permainan yang adil untuk mengambil keuntungan dari itu jika Anda sedang menulis sebuah program alih-alih fungsi. Saya melakukannya di Extreme Fibonacci . (Dalam executable yang terhubung secara dinamis, ld.so berjalan sebelum melompat ke Anda _start, dan tidak meninggalkan sampah di register, tetapi statis hanyalah kode Anda.)
Peter Cordes
3

Untuk menambah atau mengurangi 1, gunakan satu byte incatau decinstruksi yang lebih kecil dari multibyte tambah dan sub instruksi.

pengguna230118
sumber
Perhatikan bahwa mode 32-bit memiliki 1-byte inc/dec r32dengan nomor register yang dikodekan dalam opcode. Jadi inc ebx1 byte, tetapi inc bl2. Masih lebih kecil add bl, 1dari tentu saja, untuk register selain al. Perhatikan juga bahwa inc/ decbiarkan CF tidak dimodifikasi, tetapi perbarui flag lainnya.
Peter Cordes
1
2 untuk +2 & -2 di x86
l4m2
3

lea untuk matematika

Ini mungkin salah satu hal pertama yang dipelajari tentang x86, tapi saya tinggalkan di sini sebagai pengingat. leadapat digunakan untuk melakukan perkalian dengan 2, 3, 4, 5, 8, atau 9, dan menambahkan offset.

Misalnya, untuk menghitung ebx = 9*eax + 3dalam satu instruksi (dalam mode 32-bit):

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

Ini dia tanpa offset:

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

Wow! Tentu saja, leadapat digunakan juga untuk melakukan matematika seperti ebx = edx + 8*eax + 3untuk menghitung pengindeksan array.

qwr
sumber
1
Mungkin perlu disebutkan bahwa itu lea eax, [rcx + 13]adalah versi awalan tanpa tambahan untuk mode 64-bit. Ukuran operan 32-bit (untuk hasilnya) dan ukuran alamat 64-bit (untuk input).
Peter Cordes
3

Instruksi loop dan string lebih kecil dari urutan instruksi alternatif. Paling berguna adalah loop <label>yang lebih kecil dari dua urutan instruksi dec ECXdan jnz <label>, dan lodsblebih kecil dari mov al,[esi]dan inc si.

pengguna230118
sumber
2

mov kecil segera masuk ke register yang lebih rendah bila berlaku

Jika Anda sudah tahu bit atas dari register adalah 0, Anda dapat menggunakan instruksi yang lebih pendek untuk memindahkan langsung ke register yang lebih rendah.

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

melawan

b0 0a                   mov    $0xa,%al

Gunakan push/ popuntuk imm8 ke nol bit atas

Penghargaan untuk Peter Cordes. xor/ movadalah 4 byte, tetapi push/ pophanya 3!

6a 0a                   push   $0xa
58                      pop    %eax
qwr
sumber
mov al, 0xabagus jika Anda tidak perlu diperpanjang nol ke reg penuh. Tetapi jika Anda melakukannya, xor / mov adalah 4 byte vs 3 untuk push imm8 / pop atau leadari konstanta lain yang diketahui. Ini bisa berguna dalam kombinasi dengan mulnol register 3 dalam 4 byte , atau cdq, jika Anda membutuhkan banyak konstanta.
Peter Cordes
Kasus penggunaan lainnya adalah untuk konstanta dari [0x80..0xFF], yang tidak dapat direpresentasikan sebagai imm8 yang diperpanjang tanda. Atau jika Anda sudah tahu byte atas, misalnya mov cl, 0x10setelah loopinstruksi, karena satu-satunya cara untuk looptidak melompat adalah ketika dibuat rcx=0. (Saya kira Anda mengatakan ini, tetapi contoh Anda menggunakan a xor). Anda bahkan dapat menggunakan byte rendah dari register untuk sesuatu yang lain, selama sesuatu yang lain mengembalikannya ke nol (atau apa pun) ketika Anda selesai. mis. Program Fibonacci saya terus -1024naik, dan menggunakan bl.
Peter Cordes
@PeterCordes Saya telah menambahkan teknik push / pop Anda
qwr
Mungkin harus masuk ke jawaban yang ada tentang konstanta, di mana anatolyg sudah menyarankannya dalam komentar . Saya akan mengedit jawaban itu. IMO Anda harus mengolah yang ini untuk menyarankan menggunakan ukuran operan 8-bit untuk lebih banyak barang (kecuali xchg eax, r32) misalnya mov bl, 10/ dec bl/ jnzjadi kode Anda tidak peduli dengan byte tinggi RBX.
Peter Cordes
@PeterCordes hmm. Saya masih tidak yakin kapan harus menggunakan operan 8-bit jadi saya tidak yakin apa yang harus dimasukkan dalam jawaban itu.
qwr
2

The FLAGS ditetapkan setelah banyak instruksi

Setelah banyak instruksi aritmatika, Bendera Carry (tidak bertanda) dan Bendera Overflow (ditandatangani) diatur secara otomatis ( info lebih lanjut ). Tanda Bendera dan Nol Bendera ditetapkan setelah banyak operasi aritmatika dan logis. Ini dapat digunakan untuk percabangan bersyarat.

Contoh:

d1 f8                   sar    %eax

ZF diatur oleh instruksi ini, sehingga kami dapat menggunakannya untuk percabangan opsional.

qwr
sumber
Kapan Anda pernah menggunakan bendera paritas? Anda tahu itu adalah xor horizontal dari 8 bit hasil, bukan? (Terlepas dari ukuran operan, PF diatur hanya dari 8 bit yang rendah ; lihat juga ). Bukan genap / ganjil; untuk itu periksa ZF setelah test al,1; Anda biasanya tidak mendapatkannya secara gratis. (Atau and al,1untuk membuat bilangan bulat 0/1 tergantung ganjil / genap.)
Peter Cordes
Pokoknya, jika jawaban ini mengatakan "gunakan flag yang sudah diatur oleh instruksi lain untuk menghindari test/ cmp", maka itu akan menjadi x86 pemula yang cukup mendasar, tetapi masih layak mendapat upvote.
Peter Cordes
@PeterCordes Huh, sepertinya saya salah mengerti bendera paritas. Saya masih mengerjakan jawaban saya yang lain. Saya akan mengedit jawabannya. Dan seperti yang mungkin Anda tahu, saya seorang pemula jadi tips dasar membantu.
qwr
2

Gunakan do-while loop sebagai ganti while loop

Ini bukan spesifik x86 tetapi tip perakitan pemula yang berlaku luas. Jika Anda tahu loop sementara akan berjalan setidaknya sekali, menulis ulang loop sebagai loop do-while, dengan memeriksa kondisi loop di akhir, sering menyimpan instruksi lompat 2 byte. Dalam kasus khusus Anda bahkan mungkin dapat menggunakan loop.

qwr
sumber
2
Terkait: Mengapa loop selalu dikompilasi seperti ini? menjelaskan alasannyado{}while() idiom pengulangan alami dalam perakitan (terutama untuk efisiensi). Perhatikan juga bahwa 2-byte jecxz/ jrcxzsebelum loop bekerja dengan sangat baik loopuntuk menangani "kebutuhan untuk menjalankan nol kali" case "secara efisien" (pada CPU langka di mana looptidak lambat). jecxzjuga dapat digunakan di dalam loop untuk mengimplementasikanwhile(ecx){} , dengan jmpdi bagian bawah.
Peter Cordes
@PeterCordes itu adalah jawaban yang ditulis dengan sangat baik. Saya ingin menemukan kegunaan untuk melompat ke tengah lingkaran dalam program golf kode.
qwr
Gunakan goto jmp dan indentation ... Loop follow
RosLuP
2

Gunakan konvensi panggilan apa pun yang nyaman

Sistem V x86 menggunakan stack dan sistem V x86-64 kegunaan rdi, rsi, rdx, rcx, dll untuk parameter input, dan raxsebagai nilai kembali, tetapi masuk akal untuk menggunakan konvensi menelepon Anda sendiri. __Panggilan cepat digunakan ecxdan edxsebagai parameter input, dan kompiler / OS lain menggunakan konvensi mereka sendiri . Gunakan tumpukan dan register apa pun sebagai input / output saat nyaman.

Contoh: Penghitung byte berulang , menggunakan konvensi panggilan pintar untuk solusi 1 byte.

Meta: Menulis input ke register , Menulis output ke register

Sumber lain: Catatan Agner Fog tentang konvensi pemanggilan

qwr
sumber
1
Saya akhirnya sempat memposting jawaban saya sendiri tentang pertanyaan ini tentang membuat panggilan konvensi, dan apa yang masuk akal vs tidak masuk akal.
Peter Cordes
@PeterCordes tidak terkait, apa cara terbaik untuk mencetak di x86? Sejauh ini saya telah menghindari tantangan yang membutuhkan pencetakan. DOS sepertinya memiliki interupsi yang berguna untuk I / O tetapi saya hanya berencana untuk menulis jawaban 32/64 bit. Satu-satunya cara saya tahu adalah int 0x80yang membutuhkan banyak pengaturan.
qwr
Ya, int 0x80dalam kode 32-bit, atau syscalldalam kode 64-bit, untuk memohon sys_write, adalah satu-satunya cara yang baik. Itu yang saya gunakan untuk Extreme Fibonacci . Dalam kode 64-bit __NR_write = 1 = STDOUT_FILENO,, jadi Anda bisa mov eax, edi. Atau jika byte atas EAX adalah nol, mov al, 4dalam kode 32-bit. Anda juga bisa call printfatau puts, saya kira, dan menulis jawaban "x86 asm for Linux + glibc". Saya pikir masuk akal untuk tidak menghitung ruang entri PLT atau GOT, atau kode perpustakaan itu sendiri.
Peter Cordes
1
Saya akan lebih cenderung memiliki pemanggil lulus char*bufdan menghasilkan string di dalamnya, dengan pemformatan manual. misal seperti ini (canggung dioptimalkan untuk kecepatan) asm FizzBuzz , di mana saya memasukkan data string ke register dan kemudian menyimpannya mov, karena string pendek dan panjang tetap.
Peter Cordes
1

Gunakan gerakan CMOVccdan set kondisionalSETcc

Ini lebih merupakan pengingat bagi saya, tetapi instruksi set bersyarat ada dan instruksi pemindahan bersyarat ada pada prosesor P6 (Pentium Pro) atau yang lebih baru. Ada banyak instruksi yang didasarkan pada satu atau lebih dari flag yang diatur dalam EFLAGS.

qwr
sumber
1
Saya menemukan bercabang biasanya lebih kecil. Ada beberapa kasus di mana itu adalah fit alami, tetapi cmovmemiliki 2-byte opcode ( 0F 4x +ModR/M) sehingga minimal 3 byte. Tetapi sumbernya adalah r / m32, sehingga Anda dapat memuat secara kondisional dalam 3 byte. Selain percabangan, setccberguna dalam lebih banyak kasus daripada cmovcc. Namun, pertimbangkan seluruh rangkaian instruksi, bukan hanya instruksi dasar 386. (Meskipun SSE2 dan BMI / BMI2 instruksi sangat besar sehingga mereka jarang berguna. rorx eax, ecx, 32Adalah 6 byte, lebih lama dari mov + ror. Bagus untuk kinerja, bukan golf kecuali POPCNT atau PDEP menyimpan banyak isn)
Peter Cordes
@PeterCordes terima kasih, saya telah menambahkan setcc.
qwr
1

Menghemat jmpbyte dengan mengatur if / then daripada if / then / else

Ini tentu sangat mendasar, hanya berpikir saya akan memposting ini sebagai sesuatu untuk dipikirkan ketika bermain golf. Sebagai contoh, pertimbangkan kode sederhana berikut untuk mendekode karakter digit heksadesimal:

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

Ini dapat dipersingkat dua byte dengan membiarkan case "then" jatuh ke case "else":

    cmp $'A', %al
    jb .digit
    sub $('A'-'0'-10), %eax
.digit:
    sub $'0', %eax
    movzbl %al, %eax
    ...
Daniel Schepler
sumber
Anda akan sering melakukan ini secara normal ketika mengoptimalkan kinerja, terutama ketika sublatensi ekstra pada jalur kritis untuk satu case bukan bagian dari rantai ketergantungan loop-carry (seperti di sini di mana setiap digit input independen hingga penggabungan potongan 4-bit ). Tapi saya rasa +1 juga. BTW, contoh Anda memiliki pengoptimalan terlewatkan yang terpisah: jika Anda tetap akan membutuhkannya movzxdi akhir, maka gunakan sub $imm, %albukan EAX untuk mengambil keuntungan dari pengkodean 2-byte no-modrm op $imm, %al.
Peter Cordes
Anda juga dapat menghilangkan cmpdengan melakukan sub $'A'-10, %al; jae .was_alpha; add $('A'-10)-'0'. (Saya pikir saya punya logika yang benar). Perhatikan bahwa 'A'-10 > '9'jadi tidak ada ambiguitas. Mengurangi koreksi untuk huruf akan membungkus angka desimal. Jadi ini aman jika kita mengasumsikan input kita adalah hex yang valid, sama seperti input Anda.
Peter Cordes
0

Anda dapat mengambil objek berurutan dari tumpukan dengan mengatur esi ke esp, dan melakukan urutan lodsd / xchg reg, eax.

Peter Ferrie
sumber
Mengapa ini lebih baik dari pop eax/ pop edx/ ...? Jika Anda harus meninggalkannya di stack, Anda dapat pushmengembalikan semuanya setelah memulihkan ESP, masih 2 byte per objek tanpa perlu mov esi,esp. Atau maksud Anda untuk objek 4-byte dalam kode 64-bit di mana popakan mendapatkan 8 byte? BTW, Anda bahkan dapat menggunakan popuntuk mengulang buffer dengan kinerja yang lebih baik daripada lodsd, misalnya untuk penambahan presisi yang tinggi dalam Extreme Fibonacci
Peter Cordes
itu lebih tepat berguna setelah "lea esi, [esp + size of ret address]", yang akan menghalangi penggunaan pop kecuali Anda memiliki register cadangan.
peter ferrie
Oh, untuk args fungsi? Sangat jarang Anda menginginkan lebih banyak args daripada register, atau Anda ingin penelepon meninggalkannya dalam memori alih-alih menyerahkan semuanya dalam register. (Saya memiliki jawaban setengah jadi tentang penggunaan konvensi panggilan kustom, jika salah satu dari konvensi panggilan register standar tidak cocok dengan sempurna.)
Peter Cordes
cdecl bukan fastcall akan meninggalkan parameter di stack, dan mudah untuk memiliki banyak parameter. Lihat github.com/peterferrie/tinycrypt, misalnya.
peter ferrie
0

Untuk codegolf dan ASM: Gunakan instruksi hanya menggunakan register, tekan pop, meminimalkan memori register atau memori segera

RosLuP
sumber
0

Untuk menyalin register 64-bit, gunakan push rcx; pop rdxbukannya 3 byte mov.
Ukuran operan standar untuk push / pop adalah 64-bit tanpa memerlukan awalan REX.

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

(Awalan ukuran operan dapat mengesampingkan ukuran push / pop menjadi 16-bit, tetapi ukuran operan / push 32-bit tidak dapat dikodekan dalam mode 64-bit bahkan dengan REX.W = 0.)

Jika salah satu atau kedua register adalah r8.. r15, gunakan movkarena push dan / atau pop akan memerlukan awalan REX. Kasus terburuk ini sebenarnya hilang jika keduanya membutuhkan awalan REX. Tentunya Anda biasanya harus menghindari r8..r15 pula dalam kode golf.


Anda dapat membuat sumber Anda lebih mudah dibaca saat berkembang dengan makro NASM ini . Ingatlah bahwa ia menginjak 8 byte di bawah RSP. (Di zona merah di Sistem x86-64 V). Tetapi dalam kondisi normal itu adalah pengganti drop-in untuk 64-bit mov r64,r64ataumov 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

Contoh:

   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)

Bagian xchgdari contoh ini adalah karena kadang-kadang Anda perlu mendapatkan nilai ke EAX atau RAX dan tidak peduli tentang mempertahankan salinan lama. push / pop tidak membantu Anda bertukar sebenarnya.

Peter Cordes
sumber