Компиляция, связывание, загрузка и запуск программ

Операционная система

В операционной системе Linux программа на C проходит долгий и сложный процесс от записи до окончательного выполнения процессором. На рисунке ниже показан этот процесс

содержание

  1. компилировать
  2. формат объектного файла
  3. Ссылка на сайт
  4. нагрузка
  5. бегать

Сборник

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

предварительная обработка

Процесс предварительной компиляции делает следующее с исходным кодом

  • удалить все комментарии
  • Удалите все #define и разверните все определения макросов.
  • Вставьте все #include файлыПримечание 1Содержимое исходного файла отправляется в соответствующую позицию в исходном файле, а процесс включения выполняется рекурсивно.

gcc может использовать следующие команды для предварительной компиляции языка C и вывода предварительно скомпилированного результата в файл hello.i.

gcc -E hello.c -o hello.i

компилировать

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

gcc -S hello.i -o hello.s

Или мы можем объединить предварительную обработку и компиляцию в один шаг.

gcc -S hello.c -o hello.s

компиляция

Целью ассемблера является преобразование ассемблерного кода в машинные инструкции.Поскольку почти каждая ассемблерная инструкция соответствует машинной инструкции, процесс ассемблера относительно прост. Мы можем использовать следующую команду для сборки

gcc -c hello.s -o hello.o

Или мы можем напрямую скомпилировать файлы исходного кода в объектные файлы.

gcc -c hello.c -o hello.o

Файл, созданный в результате операции сборки, называется объектным файлом (Object File).Структура объектного файла такая же, как и у исполняемого файла, и между ними есть лишь некоторые тонкие различия. Объектный файл не может быть выполнен, его тоже нужно пройтиСсылка на сайтНа этом шаге исполняемый файл может быть сгенерирован только после того, как объектный файл будет связан.

Давайте разберемсяформат объектного файлаа такжеСсылка на сайтЧто именно делает этот шаг.

2. Формат объектного файла

Формат объектных файлов в Linux называется ELF (Executable Linkable Format) Формат ELF показан на следующем рисунке:

Заголовок ELF является наиболее важной частью файла ELF. Заголовок содержит следующее содержимое:

  • Магическое число ЭЛЬФА
  • длина файловой машины в байтах
  • ЭЛЬФ версия
  • Платформа операционной системы
  • Аппаратная платформа
  • адрес входа в программу
  • Положение и длина таблицы сегмента
  • количество сегментов

Из шапки можно почерпнуть много полезной информации, одна из которых особенно важна, т.е.Расположение и длина таблицы сегментов, с помощью этой информации мы можем получить таблицу заголовков разделов из файла ELF.По важности таблица заголовков разделов в файле ELF уступает только заголовку.

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

Поскольку таблица сегмента определяет свойства всех сегментов, файл эльфов сегмента является именно то, что это? На самом деле, только сегмент классификации различных типов данных в файле ELF. Например, мы поставляем весь код (инструкции) в один сегмент и в сегмент с именем.text; Поставить все инициализированные данные.dataсегмент; поместите все неинициализированные данные в.bssсегмент; поместите все данные только для чтения в.rodataсегмент и т.д.

Что касается того, почему данные (инструкция также считается разновидностью данных в ELF-файле, она является одним из данных ELF-файла) разбиты на разные типы, помимо удобства разграничения, есть следующие причины

  • Для сегментов удобно устанавливать права на чтение и запись, для некоторых сегментов достаточно установить права только на чтение.
  • Удобно, чтобы кеш ЦП вступил в силу
  • Выгодно экономить память, например, когда программа имеет несколько копий, в данный момент нужен только один сегмент кода.

Поскольку сегментация имеет много преимуществ, давайте внимательно рассмотрим информацию о сегментах в файле ELF. Существует следующий пример файла hello.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
int printf(const char *format, ...);

int global_init_var = 84;
int global_uninit_var;

