gdb的set follow-fork-mode child如何工作

一、 clone函数的man手册说明

clone man手册的说明:
/* Prototype for the glibc wrapper function */

#include <sched.h>

int clone(int (*fn)(void *), void *child_stack,
int flags, void *arg, ...
/* pid_t *ptid, struct user_desc *tls, pid_t *ctid */ );

/* Prototype for the raw system call */

long clone(unsigned long flags, void *child_stack,
void *ptid, void *ctid,
struct pt_regs *regs);

clone的初衷主要是为了在共享内存空间的前提下实现多线程。
The main use of clone() is to implement threads: multiple threads of control in a program that run concurrently in a shared memory space.
带入口函数参数(int (*fn)(void *))是glibc封装的,它的内部实现是先调用“raw system call”clone系统调用,新进程创建成功之后在其中执行用户提供的入口函数。
When the child process is created with clone(), it executes the function fn(arg). (This differs from fork(2), where execution continues in the child from the point of the fork(2) call.) The fn argument is a
pointer to a function that is called by the child process at the beginning of its execution. The arg argument is passed to the fn function.
flags标志位中可以设置SIGCHLD信号,如果没有设置,新任务退出的时候父任务不会收到通知。这个SIGCHLD的设置比它看起来的更加重要,甚至可以说,是这个标志位决定了gdb认为它是一个新线程还是一个新进程(尽管常规意义上我们认为如果共享内存地址空间、文件系统才是线程)。
The low byte of flags contains the number of the termination signal sent to the parent when the child dies. If this signal is specified as anything other than SIGCHLD, then the parent process must specify the
__WALL or __WCLONE options when waiting for the child with wait(2). If no signal is specified, then the parent process is not signaled when the child terminates.

二、 内核的代码

fork和vfork默认设置了SIGCHLD标志位,而clone没有,并且内核内核线程还默认设置了CLONE_UNTRACED标志位。
/*
* Ok, this is the main fork-routine.
*
* It copies the process, and if successful kick-starts
* it and waits for it to finish using the VM if required.
*/
long do_fork(unsigned long clone_flags,
unsigned long stack_start,
unsigned long stack_size,
int __user *parent_tidptr,
int __user *child_tidptr)
{
struct task_struct *p;
int trace = 0;
long nr;

/*
* Determine whether and which event to report to ptracer. When
* called from kernel_thread or CLONE_UNTRACED is explicitly
* requested, no event is reported; otherwise, report if the event
* for the type of forking is enabled.
*/
if (!(clone_flags & CLONE_UNTRACED)) {
if (clone_flags & CLONE_VFORK)
trace = PTRACE_EVENT_VFORK;
else if ((clone_flags & CSIGNAL) != SIGCHLD)
trace = PTRACE_EVENT_CLONE;
else
trace = PTRACE_EVENT_FORK;

if (likely(!ptrace_event_enabled(current, trace)))
trace = 0;
}
……
/* forking complete and child started to run, tell ptracer */
if (unlikely(trace))
ptrace_event(trace, nr);
……
}
pid_t kernel_thread(int (*fn)(void *), void *arg, unsigned long flags)
{
return do_fork(flags|CLONE_VM|CLONE_UNTRACED, (unsigned long)fn,
(unsigned long)arg, NULL, NULL);
}

#ifdef __ARCH_WANT_SYS_FORK
SYSCALL_DEFINE0(fork)
{
#ifdef CONFIG_MMU
return do_fork(SIGCHLD, 0, 0, NULL, NULL);
#else
/* can not support in nommu mode */
return(-EINVAL);
#endif
}
#endif

#ifdef __ARCH_WANT_SYS_VFORK
SYSCALL_DEFINE0(vfork)
{
return do_fork(CLONE_VFORK | CLONE_VM | SIGCHLD, 0,
0, NULL, NULL);
}
#endif
SYSCALL_DEFINE5(clone, unsigned long, clone_flags, unsigned long, newsp,
int __user *, parent_tidptr,
int __user *, child_tidptr,
int, tls_val)
#endif
{
return do_fork(clone_flags, newsp, 0, parent_tidptr, child_tidptr);
}

三、 glibc的代码

可以看到,和fork的逻辑类似,需要根据clone的返回值判断是当前进程还是新创建进程。如果是当前进程则函数返回,新进程则开始执行指定的函数。
static int
create_thread (struct pthread *pd, const struct pthread_attr *attr,
STACK_VARIABLES_PARMS)
{
……
The termination signal is chosen to be zero which means no signal
is sent. */
int clone_flags = (CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGNAL
| CLONE_SETTLS | CLONE_PARENT_SETTID
| CLONE_CHILD_CLEARTID | CLONE_SYSVSEM
| 0);
……
}

