Chinaunix首页 | 论坛 | 博客
  • 博客访问: 2358653
  • 博文数量: 527
  • 博客积分: 10343
  • 博客等级: 上将
  • 技术积分: 5565
  • 用 户 组: 普通用户
  • 注册时间: 2005-07-26 23:05
文章分类

全部博文(527)

文章存档

2014年(4)

2012年(13)

2011年(19)

2010年(91)

2009年(136)

2008年(142)

2007年(80)

2006年(29)

2005年(13)

我的朋友

分类: LINUX

2010-05-28 11:52:01

在真实的程序执行环境下, 一个进程可能不能通过gdb filename的方式来调试, 原因:
1. 父进程为它准备了socket, 尤其是socketpair 这样的调用, 指望父子进程根据这对socket通讯
2. 父进程打开了一些文件.

所有继承自父进程的资源, 在直接运行程序时不会出现.

所以, 需要有一种方法, 在另一程序fork / execl一个新程序时, 有机会在该程序的最开始比如main断住.

假设程序为test2,
mv test2 test2.real
cat > test2 <#!/bin/bash

while [ -z "$GDB_READY" ] ; do
        echo sleep 1 second...
        export FORCE_REFRESH_ENV=1
        sleep 1
        export -n FORCE_REFRESH_ENV
done

exec ./test2.real "$@"
END

这段脚本偷梁换柱取代了原来的ELF格式可执行文件, 在它被执行时, 不停地循环, 循环的条件是检测一个叫
GDB_READY的环境变量.

这样, 在该程序被调用时, 相当于阻塞在了exec调用上, 原因是该环境变量不存在.

在另一终端中, 用ps ax | grep test2 找到PID,
gdb -p PID
调试该bash 进程. 通过
p setenv("GDB_READY", "1")

为该进程设置环境变量, 从而退出上面的while循环, 在设置之后, 先不能急于continue, 因为断点还没设置.

symbol-file ./test2.real -readnow
可以在尚未load该进程时, 提前告之gdb,
然后可以:
b test2.c:main
set follow-fork-mode child

continue

会在该进程的main处断住.

bash脚本中FORCE_REFRESH_ENV 是最让人迷惑的地方, 如果没有export + export -n, 整个方案就不行, 这是因为bash会对环境变量作一个cache, 这两个动作保证能引发它刷新该cache, 这样下一次循环时才能判断出变量GDB_READY 是否真正设置了.

bash对 $GDB_READY的实现并不是直接调用 getenv, 而是在一个内部的hash中去找.

======================================================
看来bash对环境变量的处理不象我想象的那么简单.

上述步骤有时有效, 有时无效, 无效的原因是环境变量仍未得到更新.
曾经尝试在sleep 调用处额外调用一个
bash -c 'echo ENV=$GDB_READY'
因为这样的调用bash若要保证正确性一定要刷新一遍env变量, 试验后可以, 但不能保证永远是正确的.



以上是bash脚本不能正确获取环境变量时的情境. 总之, 何时同步进程的环境变量到bash的内部hash, 现在看来不象想象中那么简单可以通过一些操作准确地触发

最后, 我找到了一个不需要手工去设置symbol-file的办法, 只需要在__libc_start_main上设置一个断点, 这个符号是libc的导出符号, 所以即使没有libc的debug info也总是可以在其上设置断点的, 在此处断住, 主程序的main还没到, 而symbol 已经由gdb自动装入, 可以设置断点了.

这个办法更简洁有效. 但仍需要使用前面的环境变量.

其实一个利用文件的略加变通的办法即可避开环境变量:

#!/bin/bash

echo 0 > ~/gdb_ready.txt
while [ "$(cat ~/gdb_ready.txt)" != "1" ] ; do
        sleep 1
done

exec ./test2.real "$@"
END

脚本总是将该文件内容设置为0, 这样进入死循环, 直到在gdb 中, 通过命令
shell echo 1 > ~/gdb_ready.txt
continue
才可继续执行到最终程序, 仍可配置上面的 __libc_start_main 办法.

