1

I work on making a mini debugger as a personal project. the debugger is for x86 processors under GNU/Linux environment.

In my approach of setting a breakpoint at a specific address of the debugged program works for the first time i lunch my debugger. if i try it again it fails.

I want to make sure if i do this in a right way.

suppose i have a helloworld program to be debugged as following:

#include <iostream>
int main()
{
    std::cerr << "hello,World\n";
return 0;
}

and it is dissembled with objdump -d as following

0000000000400907 <main>:
400907: 55                      push   %rbp
  400908:   48 89 e5                mov    %rsp,%rbp
  40090b:   be 04 0a 40 00          mov    $0x400a04,%esi
  400910:   bf 60 10 60 00          mov    $0x601060,%edi
  400915:   e8 06 ff ff ff          callq  400820 <_ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc@plt>
  40091a:   b8 00 00 00 00          mov    $0x0,%eax
  40091f:   5d                      pop    %rbp
  400920:   c3                      retq 

and for sure i compiled my helloworld with -g option.

for the first trial, i was setting a breakpoint at 0x400915 and at 0x40091a . so what i was expecting is that when i continue(the command like GDB) my debugger, it will stop at 0x400915 and when i continue again it will print "helloworld" but non of that happened. it just go to the return as there was no any breakpoints.

So as another trial, when my debugger starts my helloworld program. i see the pid of helloworld program and open its maps file at /proc/[helloworld pid]/maps which looks like

00400000-00401000 r-xp 00000000 08:07 1578540                            /home/yahia/mytinydebugger/tdbg/build/debugging-examples/helloworld
00600000-00602000 rw-p 00000000 08:07 1578540                            /home/yahia/mytinydebugger/tdbg/build/debugging-examples/helloworld
7f9483434000-7f9483457000 r-xp 00000000 08:06 529041                     /lib/x86_64-linux-gnu/ld-2.19.so
7f9483656000-7f9483658000 rw-p 00022000 08:06 529041                     /lib/x86_64-linux-gnu/ld-2.19.so
7f9483658000-7f9483659000 rw-p 00000000 00:00 0 
7fff5a314000-7fff5a335000 rw-p 00000000 00:00 0                          [stack]
7fff5a335000-7fff5a337000 r--p 00000000 00:00 0                          [vvar]
7fff5a337000-7fff5a339000 r-xp 00000000 00:00 0                          [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0                  [vsyscall]

then focusing on the first address of the stack which is

7fff5a314000-7fff5a335000 rw-p 00000000 00:00 0                          [stack

and adding 0x7fff5a314000 + 0x400915 = 0x7FFF5A714915 and the same for 0x40091a . it worked for the first time as i expected but when i tried it again it doesn't work. i even automated doing this hex math in the code by fetching it from /proc/[helloworld pid]/maps automatically. it seems it works right for the first time and whatever i try again it doesn'.

I don't know why it doesn't work always.

is my approach of setting a breakpoint by second trial is right?

I am aware that i should place INT3 instruction of intel at address i want to have the breakpoint. this is the code i use for setting the breakpoint.

void breakpoint::enable() {
    // Fetch the program instruction at the desired address of a specific process.
    auto data = ptrace(PTRACE_PEEKDATA, m_pid, m_addr, nullptr);
    // Save the lower byte which will be replaced with INT3 instruction.
    m_saved_data = static_cast<uint8_t>(data & 0xff);
    uint64_t int3 = 0xcc;
    // Set the lower byte to INT3 instruction
    uint64_t data_with_int3 = ((data & ~0xff) | int3);
    // Push the modified instruction with the breakpoint to the same address it was fetched.
    ptrace(PTRACE_POKEDATA, m_pid, m_addr, data_with_int3);
    // Enable that (this) object of the class has a breakpoint at [m_addr] of [m_pid] process.
    m_enabled = true;
}

the full code of the debugger is here. it is not big. it is simple with few commits as it is in its early age.

my OS is ubuntu 14.04 and both the debugger and the helloworld example is compiled with g++ 7.2.0

Yahia Farghaly
  • 857
  • 2
  • 10
  • 20

2 Answers2

3

First of all, you should use personality with ADDR_NO_RANDOMIZE to get predictable addresses, just like GDB does by default. This will help you with figuring out what is going on. You have not enabled PIE, though, so ASLR is not the cause of your difficulties for setting the breakpoint in the main program.

Then you need to check the result of the ptrace function for errors. You need to set errno to zero before the call and check if it has changed afterwards. This will tell you (within reason) whether you have passed a completely bogus address.

Also note that ptrace with PTRACE_PEEKDATA and PTRACE_POKEDATA always patches complete words. In order to patch a single byte, you need to use shifts and masking operations to put the breakpoint instruction into the right byte within the word.

Florian Weimer
  • 32,022
  • 3
  • 48
  • 92
  • I tried personality i used it at my debugger start, is this the correct usage or i need to use before ptrace(peekdata,..)? but it doesn't help. i checked the error of ptrace, it returns me EIO when using trial 2. the trial 1 still fails with personallity or without. – Yahia Farghaly Oct 22 '17 at 19:06
  • You need to set the personality value before the `execve` system call which creates the new process. – Florian Weimer Oct 22 '17 at 19:18
0

I fixed the issue by restoring the instruction which was corrupted by INT3 instruction with other changes as showing in my merge request in my repository.

Yahia Farghaly
  • 857
  • 2
  • 10
  • 20