Init c что это
Перейти к содержимому

Init c что это

.init, .ctors, and .init_array

In C++, dynamic initializations for non-local variables happen before the first statement of the main function. All (most?) implementations just ensure such dynamic initializations happen before main .

As an extension, GCC supports __attribute__((constructor)) which can make an arbitrary function run before main . A constructor function can have an optional priority ( __attribute__((constructor(N))) ).

Priorities from 0 to 100 are reserved for the implementation ( -Wprio-ctor-dtor catches violation), e.g. gcov uses __attribute__((destructor(100))) . Applications can use 101 to 65535. 65535 ( .init_array or .ctors , without a suffix) has the same priority as a non-local variable’s dynamic initialization in C++.

Under the hood, on ELF platforms, the initialization functions or constructors are implemented in two schemes. The legacy one uses .init / .ctors while the new one uses .init_array .

.init and .fini

System V release 4 introduced the dynamic tags DT_INIT and DT_FINI to implement ELF initialization and termination functions. Today it is difficult to figure out what it actually did, but it was likely similar to the GCC scheme described below.

On a GCC+glibc system, traditionally the section .init in an executable/shared object consisted of four fragments:

The linker combines .init input sections and places the fragments into the .init output section. _init is defined at offset 0 in the first input section, so its address equals the address of the .init output section. The linker defines DT_INIT according to the value of _init (which can be changed by the -init linker option). In the absence of .dynamic , DT_INIT does not exist. The runtime references the symbol _init .

.fini is similar:

The linker defines DT_FINI according to the value of _fini (which can be changed by the -fini linker option).

In glibc x86-64, sysdeps/x86_64/crti.S and sysdeps/x86_64/crtn.S provide the definitions for crti.o and crtn.o :

crti.o calls __gmon_start__ (gmon profiling system) if defined. This is used by gcc -pg .

musl just provides empty crti.o and crtn.o .

.ctors and .dtors

In GCC libgcc/crtstuff.c , when __LIBGCC_INIT_ARRAY_SECTION_ASM_OP__ is not defined and __LIBGCC_INIT_SECTION_ASM_OP__ is defined ( HAVE_INITFINI_ARRAY_SUPPORT is 1 in $builddir/gcc/auto-host.h ), the following scheme is used. Note: the condition is not satisfied on modern systems.

C++ dynamic initializations and __attribute__((constructor)) do not use _init directly. They are implemented as ELF functions. The addresses are collected in the .ctors section which will be called by the runtime. Assume that we have one object files a.o and b.o with .ctors sections with different priorities, the layout of the .ctors output section is:

.dtors is similar:

  • crtbegin.o defines .ctors and .dtors with one element, -1 (0xffffffff on 32-bit platforms and 0xffffffffffffffff on 64-bit platforms).
  • crtend.o defines .ctors and .dtors with one element, 0.
  • crtend.o defines a .init section which calls __do_global_ctors_aux . __do_global_ctors_aux calls the static constructors in the .ctors section. The -1 and 0 sentinels are skipped.
  • crtbegin.o defines a .fini section which calls __do_global_dtors_aux . __do_global_dtors_aux calls the static constructors in the .dtors section. The -1 and 0 sentinels are skipped.

Reversed execution order

Here is an interesting property: .ctors elements are run in the reversed order and .dtors elements are run in the regular order. E.g. for a.o:(.ctors) b.o:(.ctors) , b.o’s constructor runs before a.o’s.

This is to make dynamic linking similar to static linking for .ctors sections without a suffix (having the lowest priority).

The origin may be related to a generic ABI promise: if a.so depends on b.so, then b.so’s constructors run first. If we only look at .ctors sections without a suffix, the behavior of ld main.o a.so b.so may be quite similar to the static linking ld main.o a.a b.a .

.dtors can be seen as undoing .ctors , so its order is the reverse of .ctors , which is the regular order.

.init_array and .fini_array

