这份调试器的示例代码来自《Learning Linux Binary Analysis》 书中第三章的“A simple ptrace-based debugger”一节,添加了一些调试打印和关于代码细节的注释。其他相关内容可查阅此书。
1#include <stdio.h>
2#include <string.h>
3#include <stdlib.h>
4#include <unistd.h>
5#include <fcntl.h>
6#include <errno.h>
7#include <signal.h>
8#include <elf.h>
9#include <sys/wait.h>
10#include <sys/types.h>
11#include <sys/user.h>
12#include <sys/stat.h>
13#include <sys/ptrace.h>
14#include <sys/mman.h>
15
16typedef struct handle {
17 Elf64_Ehdr *ehdr;
18 Elf64_Phdr *phdr;
19 Elf64_Shdr *shdr;
20 uint8_t *mem;
21 char *symname;
22 Elf64_Addr symaddr;
23 struct user_regs_struct pt_reg;
24 char *exec;
25} handle_t;
26
27Elf64_Addr lookup_symbol(handle_t *, const char *);
28
29int main(int argc, char **argv, char **envp)
30{
31 int fd;
32 handle_t h;
33 struct stat st;
34 long trap, orig;
35 int status, pid;
36 char * args[2]; // for child process's execv()
37
38 if (argc < 3) {
39 printf("Usage: %s <program> <function>\n", argv[0]);
40 exit(0);
41 }
42 if ((h.exec = strdup(argv[1])) == NULL) {
43 perror("strdup");
44 exit(-1);
45 }
46 args[0] = h.exec;
47 args[1] = NULL;
48 if ((h.symname = strdup(argv[2])) == NULL) {
49 perror("strdup");
50 exit(-1);
51 }
52 if ((fd = open(argv[1], O_RDONLY)) < 0) {
53 perror("open");
54 exit(-1);
55 }
56 if (fstat(fd, &st) < 0) {
57 perror("fstat");
58 exit(-1);
59 }
60
61 h.mem = mmap(NULL, st.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
62 if (h.mem == MAP_FAILED) {
63 perror("mmap");
64 exit(-1);
65 }
66 h.ehdr = (Elf64_Ehdr *)h.mem;
67 h.phdr = (Elf64_Phdr *)(h.mem + h.ehdr->e_phoff);
68 h.shdr = (Elf64_Shdr *)(h.mem + h.ehdr->e_shoff);
69 /*
70 * Using strcmp() here would cause undefined behavior,
71 * since &h.mem[1] is not terminated by NULL.
72 */
73 if (h.mem[0] != 0x7f || strncmp((char *)&h.mem[1], "ELF", 3)) {
74 printf("%s is not an ELF file\n", h.exec);
75 exit(-1);
76 }
77 if (h.ehdr->e_type != ET_EXEC) { // ET_DYN for WSL
78 printf("%s is not an ELF executable\n", h.exec);
79 exit(-1);
80 }
81 if (h.ehdr->e_shstrndx == 0 || h.ehdr->e_shoff == 0 || h.ehdr->e_shnum == 0) {
82 printf("Section header table not found\n");
83 exit(-1);
84 }
85 if ((h.symaddr = lookup_symbol(&h, h.symname)) == 0) {
86 printf("Unable to find symbol: %s not found in executable\n", h.symname);
87 exit(-1);
88 }
89 /*
90 * You may close the file right after mmap().
91 * https://stackoverflow.com/a/17490185
92 */
93 close(fd);
94
95 if ((pid = fork()) < 0) {
96 perror("fork");
97 exit(-1);
98 }
99 if (pid == 0) {
100 /* Make the child proc traceable (other args are ignored) */
101 if (ptrace(PTRACE_TRACEME, pid, NULL, NULL) < 0) {
102 perror("PTRACE_TRACEME");
103 exit(-1);
104 }
105 execve(h.exec, args, envp);
106 printf("Should never reach here\n");
107 exit(0);
108 }
109 wait(&status);
110
111 /*
112 * From execve(2):
113 * If the current program is being ptraced, a SIGTRAP signal is sent
114 * to it after a successful execve().
115 */
116 if (WIFSTOPPED(status) && WSTOPSIG(status) == SIGTRAP) {
117 printf("Yes the child process is indeed stopped by SIGTRAP\n");
118 }
119
120 printf("Beginning analysis of pid: %d at %lx\n", pid, h.symaddr);
121 /*
122 * Read (and backup) a word at target function address
123 * (in child's address space, same for the following comments so will be omitted)
124 */
125 if ((orig = ptrace(PTRACE_PEEKTEXT, pid, h.symaddr, NULL)) < 0) {
126 perror("PTRACE_PEEKTEXT");
127 exit(-1);
128 }
129 /*
130 * Prepare the code of "int 3", which stops the program's execution.
131 * x86 is a variable-length instruction set, and "int 3" is only one byte long.
132 */
133 trap = (orig & ~0xff) | 0xcc;
134 /* Write it back to replace the function code */
135 if (ptrace(PTRACE_POKETEXT, pid, h.symaddr, trap) < 0) {
136 perror("PTRACE_POKETEXT");
137 exit(-1);
138 }
139
140trace:
141 /* Let the paused child run */
142 if (ptrace(PTRACE_CONT, pid, NULL, NULL) < 0) {
143 perror("PTRACE_CONT");
144 exit(-1);
145 }
146 wait(&status);
147 if (WIFSTOPPED(status) && WSTOPSIG(status) == SIGTRAP) {
148 /* Get current values in registers */
149 if (ptrace(PTRACE_GETREGS, pid, NULL, &h.pt_reg) < 0) {
150 perror("PTRACE_GETREGS");
151 exit(-1);
152 }
153 printf("\nExecutable %s (pid: %d) has hit breakpoint 0x%lx\n", h.exec, pid, h.symaddr);
154 printf("%%rcx: %llx\n%%rdx: %llx\n%%rbx: %llx\n"
155 "%%rax: %llx\n%%rdi: %llx\n%%rsi: %llx\n"
156 "%%r8: %llx\n%%r9: %llx\n%%r10: %llx\n"
157 "%%r11: %llx\n%%r12: %llx\n%%r13: %llx\n"
158 "%%r14: %llx\n%%r15: %llx\n%%rsp: %llx\n",
159 h.pt_reg.rcx, h.pt_reg.rdx, h.pt_reg.rbx,
160 h.pt_reg.rax, h.pt_reg.rdi, h.pt_reg.rsi,
161 h.pt_reg.r8, h.pt_reg.r9, h.pt_reg.r10,
162 h.pt_reg.r11, h.pt_reg.r12, h.pt_reg.r13,
163 h.pt_reg.r14, h.pt_reg.r15, h.pt_reg.rsp);
164 printf("\nPlease hit any key to continue: ");
165 getchar();
166 /* Recover the original function code for further execution */
167 if (ptrace(PTRACE_POKETEXT, pid, h.symaddr, orig) < 0) {
168 perror("PTRACE_POKETEXT");
169 exit(-1);
170 }
171 /*
172 * "Take one step back", so that the function can be executed.
173 * Here 1 = length("int 3") = length(0xcc). For other arch, this number could be different.
174 */
175 h.pt_reg.rip = h.pt_reg.rip - 1;
176 /* Write all registers back to modify RIP */
177 if (ptrace(PTRACE_SETREGS, pid, NULL, &h.pt_reg) < 0) {
178 perror("PTRACE_SETREGS");
179 exit(-1);
180 }
181 /* Execute the function code that was replaced */
182 if (ptrace(PTRACE_SINGLESTEP, pid, NULL, NULL) < 0) {
183 perror("PTRACE_SINGLESTEP");
184 exit(-1);
185 }
186 /* Receive the SIGTRAP from PTRACE_SINGLESTEP and ignore it */
187 wait(NULL);
188 /* No practical effect, only to show how debugging could continue */
189 if (ptrace(PTRACE_POKETEXT, pid, h.symaddr, trap) < 0) {
190 perror("PTRACE_POKETEXT");
191 exit(-1);
192 }
193 goto trace;
194 }
195 if (WIFEXITED(status))
196 printf("Completed tracing pid: %d\n", pid);
197 exit(0);
198}
199
200Elf64_Addr lookup_symbol(handle_t *h, const char *symname)
201{
202 int i, j;
203 char *strtab;
204 Elf64_Sym *symtab;
205 for (i = 0; i < h->ehdr->e_shnum; i++){
206 if (h->shdr[i].sh_type == SHT_SYMTAB) {
207 /* Here it may presume the .strtab section always follows the .symtab section */
208 strtab = (char *)&h->mem[h->shdr[h->shdr[i].sh_link].sh_offset];
209 symtab = (Elf64_Sym *)&h->mem[h->shdr[i].sh_offset];
210 for (j = 0; j < h->shdr[i].sh_size/sizeof(Elf64_Sym); j++) {
211 if (strcmp(&strtab[symtab->st_name], symname) == 0)
212 return (symtab->st_value);
213 symtab++;
214 }
215 }
216 }
217 return 0;
218}
备注
1h.mem = mmap(NULL, st.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
Map the executable file into a random (and page-aligned) segment of memory chosen by the kernel, instructed by the first argument NULL
. PROT_READ
makes the memory read-only, and MAP_PRIVATE
indicates a private mapping, which means updates are not carried through to the underlying file.