多线程中的Double Close与System Call Hook

同事在拿到修改后的weston相关的多线程代码中,怀疑有double close。期望我可以帮忙确定出来。

修改后的weston,我们拿到的只有一个executive elf文件,并没有source code。对于这种hook要求,一般有三种方法,但是各不相同:

  1. 修改kernel中的close system call:要求可以编译内核
  2. wrap libc中的close():可以直接更改glibc代码,也可以使用ld的“--wrap=symbol”选项,至少要有编译后的object,因为我们需要在link的时候wrap
  3. 使用LD_PRELOAD,新建一个动态库来hook/wrap close,不需要更改内核与libc,也不需要源代码

在内核中修改System Call

因为有内核源码,因此第一种方式成为了首选。double close,一般第二次close的时候会出错,但是有的时候会错误的关闭其他线程的fd,因此我们只需要在关闭出错的时候打印出这个线程的名字、pid即可。在close system calll的实现代码中添加一行即可(文件位于fs/file.c):

int __close_fd(struct files_struct *files, unsigned fd)
{
struct file *file;
struct fdtable *fdt;

spin_lock(&files->file_lock);
fdt = files_fdtable(files);
if (fd >= fdt->max_fds)
goto out_unlock;
file = fdt->fd[fd];
if (!file)
goto out_unlock;
rcu_assign_pointer(fdt->fd[fd], NULL);
__clear_close_on_exec(fd, fdt);
__put_unused_fd(files, fd);
spin_unlock(&files->file_lock);
return filp_close(file, files);

out_unlock:
spin_unlock(&files->file_lock);
printk(KERN_ERR "---process id/pid[%d] name=%s, close fd[%d], failed with EBADF\n", current->pid,current->comm,fd);
return -EBADF;
}

在用户态Hook

显然根据程序的从编译到运行的状态迁移,我们可以想象得到有以下几种方法:

  1. Link的时候wrap
  2. 在运行之前,加载器ld进行LD_PRELOAD


对于link的时候使用ld的--wrap=symbol选项在链接的时候将所有的symbol替换成__wrap_symbol,而libc中实际的symbol,那么就变成了__real_symbol,因此我们需要定义__wrap_symbol函数,并可能需要在里面调用__real_symbol。

对于LD_PRELOAD,这个是加载器ld的功能,ld还有一些其他用于调试的功能选项,例如LD_DEBUG,这些都可以使用man 8 ld.so来查看,或者查看文档The LD_DEBUG environment variable。

用户态Hook的注意点

查看SystemCall的方法注意

不管是使用LD_PRELOAD还是使用链接器的--wrap=symbol方式,都容易出现有些System Call没有被Hook住的情况,出现这种情况的原因一般都是因为使用strace来查看一个程序的system call,正确的做法应该是使用lstrace。对此,可以查看参考2的文章,以及文章的评论。

静态链接中的System Call

静态链接的可执行文件,不需要再去其他so中解析和查找符号,因此直接就执行起来了,并不需要ld.so(加载器),因此这些方法不适用。参考StackOverFlow中答案的Commnet:How to hook ALL linux system calls during a binary execution

使用hook的参考项目

另外有许多程序都是使用用户态的Hook来调试与监控用户态程序的行为,例如valgrind wrap了malloc/free,socksify wrap了connect调用。因此这些代码都值得参考。

 

为什么double close在多线程中会有问题?

原因在于同一个进程的所有线程共享fd,即其范围都是一样的,大家open一个file/socket得到的fd的值的范围都是在同一个范围内,使用完的fd,在close之后(reference count == 0),会被回收并重复利用。

在linux中,使用ulimit -a可以看到userspace上,一个Process可以使用的fd的范围:

$ ulimit -a
core file size (blocks, -c) 0
data seg size (kbytes, -d) unlimited
scheduling priority (-e) 0
file size (blocks, -f) unlimited
pending signals (-i) 94963
max locked memory (kbytes, -l) 64
max memory size (kbytes, -m) unlimited
open files (-n) 1024
pipe size (512 bytes, -p) 8
POSIX message queues (bytes, -q) 819200
real-time priority (-r) 0
stack size (kbytes, -s) 8192
cpu time (seconds, -t) unlimited
max user processes (-u) 94963
virtual memory (kbytes, -v) unlimited
file locks (-x) unlimited

因此可以同时最多打开的文件/sockect数目为1024,这个值有两种方法来修改:

①可以在命令行中使用 ulimit -n 4096(这里4096为设置后的可以使用的句柄数)来配置一个进程可以使用的最多句柄数。
②可以在程序中调用setrlimit这个System Call来配置进程可以使用的最多的句柄数,参考代码如下,亦可参考valgrind的test代码:

#include <sys/time.h>
#include <sys/resource.h>

struct rlimit rlp;
rlp.rlim_cur = 4096;
setrlimit(RLIMIT_NOFILE, &rlp);

如何避免double close

如果有source code,那么根据参考链接1中的建议,我们应该在代码中对调用了close的地方加上返回值判断。man close可以看到:

ERRORS
EBADF fd isn't a valid open file descriptor.

EINTR The close() call was interrupted by a signal; see signal(7).

EIO An I/O error occurred.

可以看到close并无法保证每次调用都成功,这个和free是不一样的。

参考

  1. Not so obvious multi-thread programming specific bugs
  2. How to Wrap a System Call (Libc Function) in Linux
如果代码有不完整,请大家移步:http://www.hexiongjun.com/?p=283

你可能感兴趣的:(多线程,linux)