HP-UX developers noticed that the .init / .ctors scheme have multiple problems:

  • Fragmented _init function is ugly and error-prone.
  • Sentinel values in .ctors are ugly.
  • .init and .ctors use magic names instead of dedicated section types.

They invented DT_INIT_ARRAY as an alternative. glibc implemented the scheme in 1999. The GCC and binutils implementations were also quite old.

FreeBSD added support in 2012-03. OpenBSD added support in 2016-08. NetBSD made DT_INIT_ARRAY available for all ports in 2018-12.

In this scheme, .init_array and .init_array.N sections have a dedicated type SHT_INIT_ARRAY . crtbegin.o and crtend.o do not provide fragments.

Below is a layout.

Note: ctors_priority = 65535-init_array_priority

The linker defines DT_INIT_ARRAY and DT_INIT_ARRAYSZ according to the address and size of .init_array . The linker also defines __init_array_start and __init_array_end if referenced. The pair of symbols can be used by a statically linked position dependent executable which may not have .dynamic .

Unlike .ctors , the execution order of .init_array is forward (follows .init ). a.o:(.init_array) b.o:(.init_array) has a different order from a.o:(.ctors) b.o:(.ctors) . In a future section we will discuss that this difference can expose a type of very subtle bugs called «static initialization order fiasco».

In GCC, newer ABI implementations like AArch64 and RISC-V only use .init_array and don’t provide .ctors .

.preinit_array

The linker defines DT_PREINIT_ARRAY and DT_PREINIT_ARRAYSZ according to the address and size of .preinit_array . The linker also defines __preinit_array_start and __preinit_array_end if referenced.

The generic ABI says:

DT_PREINIT_ARRAY: This element holds the address of the array of pointers to pre-initialization functions, discussed in «Initialization and Termination Functions» below. The DT_PREINIT_ARRAY table is processed only in an executable file; it is ignored if contained in a shared object.

DT_PREINIT_ARRAY only applies to the executable. This feature gives the executable a way to run initialization functions before shared object dependencies.

There is no .postfini_array .

Most ld.so implementations support DT_PREINIT_ARRAY . musl does not support the feature. See add preinit_array support.

Runtime behavior

The generic ABI says:

If an object contains both DT_INIT and DT_INIT_ARRAY entries, the function referenced by the DT_INIT entry is processed before those referenced by the DT_INIT_ARRAY entry for that object. If an object contains both DT_FINI and DT_FINI_ARRAY entries, the functions referenced by the DT_FINI_ARRAY entry are processed before the one referenced by the DT_FINI entry for that object.

If the executable a depends on b.so and c.so (in order), the glibc ld.so and libc behavior is:

  • ld.so runs c.so:DT_INIT . The crtbegin.o fragment of _init calls .ctors
  • ld.so runs c.so:DT_INIT_ARRAY
  • ld.so runs b.so:DT_INIT . The crtbegin.o fragment of _init calls .ctors
  • ld.so runs b.so:DT_INIT_ARRAY
  • libc_nonshared.a runs a:DT_INIT . The crtbegin.o fragment of _init calls .ctors
  • libc_nonshared.a runs a:DT_INIT_ARRAY

As a new ABI, glibc’s RISC-V port doesn’t define ELF_INIT_FINI , so DT_INIT does not run.

Here is a test for the execution order of atexit and DT_FINI_ARRAY .

musl ensures that atexit registered hooks run before DT_FINI_ARRAY .

.ctors to .init_array transition

In 2010-12, Mike Hommey filed Replace .ctors/.dtors with .init_array/.fini_array on targets supporting them which I believe was related to his ELF hack work for Firefox.

Switching sections needed to consider backward compatibility: how to handle old object files using .ctors sections. H.J. Lu proposed that the internal linker script of GNU ld could be changed to place .ctors .ctors.N .init_array .init_array.N input sections into the .init_array output section in RFC: Support mixing .init_array.* and .ctors.* input sections .

With this GNU ld support, GCC 4.7 made the switch.

