11.6 Профилирование и покрытие кода в автономных средах
Если ваше приложение работает в среде с операционной системой, например GNU/Linux, этот раздел, вероятно, не имеет к вам отношения. Он предназначен для разработчиков приложений для автономных сред (например, встроенных систем) с ограниченными ресурсами. В частности, он посвящён системам или тестовым сценариям, в которых не поддерживаются конструкторы и деструкторы или файловый ввод-вывод библиотеки C. В этом разделе целевая система запускает ваше приложение, инструментированное для профилирования или сбора данных о покрытии кода. Вы разрабатываете и анализируете приложение в хост-системе. Сначала мы расскажем, как в таком сценарии можно получить данные профилирования и покрытия кода, а затем приведём руководство, которое можно выполнить в хост-системе. В заключение перечислим некоторые особенности инициализации системы.
11.6.1 Обзор
Для приложения, инструментированного для профилирования или сбора данных о покрытии кода, компилятор генерирует глобальные структуры данных, которые обновляются инструментирующим кодом во время выполнения приложения. Эти структуры данных называются данными gcov. Обычно при завершении работы приложения данные gcov сохраняются в файлы .gcda. Для каждой единицы трансляции, инструментированной для профилирования или сбора данных о покрытии кода, создаётся отдельный файл. Функция __gcov_exit(), сохраняющая данные gcov в файл, вызывается глобальной функцией-деструктором для каждой единицы трансляции, инструментированной для профилирования или сбора данных о покрытии кода. Она выполняется при завершении процесса. В глобальной функции-конструкторе вызывается функция __gcov_init(), регистрирующая данные gcov единицы трансляции в глобальном списке. В некоторых ситуациях этот способ не работает. Во-первых, если вы хотите профилировать глобальные конструкторы или обработку завершения операционной системы, сгенерированные компилятором функции могут помешать достижению целей тестирования. Во-вторых, может потребоваться тестирование ранних этапов инициализации системы или нестандартного поведения программы, при котором невозможно использовать глобальный конструктор или обработку завершения. В-третьих, для хранения файлов нужна файловая система.
Параметр GCC -fprofile-info-section позволяет использовать профилирование и сбор данных о покрытии кода в автономных средах. Этот параметр отключает использование глобальных конструкторов и деструкторов для данных gcov. Вместо этого указатель на данные gcov сохраняется в специальной секции входных данных компоновщика для каждой единицы трансляции, скомпилированной с этим параметром. По умолчанию секция называется .gcov_info. Данные gcov статически инициализируются. Компоновщик может собрать указатели на данные gcov всех единиц трансляции исполняемого файла в непрерывный блок памяти. Для этого с компоновщиком GNU можно использовать приведённое ниже определение выходной секции сценария компоновщика:
.gcov_info :
{
PROVIDE (__gcov_info_start = .);
KEEP (*(.gcov_info))
PROVIDE (__gcov_info_end = .);
}
Компоновщик предоставит два глобальных символа — __gcov_info_start и __gcov_info_end, которые задают соответственно начало и конец массива указателей на блоки данных gcov. Директива KEEP () необходима, чтобы предотвратить удаление указателей при сборке мусора. На них нет прямых ссылок из исполняемого файла. Секцию можно разместить в области памяти только для чтения.
Чтобы передать данные профилирования и покрытия кода с целевой системы в хост-систему, приложение должно предоставлять функцию для надёжной передачи упорядоченного потока байтов с целевой системы на хост. Поток байтов можно сжать и закодировать с помощью кодов обнаружения и исправления ошибок в соответствии с требованиями конкретного приложения. Библиотека GCC libgcov для целевой системы предоставляет две функции — __gcov_info_to_gcda() и __gcov_filename_to_gcfn(), — которые создают поток байтов из блока данных gcov. Объявления функций находятся в #include <gcov.h>. Поток байтов можно десериализовать с помощью подкоманды merge-stream команды gcov-tool, чтобы создать или обновить файлы .gcda в файловой системе хоста для инструментированного приложения.
11.6.2 Руководство
Это руководство следует выполнять в хост-системе. Мы соберём программу, инструментированную для сбора данных о покрытии кода. Программа запускает приложение и выводит данные gcov в stderr, закодированные в виде потока печатных символов. Приложение просто декодирует такие потоки символов из stdin и записывает декодированный поток символов в stdout (предупреждение: это двоичные данные). Декодированный поток символов обрабатывается подкомандой merge-stream команды gcov-tool, которая создаёт или обновляет файлы .gcda.
Для начала создайте пустой каталог и перейдите в него. Затем создайте в этом каталоге следующие три файла:
- app.h — заголовочный файл, включаемый в app.c и main.c,
- app.c — исходный файл с примером приложения,
- main.c — исходный файл с главной функцией программы и кодом для вывода данных gcov.
Сначала создайте заголовочный файл app.h со следующим содержимым:
static const unsigned char a = 'a';
static inline unsigned char *
encode (unsigned char c, unsigned char buf[2])
{
buf[0] = c % 16 + a;
buf[1] = (c / 16) % 16 + a;
return buf;
}
extern void application (void);
Затем создайте исходный файл app.c со следующим содержимым:
#include "app.h"
#include <stdio.h>
/* The application reads a character stream encoded by encode() from stdin,
decodes it, and writes the decoded characters to stdout. Characters other
than the 16 characters 'a' to 'p' are ignored. */
static int can_decode (unsigned char c)
{
return (unsigned char)(c - a) < 16;
}
void
application (void)
{
int first = 1;
int i;
unsigned char c;
while ((i = fgetc (stdin)) != EOF)
{
unsigned char x = (unsigned char)i;
if (can_decode (x))
{
if (first)
c = x - a;
else
fputc (c + 16 * (x - a), stdout);
first = !first;
}
else
first = 1;
}
}
Наконец, создайте исходный файл main.c со следующим содержимым:
#include "app.h"
#include <gcov.h>
#include <stdio.h>
#include <stdlib.h>
/* The start and end symbols are provided by the linker script. We use the
array notation to avoid issues with a potential small-data area. */
extern const struct gcov_info *const __gcov_info_start[];
extern const struct gcov_info *const __gcov_info_end[];
/* This function shall produce a reliable in order byte stream to transfer the
gcov information from the target to the host system. */
static void
dump (const void *d, unsigned n, void *arg)
{
(void)arg;
const unsigned char *c = d;
unsigned char buf[2];
for (unsigned i = 0; i < n; ++i)
fwrite (encode (c[i], buf), sizeof (buf), 1, stderr);
}
/* The filename is serialized to a gcfn data stream by the
__gcov_filename_to_gcfn() function. The gcfn data is used by the
"merge-stream" subcommand of the "gcov-tool" to figure out the filename
associated with the gcov information. */
static void
filename (const char *f, void *arg)
{
__gcov_filename_to_gcfn (f, dump, arg);
}
/* The __gcov_info_to_gcda() function may have to allocate memory under
certain conditions. Simply try it out if it is needed for your application
or not. */
static void *
allocate (unsigned length, void *arg)
{
(void)arg;
return malloc (length);
}
/* Dump the gcov information of all translation units. */
static void
dump_gcov_info (void)
{
const struct gcov_info *const *info = __gcov_info_start;
const struct gcov_info *const *end = __gcov_info_end;
/* Obfuscate variable to prevent compiler optimizations. */
__asm__ ("" : "+r" (info));
while (info != end)
{
void *arg = NULL;
__gcov_info_to_gcda (*info, filename, dump, allocate, arg);
fputc ('\n', stderr);
++info;
}
}
/* The main() function just runs the application and then dumps the gcov
information to stderr. */
int
main (void)
{
application ();
dump_gcov_info ();
return 0;
}
Если скомпилировать app.c для сбора данных о покрытии кода без дополнительных параметров профилирования, то для регистрации и вывода данных gcov используются глобальный конструктор (здесь это _sub_I_00100_0; в вашей среде его имя может отличаться) и деструктор (_sub_D_00100_1) соответственно. Также видны неопределённые ссылки на __gcov_init и __gcov_exit:
$ gcc --coverage -c app.c
$ nm app.o
0000000000000000 r a
0000000000000030 T application
0000000000000000 t can_decode
U fgetc
U fputc
0000000000000000 b __gcov0.application
0000000000000038 b __gcov0.can_decode
0000000000000000 d __gcov_.application
00000000000000c0 d __gcov_.can_decode
U __gcov_exit
U __gcov_init
U __gcov_merge_add
U stdin
U stdout
0000000000000161 t _sub_D_00100_1
0000000000000151 t _sub_I_00100_0
Скомпилируйте app.c и main.c для сбора данных о покрытии кода с параметром -fprofile-info-section. Теперь в секции .gcov_info присутствует объект размером с указатель, доступный только для чтения, а неопределённых ссылок на __gcov_init и __gcov_exit нет:
$ gcc --coverage -fprofile-info-section -c main.c
$ gcc --coverage -fprofile-info-section -c app.c
$ objdump -h app.o
app.o: file format elf64-x86-64
Sections:
Idx Name Size VMA LMA File off Algn
0 .text 00000151 0000000000000000 0000000000000000 00000040 2**0
CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
1 .data 00000100 0000000000000000 0000000000000000 000001a0 2**5
CONTENTS, ALLOC, LOAD, RELOC, DATA
2 .bss 00000040 0000000000000000 0000000000000000 000002a0 2**5
ALLOC
3 .rodata 0000003c 0000000000000000 0000000000000000 000002a0 2**3
CONTENTS, ALLOC, LOAD, READONLY, DATA
4 .gcov_info 00000008 0000000000000000 0000000000000000 000002e0 2**3
CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA
5 .comment 0000004e 0000000000000000 0000000000000000 000002e8 2**0
CONTENTS, READONLY
6 .note.GNU-stack 00000000 0000000000000000 0000000000000000 00000336 2**0
CONTENTS, READONLY
7 .eh_frame 00000058 0000000000000000 0000000000000000 00000338 2**3
CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA
Необходимо настроить процедуру компоновки программы так, чтобы все входные секции компоновщика .gcov_info размещались в непрерывном блоке памяти с символами начала и конца. Сначала получите сценарий компоновщика по умолчанию с помощью следующих команд (предполагается, что используется компоновщик GNU):
$ ld --verbose | sed '1,/^===/d' | sed '/^===/d' > linkcmds
Затем откройте файл linkcmds в текстовом редакторе и поместите определение выходной секции компоновщика из обзора после определения секции .rodata. Скомпонуйте исполняемый файл программы с помощью изменённого сценария компоновщика:
$ gcc --coverage main.o app.o -T linkcmds -Wl,-Map,app.map
В файле карты компоновщика app.map видно, что компоновщик поместил объекты размером с указатель, доступные только для чтения, из объектных файлов main.o и app.o в непрерывный блок памяти и предоставил символы __gcov_info_start и __gcov_info_end:
$ grep -C 1 "\.gcov_info" app.map
.gcov_info 0x0000000000403ac0 0x10
0x0000000000403ac0 PROVIDE (__gcov_info_start = .)
*(.gcov_info)
.gcov_info 0x0000000000403ac0 0x8 main.o
.gcov_info 0x0000000000403ac8 0x8 app.o
0x0000000000403ad0 PROVIDE (__gcov_info_end = .)
Убедитесь, что файлов .gcda нет. Запустите программу, не передавая ей данные для декодирования, и перенаправьте stderr в файл gcda-0.txt (первый запуск). Запустите программу для декодирования gcda-0.txt и передайте результат команде gcov-tool с помощью подкоманды merge-stream, чтобы создать файлы .gcda (второй запуск). Запустите gcov, чтобы создать отчёт для app.c. Видно, что после первого запуска без данных для декодирования приложение покрыто лишь частично:
$ rm -f app.gcda main.gcda $ echo "" | ./a.out 2>gcda-0.txt $ ./a.out <gcda-0.txt 2>gcda-1.txt | gcov-tool merge-stream $ gcov -bc app.c File 'app.c' Lines executed:69.23% of 13 Branches executed:66.67% of 6 Taken at least once:50.00% of 6 Calls executed:66.67% of 3 Creating 'app.c.gcov' Lines executed:69.23% of 13
Запустите программу для декодирования gcda-1.txt и передайте результат команде gcov-tool с помощью подкоманды merge-stream, чтобы обновить файлы .gcda. Запустите gcov, чтобы создать отчёт для app.c. Поскольку при втором запуске были декодированы данные gcov первого запуска, теперь приложение покрыто полностью:
$ ./a.out <gcda-1.txt 2>gcda-2.txt | gcov-tool merge-stream $ gcov -bc app.c File 'app.c' Lines executed:100.00% of 13 Branches executed:100.00% of 6 Taken at least once:100.00% of 6 Calls executed:100.00% of 3 Creating 'app.c.gcov' Lines executed:100.00% of 13
11.6.3 Особенности инициализации системы
Данные gcov единицы трансляции состоят из нескольких глобальных структур данных. Например, инструментированный код может обновлять счётчики рёбер графа потока управления программы в структуре данных, изначально заполненной нулями. Инструментированный код можно безопасно запускать до обнуления этих данных. Информация о покрытии, полученная до обнуления этих данных, непригодна для использования. Вывод данных gcov с помощью __gcov_info_to_gcda() до обнуления этих данных или загрузки инициализированных данных приводит к неопределённому поведению. Обнуление этих данных функцией, инструментированной для профилирования или сбора данных о покрытии кода, также приводит к неопределённому поведению, поскольку, например, это может привести к несогласованности счётчиков рёбер графа потока управления программы.
© Free Software Foundation
Licensed under the GNU Free Documentation License, Version 1.3.
https://gcc.gnu.org/onlinedocs/gcc-15.3.0/gcc/Freestanding-Environments.html