Сопоставление исходного кода и машинного кода

В части 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 - вот видео (но на самом деле меня зовут Лиз, а не Луис!).