gold doesn’t have the concept of an internal linker script. Ian Lance Taylor added the enabled-by-default linker option —ctors-in-init-array to emulate the GNU ld behavior

Since .ctors is rare, ld.lld does not implement converting .ctors into .init_array .

GCC vs Clang

GCC’s .ctors / .init_array choice is a configure option —enable-initfini-array .

Clang uses a CC1 option -fno-use-init-array . This makes cross compilation and testing multiple targets in one build convenient.

In the llvm-project supported toolchains, only MinGW and PlayStation 4 still use .ctors for the latest version. For MinGW, this is related to the fact that PE/COFF does not have section types and the MinGW runtime doesn’t have the .init pain, so there isn’t motivation for a switch. For PlayStation 4, it is presumably related to the fact that PlayStation 4 uses a modified FreeBSD 9 image. I saw .ctors patches to llvm-project in 2021.

Linux remnant of .ctors in 2021

If you don’t use prebuilt object files from GCC<4.7, it is difficult to see .ctors on Linux in 2021. However, I found two exceptions.

First, a libgcc file for the split stack implementation had .ctors.65535 assembly code. I filed morestack.S should support .init_array.0 besides .ctors.65535 which was fixed in 2021-10.

Second, GCC cross compilers targeting Linux did not enable —enable-initfini-array . H.J. Lu reported —enable-initfini-array should be enabled for cross compiler to Linux and fixed it for GCC 12. This affected GCC 11 builds by scripts/build-many-glibcs.py

C++ dynamic initialization

In a typical C++ object, most .init_array elements are dynamic initializations, so I will spend some paragraphs describing it.

The standard defines the order for various initializations.

  • Constant initialization and zero initialization
  • Dynamic initialization
  • main
  • Deferred dynamic initialization (e.g. optimized out, on-demand shared library)

Dynamic initialization has three types with different degrees of order guarantee:

  • Unordered dynamic initialization (static data members and variable templates not explicitly specialized)
  • Partially-ordered dynamic initialization (inline variables that are not an implicitly or explicitly instantiated specialization)
  • Ordered dynamic initialization (other non-local variables)

Basically, in one translation unit, the order of dynamic initializations usually matches the intuition, e.g. a ‘s initializations happen before b ‘s below.

C++ static initialization order fiasco

If no appearance-ordered relationship is defined, we say that two initializations are indeterminately sequenced. Relying on a particular order is called «static initialization order fiasco». (I don’t know what «static» refers to. Perhaps it refers to static variables or static storage duration.)

Below is a registry example. The order that a, b, and C are registered depends on the link order. If somehow only one order works, than the program may be brittle.

Fixing such bugs requires thoughts on the initialization order. Basically one needs to do one of the following:

  • constant initialization
  • lazy initialization (dynamic initialization of function-locale static, llvm::ManagedStatic, etc)
  • manual initialization
  • Nifty Counter idiom

Some ways to prevent static initialization order fiasco:

  • constexpr
  • constinit (constexpr — const)
  • clang -Wglobal-constructors : warning: declaration requires a global constructor [-Wglobal-constructors]

AddressSanitizer check_initialization_order is enabled by default due to strict_init_order . It enforces that a dynamic initialization does not touch memory regions of other global variables. Unfortunately in practice it misses many many cases.

ld.lld —shuffle-sections=.init_array=-1

In ld.lld, Rafael Espindola added —shuffle-sections motivated by making tests stabilized. I changed the option to apply to .init_array / .init_array as well and later changed it to the current form: —shuffle-sections=<section-glob>=<seed> : shuffle matched input sections using the given seed before mapping them to the output sections. I specialized the seed value -1 to mean the deterministic reversed order.

You can specify —shuffle-sections=.init_array=-1 —shuffle-sections=.fini_array=-1 to reverse the input section order. It’s unclear whether .fini_array needs to be reversed as well, but it is safe to do so. This does not change .init_array.N and .fini_array.N , but in practice static initialization order fiasco from prioritized sections are rare.