另一个可行的方案:
#!/bin/bash
LD_PRELOAD=/home/zhao/my_preload.so exec ./test2.real "$@"

这个办法中, 需要一个额外的my_preload.so 文件, 但bash脚本和设置断点等都变得简单, 原因是程序执行到my_preload.so中的_init函数内的死循环时, gdb已然把目标程序的调试符号载入, 此时可以从容地设置断点, 不需再用__libc_start_main.

至于my_preload.so 文件, 最好用绝对路径, 其好处是: gdb -p 来调试程序时总能找到该文件.
其内容如下:

#include    /* sleep */

int _init(void)
{
   volatile gdb_ready = 0;
   while( !gdb_ready )
   {
      sleep(1);
   }
   return 0;
}


gcc -nostartfiles -g -fPIC -shared -o my_preload.so my_preload.c

-nostartfiles to avoid multiple definition of _init error

you can even write a simple function gdb_go in ~/gdbinit to make things easiler:

define gdb_go
  set gdb_ready=1
  continue
end

gdb函数中设置的变量gdb_ready必需与my_preload.c 中的变量名保持一致. 这个方案看似麻烦一些, 因为要有一个额外的.c 文件, 但实际上, 对于一个已经安装好的系统, 只需做一次即可. 对所有被调试程序都是一样的.

为了能给被调试程序"安装"上述的bash脚本来做这件事, 下面的bash函数可以自动安装和反安装这样的脚本:


#arg 1: the absolute path to an executable file
function debug_hook_check_permission()
{
    abs_path="$1"
    if [ ! -x "$abs_path" ]; then
        echo "Fail to find executable file $1, please check the existence of the file and wether it can be found in \$PATH" >&2
        return 1
    fi

    # Check access right
    if ! ls "$abs_path" >& /dev/null; then
        echo "You have no permission to access [$abs_path]" >&2
        return 1
    fi

    # Check write access
    dir_name="${abs_path%/*}"
    if [ ! -w "${dir_name}" ]; then
        echo "You have no write permission to directory [${dir_name}]" >&2
        return 1
    fi

    return 0
}

# arg 1: the program name, absolute path or relative path or can found in $PATH
# If have insufficient permission, return 1 with error message write to stderr
function install_debug_hook()
{
    abs_path=$(which "$1")

    # General check
    if ! debug_hook_check_permission "$abs_path" ; then
        return 1
    fi

    # check: program should be ELF executable
    if [[ "$(eval file \"$abs_path\")" =~ "ELF.*executable" ]] ; then
        true
    else
        echo "file [$abs_path] is not an ELF executable" >&2
        file "$abs_path"
        return 1
    fi

    # check: program.real should not exist
    if [ -e "$abs_path.real" ] ; then
        echo "File [$abs_path.real] already exist!" >&2
        ls -l "$abs_path.real"
        return 1
    fi

    # Rename to file to a suffix ".real"
    mv "$abs_path" "$abs_path.real"
    echo "[$abs_path]" renamed to "[$abs_path.real]"

    # Create the file
    echo "file [$abs_path]"
    echo '#!/bin/bash' | tee "$abs_path"
    echo 'LD_PRELOAD=~/my_preload.so exec '"$abs_path.real"' "$@"' | tee -a "$abs_path"
    chmod a+x "$abs_path"
}

function uninstall_debug_hook()
{
    abs_path=$(which "$1")

    # check: program must be a shell script which contains 2 lines
    if [[ "$(eval file \"$abs_path\")" =~ "shell script" ]] && [ "$(eval cat \"$abs_path\" | wc -l)" -eq 2 ]; then
        true
    else
        echo "File [$abs_path] is not recognized as shell script or not a 2 lines file" >&2
        file "$abs_path"
        echo
        cat "$abs_path"
        return 1
    fi

    rm "$abs_path"
    mv "$abs_path.real" "$abs_path"
    echo "[$abs_path.real]" renamed to "[$abs_path]"
}

阅读(1713) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~