void func1(int i)
{
    printf("%d\n", i);
}

int main(void)
{
    static int static_var = 85;
    static int static_var2;

    int a = 1;
    int b;

    func1(static_var + static_var + a + b);

    return a;
}

Скомпилируйте исходный код в объектный файл, используя следующую команду

gcc -c hello.c -o hello.o

Далее мы можем использоватьobjdumpКоманда для просмотра внутренней структуры файла ELF, -h означает отображение информации заголовка файла ELF.

objdump -h hello.o

Результат выглядит следующим образом

hello.o:     file format elf64-x86-64

Sections:
Idx Name          Size      VMA               LMA               File off  Algn
0 .text         00000055  0000000000000000  0000000000000000  00000040  2**0
                CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
1 .data         00000008  0000000000000000  0000000000000000  00000098  2**2
                CONTENTS, ALLOC, LOAD, DATA
2 .bss          00000004  0000000000000000  0000000000000000  000000a0  2**2
                ALLOC
3 .rodata       00000004  0000000000000000  0000000000000000  000000a0  2**0
                CONTENTS, ALLOC, LOAD, READONLY, DATA
4 .comment      00000036  0000000000000000  0000000000000000  000000a4  2**0
                CONTENTS, READONLY
5 .note.GNU-stack 00000000  0000000000000000  0000000000000000  000000da  2**0
                CONTENTS, READONLY
6 .eh_frame     00000058  0000000000000000  0000000000000000  000000e0  2**3
                CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA

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

  • Размер: размер сегмента
  • VMA: виртуальный адрес сегмента, поскольку объектный файл еще не был связан, виртуальный адрес равен 0.
  • LMA: адрес, по которому загружается сегмент, по той же причине, что и выше, равен 0.
  • File off: адрес смещения сегмента в файле ELF.
  • СОДЕРЖАНИЕ: указывает, что этот сегмент существует в файле ELF.

Мы фокусируемся.text,.data,.bssи.rodataЭти пункты:

  • Сегмент .text сохраняет всю информацию об инструкциях в программе, objdump's-sПараметр указывает, что содержимое сегмента печатается в шестнадцатеричном формате, а-dПараметры сделают все абзацы, содержащие инструкции, поэтому вы можете получить подробную информацию о сегменте кода, используя следующую команду.
    objdump -s -d hello.o
    
  • .data раздел сохранитьИнициализированные глобальные переменные и локальные статические переменные
  • .bass раздел сохранитьНеинициализированные глобальные переменные и локальные статические переменныеЗаметка 3
  • .Rodata раздел хранения данных только для чтения, таких как строковые константы, переменные, измененные const

ELF также включает множество других типов сегментов, желающие могут получить доступ к соответствующей информации для дальнейшего понимания.

3. (Статическое) Связывание

Поскольку память и дисковое пространство машины теперь достаточно велики, а динамическая компоновка экономит очень ограниченную память и дисковое пространство, мы можем игнорировать преимущество динамической компоновки в экономии места. Наоборот, поскольку нет зависимости от библиотеки динамической компоновки, нет необходимости рассматривать разные версии библиотеки динамической компоновки.Статически связанные файлы можно компоновать и выполнять, что снижает сложность эксплуатации, обслуживания и развертывания, что очень удобно.В некоторых вновь изобретенных языках (например, golang) процесс компоновки по умолчанию начал использовать статическую компоновку.

Процесс статического связывания разделен на два этапа.

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

У нас есть два исходных файла a.c и b.c следующим образом

1
2
3
4
5
6
7
8
// a.c
extern int shared;

int main()
{
    int a = 100;
    swap(&a, &shared);
}

1
2
3
4
5
6
7
// b.c
int shared = 1;

void swap(int *a, int *b)
{
    *a ^= *b ^= *a ^= *b;
}

Скомпилируйте исходный код, чтобы получить объектные файлы A.O и B.O