In practice, most static initialization order fiasco bugs are due to the order between two translation units. For a mostly statically linked executable, testing the regular order and the reversed order is sufficient to catch such bugs.

Функции

Ч ем дальше мы изучаем си, тем больше становятся программы. Мы собираем все действия в одну функцию main и по несколько раз копируем одни и те же действия, создаём десятки переменных с уникальными именами. Наши программы распухают и становятся всё менее и менее понятными, ветвления становятся всё длиннее и ветвистее.

Но из сложившейся ситуации есть выход! Теперь мы научимся создавать функции на си. Функции, во-первых, помогут выделить в отдельные подпрограммы дублирующийся код, во-вторых, помогут логически разбить программу на части, в-третьих, с функциями в си связано много особенностей, которые позволят использовать новые подходы к структурированию приложений.

Функция – это именованная часть программы, которая может быть многократно вызвана из другого участка программы (в котором эта функция видна). Функция может принимать фиксированное либо переменное число аргументов, а может не иметь аргументов. Функция может как возвращать значение, так и быть пустой (void) и ничего не возвращать.

Мы уже знакомы с многими функциями и знаем, как их вызывать – это функции библиотек stdio, stdlib, string, conio и пр. Более того, main – это тоже функция. Она отличается от остальных только тем, что является точкой входа при запуске приложения.
Функция в си определяется в глобальном контексте. Синтаксис функции:

Самый простой пример – функция, которая принимает число типа float и возвращает квадрат этого числа

Внутри функции sqr мы создали локальную переменную, которой присвоили значение аргумента. В качестве аргумента функции передали число 9,3. Служебное слово return возвращает значение переменной tmp. Можно переписать функцию следующим образом:

В данном случае сначала будет выполнено умножение, а после этого возврат значения. В том случае, если функция ничего не возвращает, типом возвращаемого значения будет void. Например, функция, которая печатает квадрат числа:

в данном случа return означает выход из функции. Если функция ничего не возвращает, то return можно не писать. Тогда функция доработает до конца и произойдёт возврат управления вызывающей функции.

Если функция не принимает аргументов, то скобки оставляют пустыми. Можно также написать слово void:

Формальные и фактические параметры

П ри объявлении функции указываются формальные параметры, которые потом используются внутри самой функции. При вызове функции мы используем фактические параметры. Фактическими параметрами могут быть переменные любого подходящего типа или константы.

Например, пусть есть функция, которая возвращает квадрат числа и функция, которая суммирует два числа.

Обращаю внимание, что приведение типов просиходит неявно и только тогда, когда это возможно. Если функция получает число в качестве аргумента, то нельзя ей передать переменную строку, например «20» и т.д. Вообще, лучше всегда использовать верный тип или явно приводить тип к нужному.
Если функция возвращает значение, то оно не обязательно должно быть сохранено. Например, мы пользуемся функцией getch, которая считывает символ и возвращает его.

Передача аргументов

При передаче аргументов происходит их копирование. Это значит, что любые изменения, которые функция производит над переменными, имеют место быть только внутри функции. Например

Программы выведет
200
100
200
Понятно почему. Внутри функции мы работаем с переменной x, которая является копией переменной d. Мы изменяем локальную копию, но сама переменная d при этом не меняется. После выхода из функции локальная переменная будет уничтожена. Переменная d при этом никак не изменится.
Каким образом тогда можно изменить переменную? Для этого нужно передать адрес этой переменной. Перепишем функцию, чтобы она принимала указатель типа int

Вот теперь программа выводит
200
100
100
Здесь также была создана локальная переменная, но так как передан был адрес, то мы изменили значение переменной d, используя её адрес в оперативной памяти.

В программировании первый способ передачи параметров называют передачей по значению, второй – передачей по указателю. Запомните простое правило: если вы хотите изменить переменную, необходимо передавать функции указатель на эту переменную. Следовательно, чтобы изменить указатель, необходимо передавать указатель на указатель и т.д. Например, напишем функцию, которая будет принимать размер массива типа int и создавать его. С первого взгляда, функция должна выглядеть как-то так:

