Сопоставление исходного кода и машинного кода
В части 1 я рассмотрел использование системного вызова ptrace для установки точки останова в целевом исполняемом файле по адресу инструкции машинного кода, где мы хотим, чтобы он остановился. Если вы еще этого не читали, возможно, вам стоит взглянуть на него.
Но возникает важный вопрос: как узнать, где установить точку останова?
Как человек, я хотел бы иметь возможность установить точку останова в определенной строке в исходном коде, но ptrace дает нам механизм для установки точки останова на адрес инструкции машинного кода в памяти. Нам нужно сопоставить исходный код и соответствующие инструкции в памяти. Мы можем сделать это с помощью пакетов Go debug / elf и debug / gosym.
Эльф? Как у волшебных существ?
Прежде чем мы углубимся в код Go, давайте немного узнаем о формате исполняемого файла ELF. Существует исполняемый файл под названием readelf, который мы можем использовать для проверки исполняемых файлов Linux - различные параметры команды позволяют нам просматривать различные элементы файла ELF.
Сначала давайте посмотрим на заголовки ELF моего исполняемого файла hello.
$ readelf -h hello ELF Header: Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 Class: ELF64 Data: 2’s complement, little endian Version: 1 (current) OS/ABI: UNIX — System V ABI Version: 0 Type: EXEC (Executable file) Machine: Advanced Micro Devices X86–64 Version: 0x1 Entry point address: 0x456420 Start of program headers: 64 (bytes into file) Start of section headers: 456 (bytes into file) Flags: 0x0 Size of this header: 64 (bytes) Size of program headers: 56 (bytes) Number of program headers: 7 Size of section headers: 64 (bytes) Number of section headers: 23 Section header string table index: 3
Из частей, которые я выделил жирным шрифтом, мы видим, что
- hello - исполняемый файл
- первая инструкция для выполнения будет по адресу 0x456420, когда этот файл будет загружен в память
Мы также можем видеть, что есть некоторые «заголовки программ» и «заголовки разделов». Давайте воспользуемся readelf для просмотра заголовков разделов (большинство опущено для ясности):
readelf -S hello/hello There are 23 section headers, starting at offset 0x1c8: Section Headers: [Nr] Name Type Address Offset Size EntSize Flags Link Info Align [ 0] NULL 0000000000000000 00000000 0000000000000000 0000000000000000 0 0 0 … [ 6] .gosymtab PROGBITS 00000000004deae8 000deae8 0000000000000000 0000000000000000 A 0 0 1 [ 7] .gopclntab PROGBITS 00000000004deb00 000deb00 000000000005324b 0000000000000000 A 0 0 32 …
Нас особенно интересуют два заголовка, имена которых начинаются с .go - это таблица символов (раздел 6) и таблица строк (раздел 7).
Эльфы, но не гномы, сегодня
Как видите, раздел 6 с именем .gosymtab имеет нулевой размер (также выделен жирным шрифтом). Еще в очень ранних версиях Go .gosymtab использовался для хранения информации таблицы символов, но в Go 1.3 он был исключен (предположительно в пользу информации о формате Dwarf, хотя я не проверял, что это вот что случилось).
Раздел 7, называемый .gopclntab, более интересен. Он содержит соответствие между адресом счетчика программ и строками исходного кода.
Давайте прочитаем эту информацию из исполняемого файла hello в отладчик, который мы пишем на Go. Вышеупомянутый пакет debug / elf упрощает чтение информации из заголовка раздела .gopclntab. Для краткости я опустил обработку ошибок.
exe, _ := elf.Open(“hello”) lineTableData, _ := exe.Section(“.gopclntab”).Data() addr := exe.Section(“.text”).Addr lineTable := gosym.NewLineTable(lineTableData, addr) symTable := gosym.NewTable([]byte{}, lineTable)
Функция NewTable () принимает таблицу символов в качестве своего первого параметра, но мы можем просто передать пустой список байтов, поскольку мы знаем, что заголовок таблицы символов исполняемого файла имеет размер 0.
В symTable есть несколько полезных функций для сопоставления строки в файле исходного кода и соответствующей инструкции машинного кода.
Искать информацию о конкретной названной функции
Например, мы можем найти функцию main в пакете main исполняемого файла, который мы отлаживаем:
fn = symTable.LookupFunc(“main.main”) fmt.Printf(“function %s starts at %X\n”, fn.Name, fn.Entry)
Мы получаем структуру Func с информацией об этой функции, включая адрес первой инструкции машинного кода в ее коде. Когда функция вызывается, счетчик программ устанавливается на этот адрес, так что выполнение будет продолжено оттуда.
Найдите исходный код, соответствующий адресу машинного кода
Если pc - это адрес машинного кода:
file, line, fn = symTable.PCToLine(pc) address fmt.Printf(“function %s at line %d in file %s\n”, fn.Name, line, file)
Найдите адрес машинного кода, соответствующий строке в исходном коде
pc, fn, _ = symTable.LineToPC(file, line) fmt.Printf(“function %s at line %d in file %s\n”, fn.Name, line, file)
Поэтому, если мы хотим установить точку останова в определенной строке исходного кода, мы можем найти соответствующий адрес инструкции машинного кода с помощью LineToPC (). Это дает адрес, по которому мы можем написать инструкцию точки останова, как описано в Части 1.
После того, как мы остановились в точке останова, следующим шагом будет отображение стека в этой точке. Мы рассмотрим это в части 3, или вы можете узнать прямо сейчас, посмотрев видео ниже или в сопутствующем репозитории git.
Эта серия публикаций основана на моем выступлении, которое я впервые сделал на dotGo Paris, а недавно я сделал расширенную версию на GopherCon UK - вот видео (но на самом деле меня зовут Лиз, а не Луис!).