gcc -c a.c b.c -zexecstack -fno-stack-protector -g

Свяжите объектные файлы a.o и b.o, чтобы получить исполняемый файл

ld a.o b.o -e main -o ab

В файле ELF есть два именитаблица перемещенийиТаблица символовМы не рассказывали раньше, они сыграли важную роль в процессе ссылки, давайте узнаем больше об этих двух пунктах.

таблица перемещений

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

мы можем использоватьobjdump -r a.oполучить информацию о таблице перемещений

...
RELOCATION RECORDS FOR [.text]:
OFFSET           TYPE              VALUE
0000000000000014 R_X86_64_32       shared
0000000000000021 R_X86_64_PC32     swap-0x0000000000000004
...

Мы также можем использоватьreadelf -S a.oКоманда, чтобы узнать больше о файле ELF

...
[ 1] .text             PROGBITS         0000000000000000  00000040
    000000000000002c  0000000000000000  AX       0     0     1
[ 2] .rela.text        RELA             0000000000000000  00000430
    0000000000000030  0000000000000018   I      18     1     8
...

Из их,.relaНачало — сегмент релокации, выше.rela.textОн хранит информацию об инструкции, которую необходимо переместить.Точно так же, если это данные, которые необходимо переместить, имя сегмента должно называться.rela.data.

Все вышеперечисленные операции выполняются для целевого файла a.o. Когда мы выполняем вышеуказанную команду для целевого файла b.o, мы можем обнаружить, что нет ни таблицы перемещения сегментов данных, ни таблицы перемещения сегментов кода. Это связано с тем, что общая переменная и функция swap в b.c уже знают свои адреса, поэтому их не нужно перемещать.

В ac все по-другому, потому что в ac общая переменная и функция swap не определены в текущем файле, поэтому информация об их адресах не существует в объектном файле, сгенерированном после компиляции, поэтому компилятору необходимо поместить их в релокацию. В таблице дождитесь времени компоновки, а затем найдите соответствующую информацию о символе в других объектных файлах и переместите ее.

Таблица символов (.symtab)

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

использовать командуreadelf -sВы можете просмотреть содержимое таблицы символов

$ readelf -s a.o
...
 8: 0000000000000000    79 FUNC    GLOBAL DEFAULT    1 main
 9: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND shared
10: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND swap
...

$ readelf -s b.o
...
8: 0000000000000000     4 OBJECT  GLOBAL DEFAULT    2 shared
9: 0000000000000000    75 FUNC    GLOBAL DEFAULT    1 swap
...

$ readelf -s ab
...
10: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS a.c
11: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS b.c
12: 0000000000400114    45 FUNC    GLOBAL DEFAULT    1 swap
13: 00000000006001a0     4 OBJECT  GLOBAL DEFAULT    3 shared
14: 00000000006001a4     0 NOTYPE  GLOBAL DEFAULT    3 __bss_start
15: 00000000004000e8    44 FUNC    GLOBAL DEFAULT    1 main
16: 00000000006001a4     0 NOTYPE  GLOBAL DEFAULT    3 _edata
17: 00000000006001a8     0 NOTYPE  GLOBAL DEFAULT    3 _end
...
  • Первый столбец — это координаты в массиве таблицы символов.
  • Второй столбец - это символьное значение
  • Третий столбец - размер
  • Четвертый столбец — тип символа.
  • Пятая колонна является обязательной информацией
  • Последний столбец - название символа

ЗаказnmОн также может реализовать операцию просмотра символов

$ nm a.o
                 U __stack_chk_fail
0000000000000000 T main
                 U shared
                 U swap

$ nm b.o
0000000000000000 D shared
0000000000000000 T swap

$ nm ab
00000000006001a4 D __bss_start
00000000006001a4 D _edata
00000000006001a8 D _end
00000000004000e8 T main
00000000006001a0 D shared
0000000000400114 T swap