Но эта функция выведет ERROR. Мы передали адрес переменной. Внутри функции init была создана локальная переменная a, которая хранит адрес массива. После выхода из функции эта локальная переменная была уничтожена. Кроме того, что мы не смогли добиться нужного результата, у нас обнаружилась утечка памяти: была выделена память на куче, но уже не существует переменной, которая бы хранила адрес этого участка.

Для изменения объекта необходимо передавать указатель на него, в данном случае – указатель на указатель.

Вот теперь всё работает как надо.
Ещё подобный пример. Напишем функцию, которая принимает в качестве аргумента строку и возвращает указатель на область памяти, в которую скопирована эта строка.

В этом примере утечки памяти не происходит. Мы выделили память с помощью функции malloc, скопировали туда строку, а после этого вернули указатель. Локальные переменные были удалены, но переменная test хранит адрес участка памяти на куче, поэтому можно его удалить с помощью функции free.

Объявление функции и определение функции. Создание собственной библиотеки

В си можно объявить функцию до её определения. Объявление функции, её прототип, состоит из возвращаемого значения, имени функции и типа аргументов. Имена аргументов можно не писать. Например

Это смешанная рекурсия – функция odd возвращает 1, если число нечётное и 0, если чётное.

Обычно объявление функции помещают отдельно, в .h файл, а определение функций в .c файл. Таким образом, заголовочный файл представляет собой интерфейс библиотеки и показывает, как с ней работать, не вдаваясь в содержимое кода.

Давайте создадим простую библиотеку. Для этого нужно будет создать два файла – один с расширением .h и поместить туда прототипы функций, а другой с расширением .c и поместить туда определения этих функций. Если вы работаете с IDE, то .h файл необходимо создавать в папке Заголовочные файлы, а файлы кода в папке Файлы исходного кода. Пусть файлы называются File1.h и File1.c
Перепишем предыдущий код. Вот так будет выглядеть заголовочный файл File1.h

Содержимое файла исходного кода File1.c

Наша функция main

Рассмотрим особенности каждого файла. Наш файл, который содержит функцию main, подключает необходимые ему библиотеки а также заголовочный файл File1.h. Теперь компилятору известны прототипы функций, то есть он знает возвращаемый тип, количество и тип аргументов и имена функций.

Заголовочный файл, как и оговаривалось ранее, содержит прототип функций. Также здесь могут быть подключены используемые библиотеки. Макрозащита #define _FILE1_H_ и т.д. используется для предотвращения повторного копирования кода библиотеки при компиляции. Эти строчки можно заменить одной

Файл File1.c исходного кода подключает свой заголовочный файл. Всё как обычно логично и просто. В заголовочные файлах принято кроме прототипов функций выносить константы, макроподстановки и определять новые типы данных. Кроме того, именно в заголовочных файлах можно обширно комментировать код и писать примеры его использования.

Передача массива в качестве аргумента

К ак уже говорилось ранее, имя массива подменяется на указатель, поэтому передача одномерного массива эквивалентна передаче указателя. Пример: функция получает массив и его размер и выводит на печать:

В этом примере функция может иметь следующий вид

Также напомню, что правило подмены массива на указатель не рекурсивное. Это значит, что необходимо указывать размерность двумерного массива при передаче

Либо, можно писать

Если двумерный массив создан динамически, то можно передавать указатель на указатель. Например функция, которая получает массив слов и возвращает массив целых, равных длине каждого слова:

Можно вместо того, чтобы возвращать указатель на массив, передавать массив, который необходимо заполнить

На этом первое знакомство с функциями заканчивается: тема очень большая и разбита на несколько статей.

email

Всё ещё не понятно? – пиши вопросы на ящик

Как работают демоны, процесс Init и как у процессов рождаются потомки — изучаем основы Unix

Если вы когда-нибудь работали c Unix-системами, то наверняка слышали термин «демон». В этой статье я хочу объяснить, что это за демоны и как они работают, тем более что их название заставляет думать, что это что-то плохое.

