Inline assembly dalam C

GCC extended asm dengan templat, constraint, dan clobber untuk menyisipkan assembly dalam C.

Daftar isi

Sebagian besar kode assembly yang digunakan sehari-hari tidak ditulis sebagai berkas .asm terpisah, melainkan disisipkan di dalam program C melalui fitur extended asm milik GCC. Topik ini membahas bentuk dasar __asm__ dengan output, input, dan clobber, cara constraint mengikat variabel C ke operan assembly, serta syarat kapan inline assembly layak digunakan dan kapan compiler biasa sudah lebih tepat. Ketiga program pada topik ini bersifat mandiri sehingga dapat dirakit dan dijalankan apa adanya.

Bentuk dasar extended asm#

GCC mengenal dua bentuk penyisipan assembly. Bentuk pertama adalah basic asm, yaitu satu string instruksi tanpa daftar operand, misalnya __asm__ ("nop"), dan penggunaannya terbatas pada instruksi yang tidak menyentuh variabel C sama sekali. Bentuk kedua adalah extended asm, yang menyatakan secara eksplisit data apa yang dibaca dan ditulis sehingga compiler dapat mengaitkannya dengan variabel. Bentuk lengkapnya sebagai berikut.

c Kerangka extended asm
// tidak-dijalankan
__asm__ (
    "templat instruksi dengan %[nama]"
    : [nama] "constraint" (variabel keluaran)
    : [nama] "constraint" (variabel masukan)
    : "daftar clobber"
);

Bagian dalam tanda kutip pertama disebut template, yaitu teks instruksi yang akan dirakit, dan setiap %[nama] adalah placeholder yang digantikan oleh operan nyata saat kompilasi. Bagian setelah titik dua pertama adalah daftar keluaran, bagian setelah titik dua kedua adalah daftar masukan, dan bagian terakhir adalah daftar clobber, yaitu daftar sumber daya prosesor yang dirusak oleh instruksi tetapi tidak tercantum sebagai operand. Huruf constraint menentukan tempat operan boleh diletakkan. Yang paling sering digunakan adalah r untuk register, +r untuk register yang dibaca sekaligus ditulis, m untuk lokasi memori, dan i untuk konstanta langsung.

Penjumlahan dengan register constraint#

Program berikut menambahkan dua variabel dengan instruksi addq yang disisipkan, dan seluruh pemindahan nilai masuk dan keluar register ditangani compiler melalui constraint.

c tambah-asm.c
#include <stdio.h>

int main(void) {
    long a = 20;
    long b = 22;
    long hasil;

    __asm__ (
        "addq %[b], %[a]"
        : [a] "+r" (a)
        : [b] "r" (b)
        : "cc"
    );
    hasil = a;

    printf("20 + 22 = %ld\n", hasil);
    return 0;
}
text Keluaran
20 + 22 = 42

Constraint [a] "+r" menyatakan bahwa a dibaca lalu hasilnya ditulis kembali ke a, sedangkan [b] "r" menyatakan bahwa b hanya dibaca. Clobber "cc" wajib ada karena addq mengubah register flag, dan compiler harus diberi tahu agar tidak mengandalkan isi flag dari sebelum instruksi. Pada kompilasi -O0 dengan gcc 14.2.0, compiler memilih rax untuk a dan rdx untuk b, sehingga template yang dirakit menjadi addq %rdx, %rax. Pilihan register itu bukan kontrak, melainkan keputusan compiler saat itu, dan hanya rujukan melalui %[nama] yang aman digunakan karena register yang dipilih compiler dapat berubah kapan saja.

Membaca memori dengan constraint m#

Constraint m menyerahkan pemilihan mode pengalamatan kepada compiler, sehingga template hanya perlu menyebutkan bahwa operannya berada di memori. Program berikut mengambil satu elemen array melalui instruksi movq dengan operan memori.

c baca-memori.c
#include <stdio.h>

int main(void) {
    long harga[] = {10, 20, 30, 40, 50};
    long indeks = 2;
    long nilai;

    __asm__ (
        "movq %[src], %[out]"
        : [out] "=r" (nilai)
        : [src] "m" (harga[indeks])
    );

    printf("harga[%ld] = %ld\n", indeks, nilai);
    return 0;
}
text Keluaran
harga[2] = 30

