Bagaimana tepatnya __attribute __ ((constructor)) bekerja?

347

Tampaknya cukup jelas bahwa itu seharusnya mengatur segalanya.

  1. Kapan tepatnya itu berjalan?
  2. Mengapa ada dua tanda kurung?
  3. Apakah __attribute__suatu fungsi? Makro? Sintaksis?
  4. Apakah ini berfungsi di C? C ++?
  5. Apakah fungsinya berfungsi dengan harus statis?
  6. Kapan __attribute__((destructor))lari?

Contoh di Objective-C :

__attribute__((constructor))
static void initialize_navigationBarImages() {
  navigationBarImages = [[NSMutableDictionary alloc] init];
}

__attribute__((destructor))
static void destroy_navigationBarImages() {
  [navigationBarImages release];
}
Casebash
sumber

Jawaban:

273
  1. Ini berjalan ketika pustaka bersama dimuat, biasanya selama startup program.
  2. Begitulah semua atribut GCC; mungkin untuk membedakannya dari panggilan fungsi.
  3. Sintaks khusus GCC.
  4. Ya, ini berfungsi dalam C dan C ++.
  5. Tidak, fungsinya tidak harus statis.
  6. Destuktor berjalan ketika pustaka bersama diturunkan, biasanya di program keluar.

Jadi, cara kerja konstruktor dan destruktor adalah bahwa file objek bersama berisi bagian khusus (.tor dan .dtor pada ELF) yang masing-masing berisi referensi ke fungsi yang ditandai dengan atribut konstruktor dan destruktor. Ketika perpustakaan dimuat / dibongkar program pemuat dinamis (ld.so atau semacamnya) memeriksa apakah ada bagian tersebut, dan jika demikian, panggil fungsi yang direferensikan di dalamnya.

Kalau dipikir-pikir, mungkin ada beberapa sihir yang serupa di penghubung statis normal sehingga kode yang sama dijalankan pada startup / shutdown terlepas apakah pengguna memilih penghubung statis atau dinamis.