Где D означает, что символ является инициализированной переменной, T означает, что символ является инструкцией, а U означает, что символ еще не определен.

Из приведенных выше результатов видно, что процесс компоновки «склеивает» символы объектного файла.

В: Какова связь между таблицей перемещений и таблицей символов?

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

Из приведенного выше процесса мы видим, что есть три вещи, которые компоновщик должен сделать в конце:

  1. Слияние сегментов одного типа из разных объектных файлов
  2. Для ссылок на символы в объектных файлах найдите символы, на которые можно ссылаться в других объектных файлах.
  3. Переместить адреса переменных в объектные файлы

подключение статических библиотек

Операционная система обычно поставляется с некоторыми библиотечными файлами.Наиболее известной из Linux является статическая библиотека libc, которая обычно находится в/usr/lib/libc.a, Libc.a на самом деле сжатый файл, который содержит библиотеки printf.o, scanf.o, malloc.o, read.o и т.д. Когда содержимое стандартной библиотеки, компоновщик будет ориентироваться на пользовательские файлы и ссылку на стандартную библиотеку, чтобы получить окончательный исполняемый файл.

контроль процесса линковки

Ссылки по умолчанию генерируют файл ELF, чего мы и хотим в Linux. Но иногда нам нужны другие форматы объектных файлов, и даже иногда мы хотим сами написать ядро ​​операционной системы, в настоящее время формат файла ELF явно не может удовлетворить наши требования. На самом деле, мы можем контролировать процесс связывания и результаты связывания через некоторые параметры командной строки или непосредственно с помощью конфигурационных файлов.За подробностями обращайтесь к соответствующей документации команды ld, которая здесь не будет представлена.

4. Загрузка

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

Поскольку современные операционные системы используют подкачку для управления памятью, операционной системе нужно только прочитать заголовок исполняемого файла, а затем встроить исполняемый файл в виртуальную память.Примечание 5Отношения отображения без необходимости реальной программы в памяти. В процессе запуска программы мы обнаружили, что некоторые страницы памяти ЦП в физической памяти не существуют и поэтому вызывают исключение отсутствующей страницы. страницы читаются с диска в физическую память. После того, как данные будут прочитаны, операционная система разрешит выполнение инструкции jmp ЦП, вызвавшей исключение отсутствующей страницы, для продолжения, тогда выполнение инструкции не будет иметь аномалии отсутствующих страниц.

Игнорируя влияние адресов физической памяти и исключений ошибки страницы, после того, как операционная система создаст процесс (форк) и загрузит исполняемый файл (exec), распределение виртуальной памяти должно быть таким, как показано на следующем рисунке.

Вы можете видеть, что несколько разделов в файле ELF объединены в 3 сегмента в памяти.

Segment name Data type
1 BSS segment сохранить неинициализированные данные
2 Data segment сохранить инициализированные данные
3 Text segment Инструкция по сохранению программы

На картинке выше, помимо трех сегментов, которые сохраняют данные файла ELF, есть следующие части

название описывать
Kernel space Пространство ядра операционной системы, сохраняет данные ядра операционной системы, и пользовательский процесс не имеет права доступа к этому адресу
Куча Он используется для реализации вызовов функций в программе.В следующем разделе работы программы мы узнаем больше о том, как работает стек.
куча Чтобы сохранить глобальные переменные, сгенерированные во время выполнения программы (не во время компиляции)Примечание 6.
Memory Map Отображение дискового пространства в память, данными на диске можно манипулировать так же, как данными в памяти.

5. Беги

Начать выполнение

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

Выполнение программы выглядит следующим образом:

  1. После операционной системы создает процесс, JMP в функцию ввода процесса
  2. Инициализируется среда выполнения функции входа, включая стек, ввод-вывод, потоки, конфигурационные/глобальные переменные и т.д.
  3. После завершения функции ввода завершается инициализация, она вызывает основную функцию для начала выполнения основного корпуса программы
  4. После выполнения основной функции она возвращается к функции входа, функция входа очищается и, наконец, завершает процесс с помощью системного вызова.

