在真实的程序执行环境下, 一个进程可能不能通过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]"
}
阅读(1705) | 评论(0) | 转发(0) |