Skip to content

高级调试技术

《Debug Hacks--深入调试的技术和工具》 - 吉冈弘隆,第四章和第六章的读书比较,本文中所有代码可在GitHub仓库中找到

GOT和PLT

工作原理

  • GOT(Global Offset Table)
    • GOT是保存库函数地址的区域,程序运行时,用到的库函数地址会设置到该区域
  • PLT(Procedure Linkage Table)
    • PLT时调用库函数时的小型代码集合,程序可以像调用自己的用户函数一样调用这些小型代码。这些代码只是跳转到GOT中设置的值而已。如果GOT中尚未设置调用函数的地址,就将地址设置到GOT中再跳转。

动态库函数每次运行的地址都不同,利用GOT和PLT机制,进程可将库函数看作一个固定不变的地址,详情可参考文章"动态链接"

实例

示例代码的汇编代码中,

  • callq 0x555555555050 <foo@plt>
    • 通过PLT技术调用库函数foo
  • 0x555555555050foo@plt地址
    • 对应的汇编代码第一句是跳转指令,跳转地址保存在0x555555557fd0地址中(代码中的*是取值的意思)
  • 0x555555557fd0foo@got.plt地址
    • 如果GOT尚未设置过,后面的指令会将库函数foo的地址填入此地址
    • 如果GOT已经设置过,会直接跳转至库函数foo,如下面的的地址0x7ffff7fc30f9
  • 由于foo库函数的PLT地址是固定的,从主进程的角度看,其调用的foo库函数的地址,每次运行都不变,都是0x555555555050
    (gdb) disas main
    Dump of assembler code for function main:
    => 0x0000555555555158 <+0>:     endbr64 
       0x000055555555515c <+4>:     push   %rbp
       0x000055555555515d <+5>:     mov    %rsp,%rbp
       0x0000555555555160 <+8>:     sub    $0x10,%rsp
       0x0000555555555164 <+12>:    callq  0x555555555149 <bar>
       0x0000555555555169 <+17>:    mov    %eax,-0xc(%rbp)
       0x000055555555516c <+20>:    mov    $0x0,%eax
       0x0000555555555171 <+25>:    callq  0x555555555050 <foo@plt>
       0x0000555555555176 <+30>:    mov    %eax,-0x8(%rbp)
       0x0000555555555179 <+33>:    mov    -0xc(%rbp),%edx
       0x000055555555517c <+36>:    mov    -0x8(%rbp),%eax
       0x000055555555517f <+39>:    add    %edx,%eax
       0x0000555555555181 <+41>:    mov    %eax,-0x4(%rbp)
       0x0000555555555184 <+44>:    mov    $0x0,%eax
       0x0000555555555189 <+49>:    leaveq 
       0x000055555555518a <+50>:    retq 
    
    (gdb) disas 0x555555555050
    Dump of assembler code for function rand@plt:
       0x0000555555555050 <+0>:     endbr64 
       0x0000555555555054 <+4>:     bnd jmpq *0x2f75(%rip)        # 0x555555557fd0 <foo@got.plt>
       0x000055555555505b <+11>:    nopl   0x0(%rax,%rax,1)
    
    (gdb) p/x *(uint64_t*)0x555555557fd0
    $1 = 0x7ffff7fc30f9
    # 和foo@got.plt的内容相同
    (gdb) p/x &foo
    $2 = 0x7ffff7fc30f9
    

strace

strace能够跟踪进程使用的系统调用,并显示其内容。例如,下面的代码会因为没有文件权限而出错。

int main(void)
{
   FILE *fp;
   fp = fopen("/etc/shadow", "r");
   if (fp == NULL)
   {
      printf("Error!\n");
      return EXIT_FAILURE;
   }
   return EXIT_SUCCESS;
}

利用strace ./main,可找到出错的原因:

...
openat(AT_FDCWD, "/etc/shadow", O_RDONLY) = -1 EACCES (Permission denied)
fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(0x88, 0x3), ...}) = 0
write(1, "Error!\n", 7Error!)                 = 7
...

结合GDB详细调查

通过strace -i可以查看系统调用的地址,因此可以在此地址上用GDB断点调试。为了让每次运行的地址保持不变,需要通过setarch -R命令,关闭地址空间布局随机化(Address Space Layout Randomization, ASLR)功能。

例如,例子"strace",通过命令setarch -R关闭地址空间布局随机化后,openat的地址是0x7ffff7ecfd1b。通过GDB断点命令b *0x7ffff7ecfd1b,可知此位置是"open64.c:48"。从而,可以很方便地在strace相关的位置打上断点。

$ setarch -R
$ strace -i ./main
...
[00007ffff7ed625b] brk(0x55555557a000)  = 0x55555557a000
[00007ffff7ecfd1b] openat(AT_FDCWD, "/etc/shadow", O_RDONLY) = -1 EACCES (Permission denied)
[00007ffff7ecf4f9] fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(0x88, 0x3), ...}) = 0
[00007ffff7ed0057] write(1, "Error!\n", 7Error!) = 7
...

$ gdb ./main
(gdb) start
Temporary breakpoint 1 at 0x1169: file main.c, line 5.
Starting program: /home/yuxiangw/GitHub/learning_book/docs/booknotes/debug_hacks/advance/code/strace/main 

Temporary breakpoint 1, main () at main.c:5
5       {
(gdb) b *0x7ffff7ecfd1b
Breakpoint 2 at 0x7ffff7ecfd1b: file ../sysdeps/unix/sysv/linux/open64.c, line 48.
(gdb) c
Continuing.

Breakpoint 2, 0x00007ffff7ecfd1b in __libc_open64 (file=0x555555556006 "/etc/shadow", oflag=0)
    at ../sysdeps/unix/sysv/linux/open64.c:48
48      ../sysdeps/unix/sysv/linux/open64.c: No such file or directory.