вызов функции

Стек используется для поддержки контекста вызовов функций, а вызовы функций выполняются через стек.

Стек сам по себе является контейнером, и его характеристика — FILO. Из приведенной выше диаграммы распределения памяти Linux мы можем знать, что стек в памяти растет вниз. В x86 регистр esp используется для сохранения адреса вершины стека текущего процесса, вталкивание элементов в стек, значение в esp уменьшается, выталкивание элементов из стека, значение в esp увеличивается.

В стеке хранится некоторая информация, необходимая для каждого вызова функции. Часть информации, хранимой для каждой функции, называется кадром стека, и стек делится на множество кадров стека. Каждый кадр стека содержит следующую информацию о функции

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

Когда функция вызывается, она делает следующее

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

Когда вызывается функция, esp сводится к местоположению данных на шаге 2 выше, адрес инструкции извлекается из стека, а jmp продолжает выполнять инструкцию.

Управление кучей и памятью

Куча - это огромный кусок памяти. Программы могут претендовать на память в куче. Эта память может использоваться по желанию, пока программа добровольно не откажется. Куча в желтой части картинки выше, мы называем ее здесьтрадиционная динамическая память, память кучи Linux состоит изТрадиционная кучаиMemory Map Segmentсоставлены вместе.

под линуксbrk()иmmap()Все системные вызовы могут использоваться для обращения к куче памяти, и способы получения кучи памяти следующие:

  • brk — отправить программу brk по старшему адресу, чтобы получить новое традиционное пространство кучи памяти.
  • MMAP - это свободное пространство памяти в сегменте карты памяти

Но мы обычно не используем прямые системные вызовы, а используем библиотечные функции для кучи памяти приложения, мы обычно применяем к использованию памяти в функции glibc malloc, она будет использовать разные реализации в зависимости от размера памяти приложения

Если запрошенная память меньше 128 КБ

  1. Во-первых, проверьте, достаточно ли свободной памяти в традиционной памяти кучи, если есть достаточно свободной памяти, она напрямую выделяется пользовательской программе, минуя системный вызов.
  2. Вызывается, если в традиционной куче недостаточно свободной памятиbrkСистемные вызовы для увеличения традиционной кучи для получения новой памяти для выделения пользовательским программам.
  3. Если освобожденная память находится в программе brk, то вызовитеbrkРазмер кучи сокращения системных вызовов
  4. Если свободная память находится внутри традиционной кучи, то следующая запись, которая будет освобождена библиотекой функций расположения и размера пространства, после еще одного приоритета приложения памяти из традиционной кучи свободной памяти, выделенного пространства, без необходимости повторного вызоваbrkсистемный вызов

Если запрошенная память больше 128 КБ

  1. использоватьmmapСистемный вызов для запроса памяти
  2. mmapБесплатное использование запрошенной памятиmunmapсистемный вызов для реализации

системный вызов

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

Процессор x86 предоставляет четыре уровня привилегий, а Linux использует два из них, которые в Linux называются режимом ядра и режимом пользователя Уровень привилегий режима ядра выше, чем у пользовательского режима. Ядро операционной системы (верхнее пространство ядра на рисунке выше) работает в режиме ядра, и программа пользователя не имеет права доступа к данным в режиме ядра.Если программа пользователя хочет вызвать функцию в ядре , он должен использовать системный вызов.

В x86 прерывания используются для отправки информации в ЦП.Как только ЦП получает информацию о прерывании, он прекращает выполнение текущей задачи и выполняет ее в соответствии с номером прерывания.обработчик прерываний. Функция обработки прерывания реализуется операционной системой.Вообще говоря, каждый номер прерывания имеет свою собственную функцию обработки прерывания.Эти функции обработки прерывания формируюттаблица векторов прерываний, таблица векторов прерываний реализована и управляется операционной системой. Прерывания могут генерироваться аппаратными средствами, такими как нажатия клавиш, щелчки мышью и т. д. Прерывания также могут генерироваться программным обеспечением В x86 прерывание 0x80 запускается программным обеспечением, а прерывание 0x80 является ядром реализации системных вызовов.

