Chính xác thì __attribution __ ((constructor)) hoạt động như thế nào?


347

Có vẻ như khá rõ ràng rằng nó được cho là để thiết lập mọi thứ.

  1. Khi nào chính xác nó chạy?
  2. Tại sao có hai dấu ngoặc đơn?
  3. __attribute__một chức năng? Một vĩ mô? Cú pháp?
  4. Cái này có hoạt động ở C không? C ++?
  5. Liệu chức năng nó hoạt động có cần phải tĩnh không?
  6. Khi nào __attribute__((destructor))chạy?

Ví dụ trong Mục tiêu-C :

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

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

Câu trả lời:


273
  1. Nó chạy khi một thư viện chia sẻ được tải, thường là trong khi khởi động chương trình.
  2. Đó là cách tất cả các thuộc tính GCC là; có lẽ để phân biệt chúng với các cuộc gọi chức năng.
  3. Cú pháp dành riêng cho GCC.
  4. Vâng, điều này hoạt động trong C và C ++.
  5. Không, chức năng không cần phải tĩnh.
  6. Hàm hủy chạy khi thư viện dùng chung không tải, thường là khi thoát chương trình.

Vì vậy, cách các hàm tạo và hàm hủy hoạt động là tệp đối tượng dùng chung chứa các phần đặc biệt (.ctors và .dtor trên ELF) có chứa các tham chiếu đến các hàm được đánh dấu bằng các thuộc tính của hàm tạo và hàm hủy. Khi thư viện được tải / không tải, chương trình bộ tải động (ld.so hoặc somesuch) sẽ kiểm tra xem các phần đó có tồn tại hay không, và nếu vậy, hãy gọi các hàm được tham chiếu trong đó.

Hãy nghĩ về nó, có lẽ có một số phép thuật tương tự trong trình liên kết tĩnh thông thường để cùng một mã được chạy khi khởi động / tắt máy bất kể người dùng chọn liên kết tĩnh hay liên kết động.