Operan [src] "m" (harga[indeks]) tidak memaksa compiler menyalin nilai ke register terlebih dahulu, melainkan membiarkan instruksi movq membaca langsung dari tempat variabel itu berada. Pada kompilasi -O0, compiler memilih mode pengalamatan berskala -64(%rbp,%rax,8) untuk menghitung alamat elemen, lengkap dengan perkalian indeks, tanpa satu baris pun perhitungan alamat yang ditulis secara manual. Apabila template ditulis dengan constraint r untuk operan yang sama, compiler akan menambahkan instruksi pemuatan terpisah dan kode yang dihasilkan menjadi lebih panjang. Pemilihan constraint dengan demikian adalah bagian dari pekerjaan optimasi yang dapat dibantu maupun dirusak oleh penulis template.

Kapan inline assembly tidak diperlukan#

Penjumlahan yang sama ditulis kembali dalam bahasa C biasa tanpa satu instruksi tersisip pun.

c tanpa-asm.c
#include <stdio.h>

int main(void) {
    long a = 20;
    long b = 22;
    long hasil = a + b;

    printf("20 + 22 = %ld\n", hasil);
    return 0;
}
text Keluaran
20 + 22 = 42

Keluarannya identik dengan versi extended asm, sedangkan assembly yang dihasilkan compiler untuk ekspresi a + b adalah instruksi addq yang sama. Inline assembly justru menyembunyikan maksud instruksi dari compiler, sehingga peluang optimasi, termasuk pelipatan konstanta dan analisis aliran data, hilang di sekitarnya. Aturan praktisnya berupa urutan langkah, yaitu menulis kode C biasa terlebih dahulu, memeriksa keluaran compiler dengan objdump atau -S, kemudian menimbang intrinsics untuk kebutuhan SIMD, dan menggunakan extended asm hanya untuk instruksi yang memang tidak dapat dinyatakan dengan cara lain, misalnya rdtsc, cpuid, atau pemanggilan syscall dengan kontrak register yang ketat.

Kesalahan umum#

  • Clobber "cc" ditiadakan padahal instruksi mengubah flag. Compiler menyangka flag masih utuh sehingga keputusan jump bersyarat di sekitar asm dapat menjadi salah. Instruksi aritmetika dan logika hampir selalu mengubah flag, sehingga clobber "cc" dicantumkan.
  • Register tertentu diasumsikan selalu terpilih. Kebiasaan berharap %[a] selalu menempati rax membuat template ditulis dengan nama register langsung, dan kode itu berhenti bekerja ketika compiler memilih register lain. Rujukan operan selalu melalui %[nama].
  • Inline assembly dipilih untuk operasi yang sudah didukung C. Penjumlahan, pergeseran, dan operasi logika ditangani compiler dengan hasil yang sama atau lebih baik, sedangkan asm tersisip mematikan optimasi di sekitarnya. Extended asm dipertahankan untuk instruksi yang tidak terjangkau C maupun intrinsics.
  • Clobber "memory" ditiadakan bila asm menulis ke memori melalui pointer. Compiler tidak mengetahui bahwa memori berubah sehingga dapat menyimpan cache nilai lama di register. Setiap penulisan memori yang tidak tercantum sebagai operand keluaran wajib disertai clobber "memory".

Ringkasan#

  • Extended asm terdiri atas template berisi %[nama], daftar keluaran, daftar masukan, dan daftar clobber, sedangkan basic asm hanya berisi string instruksi tanpa operand.
  • Constraint "+r" untuk operan baca-tulis, "r" untuk register, "m" untuk memori yang mode pengalamatannya dipilih compiler, dan "i" untuk konstanta.
  • Clobber "cc" dan "memory" wajib dilaporkan bila instruksi mengubah flag atau memori di luar operand terdaftar, karena compiler tidak memahami semantik instruksi yang disisipkan.
  • Inline assembly adalah pilihan terakhir, setelah kode C biasa dan intrinsics terbukti tidak memenuhi kebutuhan.

Referensi#