glibc-2.17\sysdeps\unix\sysv\linux\i386\clone.S
.text
ENTRY (BP_SYM (__clone))
/* Sanity check arguments. */
……
movl $SYS_ify(clone),%eax

#ifdef RESET_PID
/* Remember the flag value. */
movl %ebx, (%ecx)
#endif

/* End FDE now, because in the child the unwind info will be
wrong. */
cfi_endproc

int $0x80
popl %edi
popl %esi
popl %ebx

test %eax,%eax
jl SYSCALL_ERROR_LABEL
jz L(thread_start)

ret

L(thread_start):
cfi_startproc;
/* Clearing frame pointer is insufficient, use CFI. */
cfi_undefined (eip);
/* Note: %esi is zero. */
movl %esi,%ebp /* terminate the stack frame */

L(thread_start):

四、 gdb的代码

gdb-7.6.1\gdb\infrun.c
/* Tell the target to follow the fork we're stopped at. Returns true
if the inferior should be resumed; false, if the target for some
reason decided it's best not to resume. */

static int
follow_fork (void)
{
……
/* If there were any forks/vforks that were caught and are now to be
followed, then do so now. */
switch (tp->pending_follow.kind)
{
case TARGET_WAITKIND_FORKED:
case TARGET_WAITKIND_VFORKED:
{
……
/* Tell the target to do whatever is necessary to follow
either parent or child. */
if (target_follow_fork (follow_child))
{
/* Target refused to follow, or there's some other reason
we shouldn't resume. */
should_resume = 0;
}
……
}
}

五、 测试代码

可以看到,如果执行clone是设置了SIGCHLD标志,gdb认为是一个新进程;如果没有设置则认为是一个新进程,而和是否共享地址空间(CLONE_VM)无关
tsecer@harry: cat -n gdb.follow.fork.cpp
1 #include <unistd.h>
2 #include <stdlib.h>
3 #include <syscall.h>
4 #include <sched.h>
5 #include "signal.h"
6
7 int x = 0;
8 static int threadEntry(void * arg)
9 {
10 x++;
11 return 0;
12 }
13
14 int main()
15 {
16 const int STACKSIZE = 1024 * 1024 * 2;
17 char *stack = (char*)malloc(STACKSIZE);
18 stack += STACKSIZE - 256;
19 const pid_t child = clone(threadEntry, stack, CLONE_FS
20 #if ENABLE_SIGCHLD
21 | SIGCHLD
22 #endif
23 , nullptr, nullptr, nullptr, nullptr);
24 if (child > 0)
25 {
26 while (true)
27 {
28 sleep(1);
29 }
30 }
31
32 return 0;
33 }
tsecer@harry: g++ gdb.follow.fork.cpp -g
tsecer@harry: gdb -quiet -- ./a.out
Reading symbols from ./a.out...
(gdb) b 10
Breakpoint 1 at 0x4005df: file gdb.follow.fork.cpp, line 10.
(gdb) r
Starting program:  /home/tsecer/gdb.follow.fork/a.out
[New LWP 4469]
[Switching to LWP 4469]

Thread 2 hit Breakpoint 1, threadEntry (arg=0x0) at gdb.follow.fork.cpp:10
10 x++;
(gdb) quit
A debugging session is active.

Inferior 1 [process 4465] will be killed.

Quit anyway? (y or n) y
tsecer@harry: g++ gdb.follow.fork.cpp -g -DENABLE_SIGCHLD=1
tsecer@harry: gdb -quiet -- ./a.out
Reading symbols from ./a.out...
(gdb) b 10
Breakpoint 1 at 0x4005df: file gdb.follow.fork.cpp, line 10.
(gdb) r
Starting program:  /home/tsecer/gdb.follow.fork/a.out
[Detaching after fork from child process 4653]
^C
Program received signal SIGINT, Interrupt.
0x00007ffff72321a0 in __nanosleep_nocancel () from /lib64/libc.so.6
(gdb) quit
A debugging session is active.

Inferior 1 [process 4649] will be killed.

Quit anyway? (y or n) y
tsecer@harry:

posted on 2022-04-27 19:55  tsecer  阅读(366)  评论(0编辑  收藏  举报

导航