GNU/Linux >> Belajar Linux >  >> Linux

Mengapa rakitan inline ini tidak berfungsi dengan pernyataan volatil asm terpisah untuk setiap instruksi?

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:

  1. memiliki satu argumen dalam register, "+r"(...) bahwa keduanya perlu diinisialisasi sebelum pernyataan rakitan, dan dimodifikasi oleh pernyataan rakitan, dan mengaitkan variabel bar dengan itu.
  2. memiliki argumen kedua dalam register, "r"(...) yang perlu diinisialisasi sebelum pernyataan rakitan dan diperlakukan sebagai hanya baca/tidak dimodifikasi oleh pernyataan tersebut. Di sini, kaitkan foo 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:

  1. pernyataan memiliki operan keluaran, variabel bar , bahwa setelah pernyataan itu akan ditemukan dalam register, "=r"(...)
  2. pernyataan memiliki operan input, variabel foo , yang akan ditempatkan ke dalam register, "r"(...)
  3. 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:

  1. bla (menguap - sama seperti sebelumnya, bar baik masukan/keluaran)
  2. 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.


Linux
  1. Mengapa "ls" Membutuhkan Proses Terpisah Untuk Dieksekusi?

  2. PYTHONPATH tidak berfungsi untuk sudo di GNU/Linux (berfungsi untuk root)

  3. alias nama konfigurasi ssh tidak berfungsi untuk scp

  1. Mengapa kode ini mogok dengan pengacakan alamat aktif?

  2. Mengapa 'dd' tidak berfungsi untuk membuat USB yang dapat di-boot?

  3. mengapa sftp rmdir tidak berfungsi?

  1. Mengapa Substitusi Proses Bash Tidak Bekerja Dengan Beberapa Perintah?

  2. Mengapa Bash Tidak Menyimpan Perintah yang Dimulai Dengan Spasi?

  3. Mengapa crontab saya tidak berfungsi, dan bagaimana cara mengatasinya?