Процесс вызова системного вызова пользовательской программой выглядит следующим образом:

  1. Программа пользователя сначала в соответствии с соглашением о вызовахПримечание 7Сохраните параметры, требуемые функцией обработчика прерывания, в указанном регистре, напримерeaxРегистр должен сохранять количество системных вызовов,eax = 1Соответствующий системный вызовexit,eax = 2соответствоватьfork,и т.д
  2. После установки параметров пользовательская программа выполняетсяint 0x80инструкция, ЦП получает информацию о прерывании
  3. ЦП передает полномочия управления ядру операционной системы, а стек процессов переключается с пользовательского стека на стек ядра.Примечание 8.
  4. Обработчик прерывания для прерывания № 0x80 в таблице векторов прерываний начинает выполняться
  5. Функция обработчика прерываний из регистраeaxНомер системного вызова получается из номера системного вызова, а указанная функция системного вызова находится в соответствии с номером системного вызова.
  6. Функция системного вызова получает необходимые параметры из согласованного регистра, и функция системного вызова начинает выполняться в соответствии с параметрами
  7. После выполнения системного вызова результат системного вызова сохраняется в области (регистре или памяти), к которой имеет доступ пользовательская программа.
  8. Системный вызов возвращает управление обратно в программу пользователя
  9. Пользовательская программа получает результат системного вызова из указанной области, и системный вызов завершается

Когда пользователи пишут на языке C, они не вызывают системные вызовы вручную, они обычно инкапсулируются в библиотечные функции. НапримерprintfФункция — это системный вызовwriteИнкапсуляция, назовем ее вручнуюwriteСистемный вызов для реализации функции вывода символов на стандартный вывод.

Я предпочитаю сборку формата AT&T и Intel, поддерживаемую gcc.NASMСинтаксис сборки, ниже приведен код сборки для печати строки в стандартный вывод с использованием NASM.

global _start   ; _start是一个符号(.symbol),链接器会把其作为entry point

; 数据段
section .data
    buffer db 'hello, system call', 10, 0   ; buffer,10是换行符,0是字符串结束
    length db 20                            ; buffer的长度

; 代码段
section .text
    _start:
        mov eax, 4          ; 4,write系统调用
        mov ebx, 1          ; fd(文件描述符),1为标准输出
        mov ecx, buffer     ; buffer的地址
        mov edx, [length]   ; 根据地址从数据段获取buffer的长度
        int 0x80            ; system call

        mov eax, 1          ; 1,exit系统调用
        mov ebx, 0          ; exit code
        int 0x80            ; system call

Сохраните код сборки как файл print.asm, а затем выполните следующую команду для печати.

$ nasm -f elf64 print.asm -o print.o
$ ld print.o -o print
$ ./print
hello, system call

Суммировать

Связь между операционной системой и компилятором очень тесная, а файл ELF является связующим звеном между операционной системой и компилятором. В дополнение к тесной связи между операционной системой и компилятором, операционная система и компилятор также очень тесно связаны с ЦП и памятью: операционная система отвечает за управление памятью, и большая часть наших программных операций также выполняется. связанные с памятью; Что касается ЦП, то мы не только можем реализовать системные вызовы через прерывания, но и самой операционной системе нужен уровень привилегий ЦП для защиты ядра.

Оглядываясь назад на историю, мы обнаружим, что язык C был придуман для Unix, и они постоянно дополняются и совершенствуются в процессе разработки, именно поэтому сегодня мы видим очень тесно связанные Unix-подобные операции.Система и компилятор языка C .

