Skip to content

Latest commit

 

History

History

sem01-intro

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 
 
 
 
 

Инструментарий разработчика

GCC

Вопросы на повторение:

  • Что такое компилятор?
  • Зачем писать новые компиляторы?
  • Можно ли будет любую программу этого курса скомпилировать на Apple M1, например?
  • Какие есть этапы компиляции?

В это курсе мы будем использовать компилятор gcc. В простейшем случае, чтобы скомпилировать программу из одного файла, можно просто написать gcc file.c. Называться по-умолчанию выходной бинарный файл будет a.out. Переопределить это имя можно с помощью опции -o.

Этапы компиляции

Давайте рассмотрим этапы компиляции на примере простой программы aplusb.c.

  1. Препроцессинг: разворачиваются include и define, удаляем комментарии. Выполним только эту стадию: gcc -E aplusb.c -o aplusb_preprocessed.c

Что за комментарии препроцессор оставляет в начале? Хорошо написано здесь: https://stackoverflow.com/questions/49109904/how-to-interpret-prefixed-lines-in-c-preprocessor-output

  1. Компиляция: код на C превращается в ASM

На самом деле ASM не обозначает какой-то один язык. Это семейство низкоуровневых платформоспецифичных языков. На курсе мы с вами будем разбирать ARM и x86 диалекты, но есть и множество других для менее известных архитектур.

gcc -S aplusb_preprocessed.c -o aplusb_asm.S

  1. Ассемблирование: код на ассемблере преобразуется в машинный код.

gcc -c aplusb_asm.S -o aplusb_object.o

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

  1. Компоновка/Линковка: объектные файлы собираются в один бинарный (на этом этапе разрешаются зависимости, например glibc)

gcc aplusb_object.o -o aplusb_executable.out

Makefile

Удобно выполнять сборку одно командой gcc в случае, если надо собрать проекты из пары файлов. Для больших проектов нужен некий инструмент автоматизации, системы сборки. Одна из простейших из них -- make. Makefile состоит из целей, которые берут некоторые зависимости и применяют к ним команды сборки.

Основной формат (в качетсве отступа используется именно табуляция)

target: prerequisite1 ... prerequisite_n
    command_1
    ...
    command_n

В качестве примера можно посмотреть на Makefile в этой папке.

Make не хранит своё состояние. Поэтому пересборка цели не выполняется лишь в одном случае: имя цели является файлом, и время его модификации больше времени модификации всех зависимостей цели.

Мы обсудили простейший сценарий. На самом деле, Make поддерживает циклы, функции и так далее. Подробнее про это можно почитать на https://makefiletutorial.com/. Ещё подробнее в официальной документации: https://www.gnu.org/software/make/manual/make.html

GDB

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

gcc -g aplusb.c -o aplusb_debug.out

Запустим бинарный файл под отладчиком gdb

gdb ./aplusb_debug.out

Поставим точку останова: b 7

Запустим исполнение: run.

С помощью layout src можно посмотреть на исходный код.

Дальше с помощью ni можно переключаться на следующую инструкцию (если нужно зайти в функции, используем si).

С помощью print можно вывести значения переменной.

Чтобы выйти из отладки, можно использовать команду q.

Шпаргалка по GDB https://darkdust.net/files/GDB%20Cheat%20Sheet.pdf

Попробуем под gdb запустить падающую программу.

gcc -g bad.c -o bad.out

Чтобы сразу выполнить программу, можно передать команды gdb в аргумент ex.

gdb -ex=r ./bad.out

Видим строчку, в которой произошёл Segfault.

С помощью команды bt можно вывести stacktrace.

Дальшё с помощью frame 0 можно переключиться на верхний фрейм.

info args покажет аргументы, с которыми была вызвана функция.

info locals выведет локальные переменные.

В прошлых двух мы тоже вышли за границы массива, но почему-то не упали. Узнаете, почему, когда сходите на лекцию про виртуальную память процесса.

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

Попросим складывать файлы в текущую папку:

sudo sysctl kernel.core_pattern="./coredump"

Разрешим создавать coredump файлы большого размера и выполним программу.

ulimit -c unlimited; ./bad.out

Теперь можно проанализировать coredump.

gdb ./bad.out ./coredump

Ну а дальше можно повторить все манипуляции, которые мы делали ранее.

Sanitizers

Обычно проблемы целесообразно ловить на как можно более ранних этапах. В прошлом пункте мы разобрались, как можно проанализировать упавшую программу, в этом разбёремся, как частично отлавливать их на этапе компиляции. Adress Sanitizer позволяет отлавливать выход за границы массива, утечки памяти и некоторые другие ошибки. Подробнее можно почитать по ссылке.

gcc -fsanitize=address bad.c -o bad.out

Можно заметить, что размер бинарного файла увеличился, потому что сгенерировались некоторые дополнительные проверки. Исполнение из-за них также замедлится примерно в 2 раза, но это допустимо для отладочной сборки Зато теперь, если мы запустим программу, при падении увидим сильно более понятное сообщение об ошибке.

Strace

На лекции рассказали про то, что ядро предоставляет некоторый API системных вызовов. Они используются, например, для взаимодействия с железом, создания процессов и межпроцессорной коммуникации. С помощью утилиты strace можно посмотреть список системных вызовов, которые вызывает ваша программа.

strace ./bad.out

В начале мы выделяем память, потом загружаем в неё динамические библиотеки, после этого и записываем данные с помощью системных вызова write, и, наконец, завершаем программу с помощью системного вызова exit_group.