49
Dấu ngoặc kép giúp chúng dễ dàng "macro out" ( #define __attribute__(x)). Nếu bạn có nhiều thuộc tính, ví dụ, __attribute__((noreturn, weak))thật khó để "macro out" nếu chỉ có một bộ dấu ngoặc.
Chris Jester-Young

7
Nó không được thực hiện với .init/.fini. (Bạn có thể có nhiều hàm tạo và hàm hủy trong một đơn vị dịch, không bao giờ tạo nhiều trong một thư viện - làm thế nào nó hoạt động?) Thay vào đó, trên các nền tảng sử dụng định dạng nhị phân ELF (Linux, v.v.), các hàm tạo và hàm hủy được tham chiếu trong .ctors.dtorscác phần của tiêu đề. Đúng, ngày xưa, các hàm được đặt tên initfinisẽ được chạy trên tải và hủy tải thư viện động nếu chúng tồn tại, nhưng điều đó không còn được sử dụng nữa, được thay thế bằng cơ chế tốt hơn này.
ephemient

7
@jcayzac Không, bởi vì macro matrixdic là một phần mở rộng gcc và lý do chính cho macro __attribute__là nếu bạn không sử dụng gcc, vì đó cũng là một phần mở rộng gcc.
Chris Jester-Young

9
@ ChrisJester-Macro macrodic trẻ là một tính năng C99 tiêu chuẩn, không phải là một phần mở rộng GNU.
jcayzac

4
"việc bạn sử dụng thì hiện tại (" tạo ra "thay vì" thực hiện "- các phép nhân đôi vẫn khiến chúng dễ dàng thoát ra. Bạn đã sủa sai cây mô phạm.
Jim Balter

64

.init/ .finikhông được phản đối. Nó vẫn là một phần của tiêu chuẩn ELF và tôi dám nói nó sẽ là mãi mãi. Mã trong .init/ .finiđược chạy bởi trình tải / thời gian chạy liên kết khi mã được tải / không tải. Tức là trên mỗi tải ELF (ví dụ mã thư viện dùng chung) .initsẽ được chạy. Vẫn có thể sử dụng cơ chế đó để đạt được điều tương tự như với __attribute__((constructor))/((destructor)). Đó là trường học cũ nhưng nó có một số lợi ích.

.ctors/ .dtorscơ chế ví dụ yêu cầu hỗ trợ bởi system-rtl / loader / linker-script. Điều này là không chắc chắn có sẵn trên tất cả các hệ thống, ví dụ như các hệ thống nhúng sâu, nơi mã thực thi trên kim loại trần. Tức là ngay cả khi __attribute__((constructor))/((destructor))được GCC hỗ trợ, không chắc chắn nó sẽ chạy vì nó liên kết với trình liên kết để tổ chức nó và cho trình tải (hoặc trong một số trường hợp, mã khởi động) để chạy nó. Để sử dụng .init/ .finithay vào đó, cách dễ nhất là sử dụng cờ liên kết: -init & -fini (nghĩa là từ dòng lệnh GCC, cú pháp sẽ là -Wl -init my_init -fini my_fini).

Trên hệ thống hỗ trợ cả hai phương thức, một lợi ích có thể là mã trong .initđược chạy trước .ctorsvà mã .finisau .dtors. Nếu thứ tự có liên quan đó là ít nhất một cách thô sơ nhưng dễ dàng để phân biệt giữa các hàm init / exit.

Một nhược điểm lớn là bạn không thể dễ dàng có nhiều hơn một _initvà một _finichức năng cho mỗi mô-đun có thể tải và có thể sẽ phải phân đoạn mã nhiều .sohơn động lực. Một điều nữa là khi sử dụng phương thức liên kết được mô tả ở trên, người ta sẽ thay thế các hàm _init và _finimặc định ban đầu (được cung cấp bởi crti.o). Đây là nơi tất cả các loại khởi tạo thường xảy ra (trên Linux, đây là nơi khởi tạo biến toàn cục được khởi tạo). Một cách xung quanh được mô tả ở đây

Lưu ý trong liên kết ở trên rằng _init()không cần phải xếp tầng cho bản gốc vì nó vẫn được đặt đúng chỗ. Các calltrong inline lắp ráp tuy nhiên là x86 ghi nhớ và gọi một hàm từ lắp ráp sẽ trông hoàn toàn khác nhau cho nhiều kiến trúc khác (như ARM ví dụ). Mã tức là không minh bạch.

.init/ .fini.ctors/ .detorscơ chế là tương tự, nhưng không hoàn toàn. Mã trong .init/ .finichạy "như là". Tức là bạn có thể có một số chức năng trong .init/ .fini, nhưng về mặt cú pháp, AFAIK rất khó để đặt chúng ở đó hoàn toàn trong C mà không phá vỡ mã trong nhiều .sotệp nhỏ .

.ctors/ .dtorsđược tổ chức khác với .init/ .fini. .ctors/ .dtorsphần chỉ là các bảng có con trỏ tới các hàm và "người gọi" là một vòng lặp do hệ thống cung cấp, gọi từng hàm một cách gián tiếp. Tức là người gọi vòng lặp có thể là kiến ​​trúc cụ thể, nhưng vì nó là một phần của hệ thống (nếu nó tồn tại hoàn toàn), điều đó không thành vấn đề.

Đoạn mã sau đây thêm các con trỏ hàm mới vào .ctorsmảng hàm, chủ yếu giống như cách làm __attribute__((constructor))(phương thức có thể cùng tồn tại với __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;

Người ta cũng có thể thêm các con trỏ hàm vào một phần tự phát minh hoàn toàn khác. Một kịch bản liên kết đã sửa đổi và một chức năng bổ sung bắt chước trình tải .ctors/ .dtorsvòng lặp là cần thiết trong trường hợp đó. Nhưng với nó, người ta có thể đạt được sự kiểm soát tốt hơn đối với thứ tự thực hiện, thêm đối số và trả về mã xử lý eta (Ví dụ, trong một dự án C ++, sẽ hữu ích nếu cần một cái gì đó chạy trước hoặc sau các nhà xây dựng toàn cầu).

Tôi thích __attribute__((constructor))/((destructor))ở nơi có thể, đó là một giải pháp đơn giản và thanh lịch ngay cả khi nó cảm thấy như gian lận. Đối với các lập trình viên kim loại trần như tôi, điều này không phải lúc nào cũng là một lựa chọn.

Một số tài liệu tham khảo tốt trong cuốn sách Trình liên kết & bộ tải .


làm thế nào trình nạp có thể gọi các chức năng đó? các hàm đó có thể sử dụng toàn cục và các hàm khác trong không gian địa chỉ quy trình, nhưng trình tải là một quá trình có không gian địa chỉ riêng, phải không?
user2162550

@ user2162550 Không, ld-linux.so.2 ("trình thông dịch" thông thường, trình tải cho các thư viện động chạy trên tất cả các tệp thực thi được liên kết động) chạy trong chính không gian địa chỉ của chính tệp thực thi. Nói chung, chính trình tải thư viện động là một cái gì đó cụ thể cho không gian người dùng, chạy trong ngữ cảnh của luồng cố gắng truy cập tài nguyên thư viện.
Paul Stelian

Khi tôi gọi execv () từ mã có __attribute__((constructor))/((destructor))hàm hủy không chạy. Tôi đã thử một vài thứ như thêm một mục vào .dtor như được hiển thị ở trên. Nhưng không thành công. Vấn đề rất dễ bị trùng lặp bằng cách chạy mã với numactl. Ví dụ: giả sử test_code chứa hàm hủy (thêm một hàm printf vào hàm tạo và hàm giải mã để gỡ lỗi vấn đề). Sau đó chạy LD_PRELOAD=./test_code numactl -N 0 sleep 1. Bạn sẽ thấy rằng hàm tạo được gọi hai lần nhưng hàm hủy chỉ một lần.
B Abali

39

Trang này cung cấp sự hiểu biết tuyệt vời về constructordestructorthực hiện thuộc tính và các phần bên trong trong vòng ELF cho phép họ làm việc. Sau khi tiêu hóa thông tin được cung cấp ở đây, tôi đã biên soạn một chút thông tin bổ sung và (mượn ví dụ về phần từ Michael Ambrus ở trên) đã tạo ra một ví dụ để minh họa các khái niệm và giúp tôi học hỏi. Những kết quả được cung cấp dưới đây cùng với nguồn ví dụ.

Như đã giải thích trong luồng này, các thuộc tính constructordestructorcác mục tạo ra các mục trong .ctors.dtorsphần của tệp đối tượng. Bạn có thể đặt các tham chiếu đến các hàm trong một trong ba cách. (1) sử dụng sectionthuộc tính; (2) constructordestructorcác thuộc tính hoặc (3) với một cuộc gọi lắp ráp nội tuyến (như được tham chiếu liên kết trong câu trả lời của Ambrus).

Việc sử dụng constructordestructorcác thuộc tính cho phép bạn bổ sung mức ưu tiên cho hàm tạo / hàm hủy để kiểm soát thứ tự thực hiện của nó trước khi main()được gọi hoặc sau khi nó trả về. Giá trị ưu tiên được đưa ra càng thấp, mức độ ưu tiên thực hiện càng cao (mức độ ưu tiên thấp hơn được thực hiện trước mức độ ưu tiên cao hơn trước hàm main () - và tiếp theo mức độ ưu tiên cao hơn sau hàm main ()). Các giá trị ưu tiên bạn cung cấp phải lớn hơn100 khi trình biên dịch dự trữ các giá trị ưu tiên trong khoảng từ 0 đến 100 để thực hiện. Một constructorhoặc destructorđược chỉ định với mức độ ưu tiên thực hiện trước một constructorhoặc destructorđược chỉ định mà không có mức độ ưu tiên.

Với thuộc tính 'phần' hoặc với inline-lắp ráp, bạn cũng có thể chức năng nơi tài liệu tham khảo trong .init.finiđang phần ELF rằng sẽ thực hiện trước khi bất kỳ constructor và destructor sau khi bất kỳ, tương ứng. Bất kỳ hàm nào được gọi bởi tham chiếu hàm được đặt trong .initphần, sẽ thực thi trước chính tham chiếu hàm (như bình thường).

Tôi đã cố gắng minh họa từng người trong ví dụ dưới đây:

#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;
}

đầu ra:

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

Ví dụ này đã giúp củng cố hành vi của hàm tạo / hàm hủy, hy vọng nó cũng hữu ích cho những người khác.


Nơi mà bạn thấy rằng "các giá trị ưu tiên bạn đưa ra phải lớn hơn 100"? Thông tin đó không có trong tài liệu thuộc tính chức năng GCC.
Justin

4
IIRC, có một vài tài liệu tham khảo, PATCH: Hỗ trợ đối số ưu tiên cho các đối số hàm tạo / hàm hủy (MAX_RESERVED_INIT_PRIORITY) và chúng giống như C ++ (init_priority) 7.7 C ++ - Thuộc tính biến, hàm và loại cụ thể . Sau đó, tôi đã thử nó với99: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. Tôi đã thử các ưu tiên <100 với tiếng kêu và nó dường như đang hoạt động, nhưng trường hợp thử nghiệm đơn giản của tôi (một đơn vị biên dịch) quá đơn giản .
Justin

1
Ưu tiên của các biến toàn cục tĩnh (ctor tĩnh) là gì?
bảnh bao

2
Hiệu ứng và khả năng hiển thị của toàn cầu tĩnh sẽ phụ thuộc vào cách cấu trúc chương trình của bạn (ví dụ: tệp đơn, nhiều tệp ( đơn vị dịch )) và trong đó toàn cầu được khai báo Xem: Tĩnh (từ khóa) , cụ thể là mô tả biến toàn cục tĩnh .
David C. Rankin

7

Dưới đây là một ví dụ "cụ thể" (và có thể hữu ích ) về cách thức, lý do và thời điểm sử dụng các cấu trúc tiện dụng nhưng khó coi này ...

Xcode sử dụng một "mặc định sử dụng "toàn cầu"" để quyết định XCTestObserverlớp SPEWS nó tim ra đến bao vây console.

Trong ví dụ này ... khi tôi ngầm tải thư viện psuedo này, hãy gọi nó là ... libdemure.a, thông qua một lá cờ trong mục tiêu thử nghiệm của tôi á la ..

OTHER_LDFLAGS = -ldemure

Tôi muốn..

  1. Khi tải (tức là khi XCTesttải gói thử nghiệm của tôi), ghi đè XCTestlớp "quan sát viên " mặc định " ... (thông qua constructorchức năng) PS: Theo như tôi có thể nói .. mọi thứ được thực hiện ở đây đều có thể được thực hiện với hiệu ứng tương đương bên trong tôi + (void) load { ... }phương pháp lớp .

  2. chạy thử nghiệm của tôi .... trong trường hợp này, với mức độ chi tiết kém hơn trong nhật ký (thực hiện theo yêu cầu)

  3. Trả lớp "toàn cầu" XCTestObservervề trạng thái nguyên sơ .. để không làm hỏng các XCTesthoạt động khác chưa có trên bandwagon (còn gọi là liên kết đến libdemure.a). Tôi đoán điều này trong lịch sử đã được thực hiện trong dealloc.. nhưng tôi sẽ không bắt đầu gây rối với cái mụ cũ đó.

Vì thế...

#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

Không có cờ liên kết ... (Cảnh sát thời trang Cupertino đòi quả báo , nhưng mặc định của Apple vẫn thắng thế, như mong muốn, ở đây )

nhập mô tả hình ảnh ở đây

VỚI -ldemure.acờ liên kết ... (Kết quả dễ hiểu, thở hổn hển ... "cảm ơn constructor/ destructor" ... Đám đông cổ vũ ) nhập mô tả hình ảnh ở đây


1

Đây là một ví dụ cụ thể. Nó dành cho một thư viện chia sẻ. Chức năng chính của thư viện dùng chung là giao tiếp với đầu đọc thẻ thông minh. Nhưng nó cũng có thể nhận được "thông tin cấu hình" khi chạy qua udp. Udp được xử lý bởi một luồng mà PHẢI được bắt đầu tại thời điểm init.

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

  }

Thư viện được viết bằng c.


1
Một lựa chọn kỳ lạ nếu thư viện được viết bằng C ++, vì các hàm tạo biến toàn cục thông thường là cách thành ngữ để chạy mã tiền chính trong C ++.
Nicholas Wilson

@NicholasWilson Thư viện trên thực tế được viết bằng c. Không biết làm thế nào tôi gõ c ++ thay vì c.
drlolly 17/12/17
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.