Делаем отладчик в Голанге (часть III)
До сих пор мы научились выполнять пошаговую трассировку процесса (трассировку) и получать некоторую отладочную информацию из двоичного кода (прочтите ее здесь, если вы еще этого не сделали). Теперь пора установить точку останова в желаемом месте, подождать, пока она будет достигнута, а затем исследовать состояние процесса.
Начнем с кода сборки, который использовался ранее:
section .data msg db "hello, world!", 0xA len equ $ - msg section .text global _start _start: mov rax, 1 ; write syscall (https://linux.die.net/man/2/write) mov rdi, 1 ; stdout mov rsi, msg mov rdx, len ; Passing parameters to `syscall` instruction described in ; https://en.wikibooks.org/wiki/X86_Assembly/Interfacing_with_Linux#syscall syscall mov rax, 60 ; exit syscall (https://linux.die.net/man/2/exit) mov rdi, 0 ; exit code syscall
Цель установила точку останова на:
mov rdi, 1
поэтому ловушка будет активирована перед выполнением этой инструкции. Затем мы проверим, что регистр RDI хранит 0, выполним пошаговое выполнение и убедимся, что регистр хранит 1.
Точка останова
Для процессоров x86 существует инструкция INT, которая генерирует программное прерывание. Эти прерывания используются, например, выполнить системный вызов в Linux. x86–64 представила специальную инструкцию syscall, которая работает быстрее, поэтому я использовал ее выше, но мы могли добиться того же с помощью обычного int. Чтобы установить точку останова, мы будем использовать int 3
с кодом операции 0xCC
:
Инструкция INT 3 генерирует специальный однобайтовый код операции (CC), который предназначен для вызова обработчика исключений отладки. (Эта однобайтовая форма полезна, потому что ее можно использовать для замены первого байта любой инструкции точкой останова, включая другие однобайтовые инструкции, без перезаписи другого кода).
(Руководство разработчика программного обеспечения для архитектур Intel® 64 и IA-32)
Мы воспользуемся 0xCC
, чтобы заменить его инструкцией в нужном месте. Как только это место будет достигнуто, мы:
- исследовать состояние процесса,
- замените
0xCC
его исходным значением, - уменьшить счетчик программ на 1,
- выполнить один шаг.
Первый вопрос, куда поставить 0xCC
. Мы не знаем, где именно в памяти будет храниться инструкция mov rdi, 1
. Он будет на mov rax, 1
байтов больше, чем первая инструкция программы, поскольку это вторая инструкция. Длина инструкций на x86 может быть разной, что усложняет всю задачу. Расположение первой инструкции можно проверить, остановив программу перед обработкой любой инструкции (мы уже делали это раньше). Длину первой инструкции можно получить с помощью программы objdump:
> nasm -f elf64 -o hello.o src/github.com/mlowicki/hello/hello.asm && ld -o /go/bin/hello hello.o > objdump -d -M intel /go/bin/hello /go/bin/hello: file format elf64-x86-64 Disassembly of section .text: 00000000004000b0 <_start>: 4000b0: b8 01 00 00 00 mov eax,0x1 4000b5: bf 01 00 00 00 mov edi,0x1 4000ba: 48 be d8 00 60 00 00 movabs rsi,0x6000d8 4000c1: 00 00 00 4000c4: ba 0e 00 00 00 mov edx,0xe 4000c9: 0f 05 syscall 4000cb: b8 3c 00 00 00 mov eax,0x3c 4000d0: bf 00 00 00 00 mov edi,0x0 4000d5: 0f 05 syscall
Сверху мы видим, что размер первой инструкции составляет 5 байтов (4000b5 — 4000b0
). Итак, для адреса первой инструкции нам нужно добавить 5 байтов и поместить туда 0xCC
. Давайте посмотрим, как это работает:
package main import ( "flag" "log" "os" "os/exec" "syscall" ) func step(pid int) { err := syscall.PtraceSingleStep(pid) if err != nil { log.Fatal(err) } } func cont(pid int) { err := syscall.PtraceCont(pid, 0) if err != nil { log.Fatal(err) } } func setPC(pid int, pc uint64) { var regs syscall.PtraceRegs err := syscall.PtraceGetRegs(pid, ®s) if err != nil { log.Fatal(err) } regs.SetPC(pc) err = syscall.PtraceSetRegs(pid, ®s) if err != nil { log.Fatal(err) } } func getPC(pid int) uint64 { var regs syscall.PtraceRegs err := syscall.PtraceGetRegs(pid, ®s) if err != nil { log.Fatal(err) } return regs.PC() } func setBreakpoint(pid int, breakpoint uintptr) []byte { original := make([]byte, 1) _, err := syscall.PtracePeekData(pid, breakpoint, original) if err != nil { log.Fatal(err) } _, err = syscall.PtracePokeData(pid, breakpoint, []byte{0xCC}) if err != nil { log.Fatal(err) } return original } func clearBreakpoint(pid int, breakpoint uintptr, original []byte) { _, err := syscall.PtracePokeData(pid, breakpoint, original) if err != nil { log.Fatal(err) } } func printState(pid int) { var regs syscall.PtraceRegs err := syscall.PtraceGetRegs(pid, ®s) if err != nil { log.Fatal(err) } log.Printf("RAX=%d, RDI=%d\n", regs.Rax, regs.Rdi) } func main() { flag.Parse() input := flag.Arg(0) cmd := exec.Command(input) cmd.Args = []string{input} cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr cmd.SysProcAttr = &syscall.SysProcAttr{Ptrace: true} err := cmd.Start() if err != nil { log.Fatal(err) } err = cmd.Wait() log.Printf("State: %v\n", err) pid := cmd.Process.Pid breakpoint := uintptr(getPC(pid) + 5) original := setBreakpoint(pid, breakpoint) cont(pid) var ws syscall.WaitStatus _, err = syscall.Wait4(pid, &ws, syscall.WALL, nil) clearBreakpoint(pid, breakpoint, original) printState(pid) setPC(pid, uint64(breakpoint)) step(pid) _, err = syscall.Wait4(pid, &ws, syscall.WALL, nil) printState(pid) }
Сверху размещен набор помощников. Функции setPC и getPC предназначены для управления программным счетчиком. Стоит отметить, что регистр PC содержит следующую инструкцию, которая должна быть выполнена. Если мы останавливаем процесс перед запуском чего-либо, то ПК сохраняет адрес памяти, в который помещается первая инструкция программы. Функции для управления точками останова (setBreakpoint и clearBreakpoint) манипулируют памятью, вставляя 0xCC
или удаляя ее соответственно. Результат выглядит следующим образом:
> go install github.com/mlowicki/breakpoint > breakpoint /go/bin/hello 2017/07/16 21:06:33 State: stop signal: trace/breakpoint trap 2017/07/16 21:06:33 RAX=1, RDI=0 2017/07/16 21:06:33 RAX=1, RDI=1
Выглядит нормально. Когда процесс достигает ловушки, регистр RDI не устанавливается (содержит 0
). После одного шага (выполнения 2-й инструкции) регистр RDI принимает желаемое значение, установленное инструкцией:
mov rdi, 1
Мы выполнили задачу, поставленную в самом начале этой истории. Это требует дополнительной работы, такой как вычисление длины инструкций, но мы исправим это в какой-то момент, так что не беспокойтесь.
REPL
Пришло время создать базовую структуру нашего отладчика. Это простая программа, которая в цикле обрабатывает такие команды, как «установить точку останова в», «перейти на один шаг».
package main import ( "bufio" "flag" "fmt" "io" "log" "os" "os/exec" "strings" "syscall" ) func initTracee(path string) int { cmd := exec.Command(path) cmd.Args = []string{path} cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr cmd.SysProcAttr = &syscall.SysProcAttr{Ptrace: true} err := cmd.Start() if err != nil { log.Fatal(err) } err = cmd.Wait() // Process should be stopped here because of trace/breakpoint trap if err == nil { log.Fatal("Program exited") } return cmd.Process.Pid } func main() { flag.Parse() _ = initTracee(flag.Arg(0)) for { reader := bufio.NewReader(os.Stdin) fmt.Print("> ") command, err := reader.ReadString('\n') if err != nil { if err == io.EOF { fmt.Println() break } log.Fatal(err) } command = command[:len(command)-1] // get rid of ending newline character if strings.HasPrefix(command, "register ") { fmt.Println("register...") } else if strings.HasPrefix(command, "breakpoint ") { fmt.Println("breakpoint...") } else if command == "help" { fmt.Println("help...") } else if command == "step" { fmt.Println("step...") } else if command == "continue" { fmt.Println("continue") } else { fmt.Println("unknown command") } } }
Это основа нашего отладчика. Он не предоставляет слишком много (пока), но уже имеет необходимую логику для обеспечения элементарной среды REPL. Мы более или менее знаем, как обрабатывать команды step, continue, help и register, и мы реализуем их скоро пробелы. Для Golang не столь очевидна команда точки останова. В следующем посте будет подробно объяснено, почему это немного сложнее, чем ожидалось, и как преодолеть эту дополнительную сложность.
Нажмите ❤ ниже, чтобы помочь другим узнать эту историю. Пожалуйста, подпишитесь на меня, если вы хотите получать новости о новых сообщениях или ускорять работу над будущими историями.