janneb
sumber
49
Kurung ganda memudahkannya untuk "makro keluar" ( #define __attribute__(x)). Jika Anda memiliki beberapa atribut, misalnya, __attribute__((noreturn, weak))akan sulit untuk "makro keluar" jika hanya ada satu set tanda kurung.
Chris Jester-Young
7
Itu tidak dilakukan dengan .init/.fini. (Anda dapat secara sah memiliki banyak konstruktor dan destruktor dalam satu unit terjemahan, tidak pernah kelipatan dalam satu perpustakaan - bagaimana cara kerjanya?) Sebaliknya, pada platform menggunakan format biner ELF (Linux, dll.), Konstruktor dan destruktor direferensikan di bagian .ctorsdan .dtorsheader. Benar, di masa lalu, fungsi yang dinamai initdan finiakan dijalankan pada pustaka dinamis dan bongkar jika ada, tapi itu sudah usang sekarang, digantikan oleh mekanisme yang lebih baik ini.
ephemient
7
@ jcayzac Tidak, karena makro variadic adalah ekstensi gcc, dan alasan utama untuk makro keluar __attribute__adalah jika Anda tidak menggunakan gcc, karena itu juga, adalah ekstensi gcc.
Chris Jester-Young
9
@ ChrisJester-Young macro variadic adalah fitur C99 standar, bukan ekstensi GNU.
jcayzac
4
"Penggunaan present tense Anda (" make "bukan" made "- double parens masih membuatnya mudah untuk di-makro-kan. Anda menggonggong pohon pedantic yang salah.
Jim Balter
64

.initSaya .finitidak ditinggalkan. Itu masih bagian dari standar ELF dan saya berani mengatakan itu akan selamanya. Kode di .init/ .finidijalankan oleh loader / runtime-linker ketika kode dimuat / dibongkar. Yakni pada setiap beban ELF (misalnya pustaka bersama) di .initakan dijalankan. Masih mungkin untuk menggunakan mekanisme itu untuk mencapai hal yang sama dengan __attribute__((constructor))/((destructor)). Ini sekolah tua tetapi memiliki beberapa manfaat.

.ctors/ .dtorsmekanisme misalnya memerlukan dukungan oleh skrip sistem-rtl / loader / linker. Ini jauh dari pasti untuk tersedia di semua sistem, misalnya sistem tertanam di mana kode dieksekusi pada bare metal. Yaitu bahkan jika __attribute__((constructor))/((destructor))didukung oleh GCC, tidak pasti itu akan berjalan karena terserah linker untuk mengaturnya dan ke loader (atau dalam beberapa kasus, kode boot) untuk menjalankannya. Untuk menggunakan .init/ .finisebagai gantinya, cara termudah adalah dengan menggunakan tanda tautan: -init & -fini (yaitu dari baris perintah GCC, sintaksnya adalah -Wl -init my_init -fini my_fini).

Pada sistem yang mendukung kedua metode, salah satu manfaat yang mungkin adalah bahwa kode masuk .initdijalankan sebelum .ctorsdan kode masuk .finisetelahnya .dtors. Jika pesanan relevan, itu setidaknya satu cara kasar tetapi mudah untuk membedakan antara fungsi init / exit.

Kelemahan utama adalah bahwa Anda tidak dapat dengan mudah memiliki lebih dari satu _initdan satu _finifungsi per setiap modul yang dapat dimuat dan mungkin harus memecah kode menjadi lebih .sodari termotivasi. Lain adalah bahwa ketika menggunakan metode linker yang dijelaskan di atas, satu menggantikan fungsi _init asli dan _finidefault (disediakan oleh crti.o). Di sinilah segala macam inisialisasi biasanya terjadi (di Linux ini adalah di mana tugas variabel global diinisialisasi). Cara yang dijelaskan di sini

Perhatikan di tautan di atas bahwa cascading ke aslinya _init()tidak diperlukan karena masih ada. Namun calldalam perakitan inline adalah x86-mnemonic dan memanggil fungsi dari perakitan akan terlihat sangat berbeda untuk banyak arsitektur lainnya (seperti ARM misalnya). Yaitu kode tidak transparan.

.init/ .finidan .ctors/ .detorsmekanisme serupa, tetapi tidak cukup. Kode dalam .init/ .finiberjalan "apa adanya". Yaitu Anda dapat memiliki beberapa fungsi di .init/ .fini, tetapi secara sintaksis AFAIK sulit untuk menempatkannya di sana sepenuhnya secara transparan dalam C murni tanpa memecah kode dalam banyak .sofile kecil .

.ctorsSaya .dtorsterorganisir secara berbeda dari .init/ .fini. .ctors/ .dtorsBagian keduanya hanya tabel dengan pointer ke fungsi, dan "pemanggil" adalah loop yang disediakan sistem yang memanggil setiap fungsi secara tidak langsung. Yaitu loop-caller bisa arsitektur spesifik, tetapi karena itu bagian dari sistem (jika ada sama sekali yaitu) tidak masalah.

Cuplikan berikut menambahkan pointer fungsi baru ke .ctorsarray fungsi, terutama dengan cara yang sama __attribute__((constructor))(metode dapat hidup berdampingan dengan __attribute__((constructor))).

#define SECTION( S ) __attribute__ ((section ( S )))
void test(void) {
   printf("Hello\n");
}
void (*funcptr)(void) SECTION(".ctors") =test;
void (*funcptr2)(void) SECTION(".ctors") =test;
void (*funcptr3)(void) SECTION(".dtors") =test;

Satu juga dapat menambahkan fungsi pointer ke bagian yang diciptakan sendiri sepenuhnya berbeda. Skrip linker yang dimodifikasi dan fungsi tambahan yang meniru loader .ctors/ .dtorsloop diperlukan dalam kasus tersebut. Tetapi dengan itu orang dapat mencapai kontrol yang lebih baik atas urutan eksekusi, menambahkan argumen dan mengembalikan eta penanganan kode (Dalam proyek C ++ misalnya, akan berguna jika membutuhkan sesuatu yang berjalan sebelum atau setelah konstruktor global).

Saya lebih suka __attribute__((constructor))/((destructor))jika memungkinkan, ini adalah solusi sederhana dan elegan bahkan rasanya seperti curang. Untuk coders bare-metal seperti saya, ini tidak selalu menjadi pilihan.

Beberapa referensi bagus di buku Linker & loader .

Michael Ambrus
sumber
bagaimana loader memanggil fungsi-fungsi itu? fungsi-fungsi tersebut dapat menggunakan fungsi global dan lainnya dalam ruang alamat proses, tetapi loader adalah suatu proses dengan ruang alamatnya sendiri, bukan?
user2162550
@ user2162550 Tidak, ld-linux.so.2 ("penerjemah" yang biasa, pemuat untuk pustaka dinamis yang berjalan di semua executable yang terhubung secara dinamis) berjalan di ruang alamat dari executable itu sendiri. Secara umum loader perpustakaan dinamis itu sendiri adalah sesuatu yang spesifik untuk ruang pengguna, berjalan dalam konteks utas yang mencoba mengakses sumber daya perpustakaan.
Paul Stelian
Ketika saya memanggil execv () dari kode yang memiliki __attribute__((constructor))/((destructor))destruktor tidak berjalan. Saya mencoba beberapa hal seperti menambahkan entri ke .dtor seperti yang ditunjukkan di atas. Tetapi tidak berhasil. Masalahnya mudah diduplikasi dengan menjalankan kode dengan numactl. Misalnya, anggap test_code berisi destructor (tambahkan printf ke fungsi konstruktor dan desctructor untuk men-debug masalah). Kemudian jalankan LD_PRELOAD=./test_code numactl -N 0 sleep 1. Anda akan melihat bahwa konstruktor dipanggil dua kali tetapi destruktor hanya sekali.
B Abali
39

Halaman ini memberikan pemahaman yang besar tentang constructordan destructorpelaksanaan atribut dan bagian dalam dalam ELF yang memungkinkan mereka untuk bekerja. Setelah mencerna informasi yang disediakan di sini, saya mengumpulkan sedikit informasi tambahan dan (meminjam contoh bagian dari Michael Ambrus di atas) membuat contoh untuk menggambarkan konsep dan membantu pembelajaran saya. Hasil-hasil tersebut disediakan di bawah ini bersama dengan contoh sumber.

Seperti dijelaskan di utas ini, atribut constructordan destructormembuat entri di bagian .ctorsdan .dtorsdari file objek. Anda dapat menempatkan referensi ke fungsi di salah satu bagian dalam salah satu dari tiga cara. (1) menggunakan sectionatribut; (2) constructordan destructoratribut atau (3) dengan panggilan inline-assembly (seperti yang dirujuk tautan dalam jawaban Ambrus).

Penggunaan constructordan destructoratribut memungkinkan Anda untuk menetapkan prioritas pada konstruktor / destruktor untuk mengontrol urutan pelaksanaannya sebelum main()dipanggil atau setelah kembali. Semakin rendah nilai prioritas yang diberikan, semakin tinggi prioritas eksekusi (prioritas yang lebih rendah dieksekusi sebelum prioritas yang lebih tinggi sebelum main () - dan setelah prioritas yang lebih tinggi setelah main ()). Nilai prioritas yang Anda berikan harus lebih besar daripada100 sebagai kompiler cadangan nilai prioritas antara 0-100 untuk implementasi. A constructoratau destructorditentukan dengan prioritas dijalankan sebelum constructoratau destructorditentukan tanpa prioritas.

Dengan atribut 'bagian' atau dengan perakitan inline, Anda juga dapat menempatkan referensi fungsi di bagian .initdan .finikode ELF yang akan dieksekusi sebelum konstruktor dan setelah destruktor apa pun, masing-masing. Setiap fungsi yang dipanggil oleh referensi fungsi yang ditempatkan di .initbagian, akan dieksekusi sebelum referensi fungsi itu sendiri (seperti biasa).

Saya telah mencoba menggambarkan masing-masing contoh di bawah ini:

#include <stdio.h>
#include <stdlib.h>

/*  test function utilizing attribute 'section' ".ctors"/".dtors"
    to create constuctors/destructors without assigned priority.
    (provided by Michael Ambrus in earlier answer)
*/

#define SECTION( S ) __attribute__ ((section ( S )))

void test (void) {
printf("\n\ttest() utilizing -- (.section .ctors/.dtors) w/o priority\n");
}

void (*funcptr1)(void) SECTION(".ctors") =test;
void (*funcptr2)(void) SECTION(".ctors") =test;
void (*funcptr3)(void) SECTION(".dtors") =test;

/*  functions constructX, destructX use attributes 'constructor' and
    'destructor' to create prioritized entries in the .ctors, .dtors
    ELF sections, respectively.

    NOTE: priorities 0-100 are reserved
*/
void construct1 () __attribute__ ((constructor (101)));
void construct2 () __attribute__ ((constructor (102)));
void destruct1 () __attribute__ ((destructor (101)));
void destruct2 () __attribute__ ((destructor (102)));

/*  init_some_function() - called by elf_init()
*/
int init_some_function () {
    printf ("\n  init_some_function() called by elf_init()\n");
    return 1;
}

/*  elf_init uses inline-assembly to place itself in the ELF .init section.
*/
int elf_init (void)
{
    __asm__ (".section .init \n call elf_init \n .section .text\n");

    if(!init_some_function ())
    {
        exit (1);
    }

    printf ("\n    elf_init() -- (.section .init)\n");

    return 1;
}

/*
    function definitions for constructX and destructX
*/
void construct1 () {
    printf ("\n      construct1() constructor -- (.section .ctors) priority 101\n");
}

void construct2 () {
    printf ("\n      construct2() constructor -- (.section .ctors) priority 102\n");
}

void destruct1 () {
    printf ("\n      destruct1() destructor -- (.section .dtors) priority 101\n\n");
}

void destruct2 () {
    printf ("\n      destruct2() destructor -- (.section .dtors) priority 102\n");
}

/* main makes no function call to any of the functions declared above
*/
int
main (int argc, char *argv[]) {

    printf ("\n\t  [ main body of program ]\n");

    return 0;
}

keluaran:

init_some_function() called by elf_init()

    elf_init() -- (.section .init)

    construct1() constructor -- (.section .ctors) priority 101

    construct2() constructor -- (.section .ctors) priority 102

        test() utilizing -- (.section .ctors/.dtors) w/o priority

        test() utilizing -- (.section .ctors/.dtors) w/o priority

        [ main body of program ]

        test() utilizing -- (.section .ctors/.dtors) w/o priority

    destruct2() destructor -- (.section .dtors) priority 102

    destruct1() destructor -- (.section .dtors) priority 101

Contoh ini membantu memperkuat perilaku konstruktor / penghancur, semoga akan bermanfaat bagi orang lain juga.

David C. Rankin
sumber
Di mana Anda menemukan bahwa "nilai prioritas yang Anda berikan harus lebih besar dari 100"? Informasi itu tidak ada pada dokumentasi atribut fungsi GCC.
Justin
4
IIRC, ada beberapa referensi, PATCH: Mendukung argumen prioritas untuk argumen konstruktor / destruktor ( MAX_RESERVED_INIT_PRIORITY), dan mereka sama dengan C ++ ( init_priority) 7,7 C ++ - Atribut Variabel Khusus, Fungsi, dan Jenis Atribut . Kemudian saya mencoba dengan 99: warning: constructor priorities from 0 to 100 are reserved for the implementation [enabled by default] void construct0 () __attribute__ ((constructor (99)));.
David C. Rankin
1
Ah. Saya mencoba prioritas <100 dengan dentang dan sepertinya berhasil, tetapi test case sederhana saya (unit kompilasi tunggal) terlalu sederhana .
Justin
1
Apa yang menjadi prioritas dari variabel global statis (statis ctors)?
dashesy
2
Efek dan visibilitas global statis akan tergantung pada bagaimana program Anda disusun (misalnya file tunggal, banyak file ( unit terjemahan )) dan di mana global dinyatakan Lihat: Statis (kata kunci) , khususnya deskripsi variabel global statis .
David C. Rankin
7

Berikut ini adalah contoh "konkret" (dan mungkin berguna ) tentang bagaimana, mengapa, dan kapan menggunakan konstruksi yang berguna, namun tidak sedap dipandang ini ...

Xcode menggunakan "global" "default pengguna" untuk memutuskan XCTestObserverkelas mana yang memuntahkannya ke konsol yang terkepung .

Dalam contoh ini ... ketika saya secara implisit memuat pustaka psuedo ini, sebut saja ... libdemure.a, melalui bendera di target pengujian saya á la ..

OTHER_LDFLAGS = -ldemure

Aku ingin..

  1. Saat memuat (mis. Ketika XCTestmemuat bundel pengujian saya), timpa kelas "default" XCTest"observer" ... (melalui constructorfungsi) PS: Sejauh yang saya tahu .. apa pun yang dilakukan di sini dapat dilakukan dengan efek setara di dalam + (void) load { ... }metode kelas ' .

  2. jalankan tes saya .... dalam kasus ini, dengan verbositas yang lebih tidak masuk akal dalam log (implementasi atas permintaan)

  3. Kembalikan kelas "global" XCTestObserverke keadaan semula itu .. agar tidak mengganggu jalan lain XCTestyang belum masuk dalam kereta musik (alias. Ditautkan ke libdemure.a). Saya kira ini secara historis dilakukan dalam dealloc.. tapi saya tidak akan mulai mengacaukan wanita tua itu.

Begitu...

#define USER_DEFS NSUserDefaults.standardUserDefaults

@interface      DemureTestObserver : XCTestObserver @end
@implementation DemureTestObserver

__attribute__((constructor)) static void hijack_observer() {

/*! here I totally hijack the default logging, but you CAN
    use multiple observers, just CSV them, 
    i.e. "@"DemureTestObserverm,XCTestLog"
*/
  [USER_DEFS setObject:@"DemureTestObserver" 
                forKey:@"XCTestObserverClass"];
  [USER_DEFS synchronize];
}

__attribute__((destructor)) static void reset_observer()  {

  // Clean up, and it's as if we had never been here.
  [USER_DEFS setObject:@"XCTestLog" 
                forKey:@"XCTestObserverClass"];
  [USER_DEFS synchronize];
}

...
@end

Tanpa bendera penghubung ... (Polisi mode mengeroyok Cupertino menuntut pembalasan , namun standar Apple menang, seperti yang diinginkan, di sini )

masukkan deskripsi gambar di sini

DENGAN -ldemure.abendera penghubung ... (Hasil yang bisa dipahami , terkesiap ... "terima kasih constructor/ destructor" ... Sorak sorai ) masukkan deskripsi gambar di sini

Alex Gray
sumber
1

Ini adalah contoh konkret lainnya. Ini untuk perpustakaan bersama. Fungsi utama perpustakaan bersama adalah untuk berkomunikasi dengan pembaca kartu pintar. Tetapi ia juga dapat menerima 'informasi konfigurasi' saat runtime melalui udp. Udp ditangani oleh utas yang HARUS dimulai pada waktu init.

__attribute__((constructor))  static void startUdpReceiveThread (void) {
    pthread_create( &tid_udpthread, NULL, __feigh_udp_receive_loop, NULL );
    return;

  }

Perpustakaan ditulis dalam c.

drlolly
sumber
1
Pilihan aneh jika pustaka ditulis dalam C ++, karena konstruktor variabel global biasa adalah cara idiomatis untuk menjalankan kode pra-utama dalam C ++.
Nicholas Wilson
@NicholasWilson Perpustakaan sebenarnya ditulis dalam c. Tidak tahu bagaimana saya mengetik c ++ bukannya c.
drlolly