Примечание:

  1. Включаемый файл имеет следующие два синтаксиса
    1. #include <filename.h>: Компилятор даст приоритет некоторым папкам по умолчаниюЗаметка 2найти заголовочный файл в
    2. #include "filename.h": Компилятор сначала ищет заголовочный файл в текущем каталоге, и если он не может его найти, переходит в папку по умолчанию, чтобы найти его.
  2. Вообще говоря/usr/includeили/usr/local/includeбудет использоваться как папка по умолчанию, в gcc вы также можете использовать-I $include_pathуказать каталог включаемых файлов
  3. Статика влияет только на ее видимость для глобальных переменных (по умолчанию видны другие файлы, а при добавлении статики виден только текущий файл; то же самое верно и для функций), а для локальных переменных влияет только область ее хранения. Тогда мы можем получить следующие правила
    • Неинициализированная глобальная переменная: .bss
    • Инициализированная глобальная переменная: .data
    • Неинициализированная локальная статическая переменная: .bss
    • Инициализированная локальная статическая переменная: .data
    • Неинициализированная локальная обычная переменная: стек
    • Инициализированные локальные нормальные переменные: стек
  4. Чтобы просмотреть библиотеку динамической компоновки, на которую ссылается исполняемый файл, используйте командуldd
  5. Виртуальный адрес преобразуется в физический адрес посредством сопоставления MMU.Операционная система отвечает за инициализацию MMU, а пользовательский процесс использует виртуальный адрес.

    Если память сравнить с гостиницей, то в гостинице 100 номеров, а операционная система является владельцем гостиницы. В гостиницу постоянно приезжают туристические группы, владелец гостиницы заявляет каждой туристической группе, что у нас 100 номеров, 10 из которых являются общежитиями для персонала, поэтому на каждую туристическую группу имеется 90 номеров.

    Члены туристической группы не могут найти номер самостоятельно.Они должны использовать карту, предоставленную отелем, чтобы найти соответствующий номер, но на самом деле карта в каждой карте путешествия отличается.Эта карта гарантирует, что гости никогда не найдут номер. карта, которая использовалась в других комнатах. После того, как гость заснул в комнате, трактирщик может переправить гостя на скрытый склад в трактире, чтобы комната снова освободилась, и трактирщик мог продолжать сдавать комнату другим. (Какой спекулянт)

    Как только кто-то из туристической группы начнет искать определенного пассажира, но пассажир был перемещен на склад, трактирщик быстро переместит человека со склада обратно в комнату, а затем изменит карту так, чтобы та же самая туристическая группа. может успешно найти путешественника в комнате.

  6. Зачем кучи? Чтобы сохранить глобальные переменные, сгенерированные программой во время выполнения
    • Сегмент данных: можно сохранять только переменные, сгенерированные во время компиляции.
    • Стек: вы можете сохранять переменные только в текущем методе
  7. Соглашение о вызовах системных вызовов несколько похоже на вызовы функций, но системные вызовы используют регистры вместо стека в качестве носителя для передачи параметров.
  8. Потому что системный вызов — это тоже функция по сути, так как это функция под x86, она должна использовать стек. Однако, как функция ядра, системный вызов не должен использовать стек пользовательского пространства, чтобы предотвратить доступ пользовательской программы. Поэтому нам нужно установить в ядре стек, который специально используется для выполнения функций системного вызова.Каждый процесс имеет набор стеков, а именно пользовательский стек и стек ядра., прежде чем функция системного вызова будет выполнена, необходимо переключиться с пользовательского стека на стек ядра.Переключение стека очень простое, просто нужно изменить значение esp.

Ссылаться на:

Самосовершенствование программиста
GitHub.com/1184893257/…
Компиляция языков высокого уровня: введение в процесс компоновки и загрузки
Почему двоичный исполняемый файл, скомпилированный языком golang, больше, чем у языка C?
Принцип распределения памяти в Linux — malloc/brk/mmap