Anda merusak memori tetapi tidak memberi tahu GCC tentang hal itu, sehingga GCC dapat menyimpan nilai dalam cache di buf
di seluruh panggilan perakitan. Jika Anda ingin menggunakan masukan dan keluaran, beri tahu GCC tentang semuanya.
__asm__ (
"movq %1, 0(%0)\n\t"
"movq %2, 8(%0)"
: /* Outputs (none) */
: "r"(buf), "r"(rrax), "r"(rrbx) /* Inputs */
: "memory"); /* Clobbered */
Biasanya Anda juga ingin membiarkan GCC menangani sebagian besar mov
, pemilihan register, dll -- bahkan jika Anda secara eksplisit membatasi register (rrax masih %rax
) biarkan informasi mengalir melalui GCC atau Anda akan mendapatkan hasil yang tidak diharapkan.
__volatile__
salah.
Alasannya __volatile__
ada sehingga Anda dapat menjamin bahwa kompiler menempatkan kode Anda tepat di tempatnya... yang merupakan sama sekali tidak perlu jaminan untuk kode ini. Hal ini diperlukan untuk mengimplementasikan fitur lanjutan seperti penghalang memori, tetapi hampir tidak berguna sama sekali jika Anda hanya memodifikasi memori dan register.
GCC sudah mengetahui bahwa rakitan ini tidak dapat dipindahkan setelah printf
karena printf
panggilan mengakses buf
, dan buf
bisa dikalahkan oleh majelis. GCC sudah mengetahui bahwa ia tidak dapat memindahkan rakitan sebelum rrax=0x39;
karena rax
adalah masukan untuk kode perakitan. Jadi apa artinya __volatile__
menangkapmu? Tidak ada.
Jika kode Anda tidak berfungsi tanpa __volatile__
maka ada kesalahan pada kode yang harus diperbaiki daripada hanya menambahkan __volatile__
dan berharap itu membuat segalanya lebih baik. __volatile__
kata kunci bukanlah sihir dan tidak boleh diperlakukan seperti itu.
Perbaikan alternatif:
Apakah __volatile__
diperlukan untuk kode asli Anda? Tidak. Cukup tandai input dan nilai clobber dengan benar.
/* The "S" constraint means %rsi, "b" means %rbx, and "a" means %rax
The inputs and clobbered values are specified. There is no output
so that section is blank. */
rsi = (long) buf;
__asm__ ("movq %%rax, 0(%%rsi)" : : "a"(rrax), "S"(rssi) : "memory");
__asm__ ("movq %%rbx, 0(%%rsi)" : : "b"(rrbx), "S"(rrsi) : "memory");
Mengapa __volatile__
tidak membantu Anda di sini:
rrax = 0x34; /* Dead code */
GCC berhak sepenuhnya menghapus baris di atas, karena kode dalam pertanyaan di atas mengklaim bahwa ia tidak pernah menggunakan rrax
.
Contoh yang lebih jelas
long global;
void store_5(void)
{
register long rax asm ("rax");
rax = 5;
__asm__ __volatile__ ("movq %%rax, (global)");
}
Pembongkaran kurang lebih seperti yang Anda harapkan di -O0
,
movl $5, %rax
movq %rax, (global)
Tetapi dengan pengoptimalan nonaktif, Anda bisa sangat ceroboh tentang perakitan. Ayo coba -O2
:
movq %rax, (global)
Ups! Di mana rax = 5;
Pergilah? Ini kode mati, sejak %rax
tidak pernah digunakan dalam fungsi — setidaknya sejauh yang diketahui GCC. GCC tidak mengintip ke dalam perakitan. Apa yang terjadi jika kami menghapus __volatile__
?
; empty
Nah, Anda mungkin berpikir __volatile__
apakah Anda melakukan layanan dengan mencegah GCC membuang rakitan Anda yang berharga, tetapi itu hanya menutupi fakta bahwa menurut GCC rakitan Anda tidak melakukan apa pun. GCC menganggap rakitan Anda tidak menerima input, tidak menghasilkan output, dan tidak merusak memori. Anda sebaiknya meluruskannya:
long global;
void store_5(void)
{
register long rax asm ("rax");
rax = 5;
__asm__ __volatile__ ("movq %%rax, (global)" : : : "memory");
}
Sekarang kita mendapatkan output berikut:
movq %rax, (global)
Lebih baik. Tetapi jika Anda memberi tahu GCC tentang inputnya, itu akan memastikan bahwa %rax
diinisialisasi dengan benar terlebih dahulu:
long global;
void store_5(void)
{
register long rax asm ("rax");
rax = 5;
__asm__ ("movq %%rax, (global)" : : "a"(rax) : "memory");
}
Keluarannya, dengan pengoptimalan:
movl $5, %eax
movq %rax, (global)
Benar! Dan kita bahkan tidak perlu menggunakan __volatile__
.
Mengapa __volatile__
ada?
Penggunaan utama yang benar untuk __volatile__
adalah jika kode rakitan Anda melakukan hal lain selain input, output, atau memori yang merusak. Mungkin itu mengacaukan register khusus yang tidak diketahui GCC, atau memengaruhi IO. Anda sering melihatnya di kernel Linux, tetapi sering disalahgunakan di ruang pengguna.
__volatile__
kata kunci ini sangat menggoda karena kami pemrogram C sering berpikir bahwa kami hampir pemrograman dalam bahasa assembly sudah. Tidak. Kompiler C melakukan banyak analisis aliran data — jadi Anda perlu menjelaskan aliran data ke kompiler untuk kode rakitan Anda. Dengan begitu, kompiler dapat dengan aman memanipulasi potongan rakitan Anda seperti halnya memanipulasi rakitan yang dihasilkannya.
Jika Anda menggunakan __volatile__
banyak, sebagai alternatif Anda dapat menulis seluruh fungsi atau modul dalam file rakitan.
Kompiler menggunakan register, dan mungkin menulis nilai yang telah Anda masukkan ke dalamnya.
Dalam hal ini, kompiler mungkin menggunakan rbx
daftar setelah rrbx
penugasan dan sebelum bagian rakitan inline.
Secara umum, Anda seharusnya tidak mengharapkan register mempertahankan nilainya setelah dan di antara urutan kode rakitan inline.
Sedikit di luar topik, tetapi saya ingin menindaklanjuti sedikit perakitan inline gcc.
Kebutuhan (non-) untuk __volatile__
berasal dari fakta bahwa GCC mengoptimalkan perakitan inline. GCC memeriksa pernyataan rakitan untuk efek samping / prasyarat, dan jika ternyata tidak ada, GCC dapat memilih untuk memindahkan instruksi rakitan atau bahkan memutuskan untuk menghapus dia. Semua __volatile__
lakukan adalah memberi tahu kompiler "berhenti peduli dan letakkan ini di sana".
Yang biasanya bukan yang Anda inginkan.
Disinilah perlunya batasan masuk. Nama kelebihan beban dan benar-benar digunakan untuk hal-hal yang berbeda di rakitan inline GCC:
- batasan menentukan operan input/output yang digunakan dalam
asm()
blokir - batasan menentukan "daftar clobber", yang merinci "status" apa (register, kode kondisi, memori) yang dipengaruhi oleh
asm()
. - batasan menentukan kelas operan (register, alamat, offset, konstanta, ...)
- batasan mendeklarasikan asosiasi/pengikatan antara entitas assembler dan variabel/ekspresi C/C++
Dalam banyak kasus, pengembang menyalahgunakan __volatile__
karena mereka melihat kode mereka dipindahkan atau bahkan menghilang tanpa itu. Jika ini terjadi, biasanya ini merupakan tanda bahwa pengembang telah mencoba tidak untuk memberi tahu GCC tentang efek samping / prasyarat perakitan. Misalnya, kode buggy ini:
register int foo __asm__("rax") = 1234;
register int bar __adm__("rbx") = 4321;
asm("add %rax, %rbx");
printf("I'm expecting 'bar' to be 5555 it is: %d\n", bar);
Ada beberapa bug:
- untuk satu, itu hanya dikompilasi karena bug gcc (!). Biasanya, untuk menulis nama register di rakitan inline, gandakan
%%
diperlukan, tetapi di atas jika Anda benar-benar menentukannya, Anda mendapatkan kesalahan kompiler/assembler,/tmp/ccYPmr3g.s:22: Error: bad register name '%%rax'
. - kedua, itu tidak memberi tahu kompiler kapan dan di mana Anda membutuhkan/menggunakan variabel. Sebaliknya, itu mengasumsikan compiler menghargai
asm()
secara harfiah. Itu mungkin benar untuk Microsoft Visual C++ tetapi tidak demikian untuk gcc.
Jika Anda mengompilasinya tanpa optimasi, itu menciptakan:
0000000000400524 <main>: [ ... ] 400534: b8 d2 04 00 00 mov $0x4d2,%eax 400539: bb e1 10 00 00 mov $0x10e1,%ebx 40053e: 48 01 c3 add %rax,%rbx 400541: 48 89 da mov %rbx,%rdx 400544: b8 5c 06 40 00 mov $0x40065c,%eax 400549: 48 89 d6 mov %rdx,%rsi 40054c: 48 89 c7 mov %rax,%rdi 40054f: b8 00 00 00 00 mov $0x0,%eax 400554: e8 d7 fe ff ff callq 400430 <[email protected]> [...]Anda dapat menemukan
add
Anda instruksi, dan inisialisasi dari dua register, dan itu akan mencetak yang diharapkan. Sebaliknya, jika Anda meningkatkan pengoptimalan, hal lain akan terjadi:0000000000400530 <main>: 400530: 48 83 ec 08 sub $0x8,%rsp 400534: 48 01 c3 add %rax,%rbx 400537: be e1 10 00 00 mov $0x10e1,%esi 40053c: bf 3c 06 40 00 mov $0x40063c,%edi 400541: 31 c0 xor %eax,%eax 400543: e8 e8 fe ff ff callq 400430 <[email protected]> [ ... ]Inisialisasi kedua register "bekas" Anda sudah tidak ada lagi. Kompiler membuangnya karena tidak ada yang terlihat menggunakannya, dan sementara itu menyimpan instruksi perakitan, ia meletakkannya sebelum penggunaan kedua variabel tersebut. Itu ada tetapi tidak melakukan apa-apa (Untungnya sebenarnya ... jika
rax
/ rbx
telah digunakan siapa yang tahu apa yang telah terjadi ...).
Dan alasannya adalah karena Anda belum benar-benar memberi tahu GCC bahwa rakitan menggunakan register ini / nilai operan ini. Ini tidak ada hubungannya sama sekali dengan volatile
tetapi semuanya dengan fakta bahwa Anda menggunakan asm()
yang bebas kendala ekspresi.
Cara melakukannya dengan benar adalah melalui kendala, yaitu Anda akan menggunakan:
int foo = 1234;
int bar = 4321;
asm("add %1, %0" : "+r"(bar) : "r"(foo));
printf("I'm expecting 'bar' to be 5555 it is: %d\n", bar);
Ini memberi tahu kompiler bahwa rakitan:
- memiliki satu argumen dalam register,
"+r"(...)
bahwa keduanya perlu diinisialisasi sebelum pernyataan rakitan, dan dimodifikasi oleh pernyataan rakitan, dan mengaitkan variabelbar
dengan itu. - memiliki argumen kedua dalam register,
"r"(...)
yang perlu diinisialisasi sebelum pernyataan rakitan dan diperlakukan sebagai hanya baca/tidak dimodifikasi oleh pernyataan tersebut. Di sini, kaitkanfoo
dengan itu.
Perhatikan tidak ada tugas register yang ditentukan - kompiler memilihnya tergantung pada variabel/status kompilasi. Output (dioptimalkan) di atas:
0000000000400530 <main>: 400530: 48 83 ec 08 sub $0x8,%rsp 400534: b8 d2 04 00 00 mov $0x4d2,%eax 400539: be e1 10 00 00 mov $0x10e1,%esi 40053e: bf 4c 06 40 00 mov $0x40064c,%edi 400543: 01 c6 add %eax,%esi 400545: 31 c0 xor %eax,%eax 400547: e8 e4 fe ff ff callq 400430 <[email protected]> [ ... ]Kendala rakitan inline GCC hampir selalu diperlukan dalam beberapa bentuk atau yang lain, tetapi ada banyak cara yang mungkin untuk menggambarkan persyaratan yang sama ke kompiler; selain yang di atas, Anda juga bisa menulis:
asm("add %1, %0" : "=r"(bar) : "r"(foo), "0"(bar));
Ini memberi tahu gcc:
- pernyataan memiliki operan keluaran, variabel
bar
, bahwa setelah pernyataan itu akan ditemukan dalam register,"=r"(...)
- pernyataan memiliki operan input, variabel
foo
, yang akan ditempatkan ke dalam register,"r"(...)
- operan nol juga merupakan operan masukan dan diinisialisasi dengan
bar
Atau, sekali lagi alternatif:
asm("add %1, %0" : "+r"(bar) : "g"(foo));
yang memberitahu gcc:
- bla (menguap - sama seperti sebelumnya,
bar
baik masukan/keluaran) - pernyataan memiliki operan input, variabel
foo
, yang pernyataannya tidak peduli apakah itu dalam register, dalam memori atau konstanta waktu kompilasi (itulah"g"(...)
kendala)
Hasilnya berbeda dari yang pertama:
0000000000400530 <main>: 400530: 48 83 ec 08 sub $0x8,%rsp 400534: bf 4c 06 40 00 mov $0x40064c,%edi 400539: 31 c0 xor %eax,%eax 40053b: be e1 10 00 00 mov $0x10e1,%esi 400540: 81 c6 d2 04 00 00 add $0x4d2,%esi 400546: e8 e5 fe ff ff callq 400430 <[email protected]> [ ... ]karena sekarang, GCC telah menemukan jawabannya
foo
adalah konstanta waktu kompilasi dan hanya menyematkan nilai di add
petunjuk ! Bukankah itu rapi?
Memang, ini rumit dan perlu dibiasakan. Keuntungannya adalah membiarkan kompiler memilih register mana yang digunakan untuk operan apa yang memungkinkan pengoptimalan kode secara keseluruhan; jika, misalnya, pernyataan rakitan inline digunakan dalam makro dan/atau static inline
fungsi, kompiler dapat, tergantung pada konteks pemanggilan, memilih register yang berbeda pada contoh kode yang berbeda. Atau jika nilai tertentu waktu kompilasi dapat dievaluasi/konstan di satu tempat tetapi tidak di tempat lain, kompiler dapat menyesuaikan rakitan yang dibuat untuknya.
Pikirkan kendala perakitan inline GCC sebagai semacam "prototipe fungsi yang diperluas" - mereka memberi tahu kompiler jenis dan lokasi apa untuk argumen / nilai pengembalian, ditambah sedikit lagi. Jika Anda tidak menentukan batasan ini, rakitan inline Anda membuat fungsi analog yang hanya beroperasi pada variabel/status global - yang, seperti yang mungkin kita semua setujui, jarang melakukan apa yang Anda inginkan.