Вообще демон — это фоновый процесс, который не привязан к терминалу, в котором был запущен. Но как они создаются, как они связаны с другими процессами, как они работают? Об этом мы и поговорим, но сперва давайте узнаем, как работает процесс init и как происходит рождение новых процессов.

Как работает процесс Init

Для начала поговорим о процессе init, также известном как PID 1 (поскольку его ID всегда равен 1). Это процесс создаётся сразу при запуске системы, то есть все другие процессы являются его потомками.

Обычно init запускается, когда ядро вызывает конкретный файл, обычно находящийся по адресу /etc/rc или /etc/inittab. Процесс устанавливает путь, проверяет файловую систему, инициализирует серийные порты, задаёт время и т.д. В последнюю очередь он запускает все необходимые фоновые процессы — в виде демонов. Все демоны обычно расположены в папке /etc/init.d/; принято оканчивать имена демонов на букву d (например, httpd, sshd, mysqld и т.п.), поэтому вы можете подумать, что директория названа так по этому же принципу, но на самом деле существует соглашение об именовании папок, содержащих конфигурационные файлы, именем с суффиксом .d. Итак, init запускает демонов, но мы так и не выяснили, как это происходит. Процесс init запускает демонов, создавая свои ответвления для запуска новых процессов.

Как работает разветвление процессов

Единственный способ создать новый процесс в Unix — скопировать существующий. Этот метод, известный как разветвление или форкинг, включает в себя создание копии процесса в виде потомка и системный вызов exec для запуска новой программы. Мы использовали слово «форкинг», поскольку fork — это реальный метод C в стандартной библиотеке Unix, который создаёт новые процессы именно таким образом. Процесс, вызывающий команду fork, считается родительским по отношению к созданному. Процесс-потомок почти полностью совпадает с родительским: отличаются лишь ID, родительские ID и некоторые другие моменты.

В современных дистрибутивах Unix и Linux процессы можно создавать и другим способами (например, при помощи posix_spawn), но большая часть процессов создаётся именно так.

Теперь, когда вы узнали о традиционном значении термина «fork», становится понятно, почему такое же понятие используется на GitHub. Но я отвлекся — вернемся к нашим демонам!

Как работают демоны

Схема демона Максвелла

Схема демона Максвелла

Прежде чем мы углубимся в работу демонов, давайте выясним, откуда взялось это название. Термин «демон» возник из Project MAC, который в свою очередь получил своё имя от демона Максвелла — вымышленного существа из мысленного эксперимента, которое постянно сортирует молекулы. Само слово демон происходит от греческого daemon, являющегося сверхъестественным существом, которое постоянно работает на заднем плане и не является добрым или злым (в отличие от обычного современного значения). То есть, термин «демон» (в смысле Unix-процесса) на самом деле произошёл от вымышленного сверхъестественного существа.

Демоны — это фоновые процессы, работающие отдельно от терминала и почти всегда созданные процессом init; обычно они занимаются такими вещами, как сетевые запросы, работой аппаратного обеспечения и прочими заданиями типа «жди и смотри».

Демоны появляются двумя способами. Их может создать процесс init, либо же они возникают в следущей ситуации: процесс создаёт своего потомка и тут же завершается. Первый случай ясен, но что происходит во втором: как процесс init становится родительским для этих демонов?

Когда вы создаёте процесс-потомок и тут же «убиваете» его родителя, потомок становится процессом-сиротой (не стоит путать с процессом-зомби, например, потомком, который был завершен, но всё ещё ждёт, когда родитель прочтёт его exit-статус). По умолчанию, если процесс становится сиротой, то его «приёмным» родителем становится init. Вот и всё, что делает демонов уникальными!

Заключение

В целом демоны — это очень простая для понимания концепция, но чтобы полностью в них разобраться, нам понадобилось узнать, что такое init-процесс и как устроено разветвление процессов.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *