分析进程创建、执行、切换以及可执行文件的加载

分析进程创建、执行、切换以及可执行文件的加载

447+原创作品转载请注明出处+https://github.com/mengning/linuxkernel/

一、实验步骤及分析
进程描述

我们通过进程控制块来描述来描述进程,又称其为进程描述符,他提供了进程相关的所有信息,例如状态、进程双向链表管理、控制台、文件系统、内存管理、进程间通信等等。

struct task_struct {
	volatile long state;	/* -1 unrunnable, 0 runnable, >0 stopped */
	void *stack;
	atomic_t usage;
	unsigned int flags;	/* per process flags, defined below */
	unsigned int ptrace;
           ...
}

进程创建
0号进程初始化是通过硬编码固定下来的,init_task为0号进程的进程描述符的结构体变量。init_task初始化如下:

struct task_struct init_task = INIT_TASK(init_task);
EXPORT_SYMBOL(init_task);

rest_init调用kernel_thread创建1号进程和2号进程,一个是kernel_init,init用户进程;另一个是kthreadd进程,是所有内核进程的祖先,管理内核进程。

static noinline void __init_refok rest_init(void){
....
kernel_thread(kernel_init,NULL,CLONE_FILES);
....
pid = kernel_thread(kthreadd,NULL,CLONE_FS|CLONE_FILES);
....
}

使用fork函数创建进程

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
 
int main()
{
    int pid;
    pid = fork();
    if(pid < 0)
    {
       fprintf("stderr","failed!");
    }
    else if(pid == 0) 
    {
       printf("This is child process!\n");
    }
    else
    {
       printf("This is parent process!\n");
       wait(NULL);
       printf("child complete!\n");
    }
    return 0;
}

fork系统调用把当前进程又复制了一个子进程,此时两个进程执行相同的代码,只是父进程和子进程的返回值不同,而两个进程输出信息在一个终端在这里插入图片描述
fork系统调用
fork系统调用时,用户态用int $0x80指令触发中断机制,cpu自动从用户态堆栈转为内核态堆栈,将ss:esp,cs:eip以及eflags压到当前进程内核堆栈中,接下来执行system_call,其用于保存现场,调用系统调用内核函数,处理完后返回,恢复现场。最后iret将ss:esp,cs:eip以及eflags从内核堆栈恢复到相应寄存器中。
参考代码如下:

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

#ifdef __ARCH_WANT_SYS_CLONE
#ifdef CONFIG_CLONE_BACKWARDS
SYSCALL_DEFINE5(clone, unsigned long, clone_flags, unsigned long, newsp,
		 int __user *, parent_tidptr,
		 int, tls_val,
		 int __user *, child_tidptr)
#elif defined(CONFIG_CLONE_BACKWARDS2)
SYSCALL_DEFINE5(clone, unsigned long, newsp, unsigned long, clone_flags,
		 int __user *, parent_tidptr,
		 int __user *, child_tidptr,
		 int, tls_val)
#elif defined(CONFIG_CLONE_BACKWARDS3)
SYSCALL_DEFINE6(clone, unsigned long, clone_flags, unsigned long, newsp,
		int, stack_size,
		int __user *, parent_tidptr,
		int __user *, child_tidptr,
		int, tls_val)
#else
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);
}
#endif

do_fork 函数代码如下:

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;//子进程id
	/*
	 * 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;
	}

	p = copy_process(clone_flags, stack_start, stack_size,
			 child_tidptr, NULL, trace);//创建子进程描述符
	/*
	 * Do this prior waking up the new thread - the thread pointer
	 * might get invalid after that point, if the thread exits quickly.
	 */
	if (!IS_ERR(p)) {//copy_prpcess执行成功
		struct completion vfork;
		struct pid *pid;

		trace_sched_process_fork(current, p);

		pid = get_task_pid(p, PIDTYPE_PID);//获得task结构体的pid
		nr = pid_vnr(pid);//根据pid结构体获得进程pid

		if (clone_flags & CLONE_PARENT_SETTID)
			put_user(nr, parent_tidptr);

		if (clone_flags & CLONE_VFORK) {
			p->vfork_done = &vfork;
			init_completion(&vfork);
			get_task_struct(p);
		}

		wake_up_new_task(p);//将子进程添加到调度器的队列

		/* forking complete and child started to run, tell ptracer */
		if (unlikely(trace))
			ptrace_event_pid(trace, pid);

		if (clone_flags & CLONE_VFORK) {
			if (!wait_for_vfork_done(p, &vfork))
				ptrace_event_pid(PTRACE_EVENT_VFORK_DONE, pid);
		}

		put_pid(pid);
	} else {
		nr = PTR_ERR(p);//异常处理
	}
	return nr;//返回子进程pid
}

do_fork函数主要完成了以下工作:
通过调用copy_process()复制父进程信息,创建描述符及其他的数据结构;获得pid;调用wake_up_new_task§将子进程加入调度器队列,等待cpu资源运行.
copy_process函数

static struct task_struct *copy_process(unsigned long clone_flags,
1183					unsigned long stack_start,
1184					unsigned long stack_size,
1185					int __user *child_tidptr,
1186					struct pid *pid,
1187					int trace)
1188{
1189	int retval;
1190	struct task_struct *p;
           ...
1235	retval = security_task_create(clone_flags);//安全性检查
           ...
1240	p = dup_task_struct(current);//核心语句,复制pcb,内核栈
           ...
1252	retval = -EAGAIN;
       //检查用户进程数是否超过限制
1253	if (atomic_read(&p->real_cred->user->processes) >=
1254			task_rlimit(p, RLIMIT_NPROC)) {
1255		if (p->real_cred->user != INIT_USER &&
1256		    !capable(CAP_SYS_RESOURCE) && !capable(CAP_SYS_ADMIN))
1257			goto bad_fork_free;
1258	}
	      ...
      //检查进程数量是否超过max_threads
1271	if (nr_threads >= max_threads)
1272		goto bad_fork_cleanup_count;
1273
1274	if (!try_module_get(task_thread_info(p)->exec_domain->module))
1275		goto bad_fork_cleanup_count;
1276
...
1360	retval = sched_fork(clone_flags, p);//初始化新进程调度数据结构,将新进程状态设置为TASK_RUNNING
1361	if (retval)
1362		goto bad_fork_cleanup_policy;
1363
1364	retval = perf_event_init_task(p);
1365	if (retval)
1366		goto bad_fork_cleanup_policy;
1367	retval = audit_alloc(p);
1368	if (retval)
1369		goto bad_fork_cleanup_perf;
1370	/* copy all the process information */
1371	shm_init_task(p);
1372	retval = copy_semundo(clone_flags, p);
1373	if (retval)
1374		goto bad_fork_cleanup_audit;
1375	retval = copy_files(clone_flags, p);
1376	if (retval)
1377		goto bad_fork_cleanup_semundo;
1378	retval = copy_fs(clone_flags, p);
1379	if (retval)
1380		goto bad_fork_cleanup_files;
1381	retval = copy_sighand(clone_flags, p);
1382	if (retval)
1383		goto bad_fork_cleanup_fs;
1384	retval = copy_signal(clone_flags, p);
1385	if (retval)
1386		goto bad_fork_cleanup_sighand;
1387	retval = copy_mm(clone_flags, p);
1388	if (retval)
1389		goto bad_fork_cleanup_signal;
1390	retval = copy_namespaces(clone_flags, p);
1391	if (retval)
1392		goto bad_fork_cleanup_mm;
1393	retval = copy_io(clone_flags, p);
1394	if (retval)
1395		goto bad_fork_cleanup_namespaces;
1396	retval = copy_thread(clone_flags, stack_start, stack_size, p);//初始化子进程内核栈
 
         ...
1548
1549	return p;//返回被创建的子进程描述符指针p
         ...
}

copy_process函数主要完成以下工作:
调用dup_task_struct复制父进程描述符 ;调用copy_thread初始化子进程内核栈;将子进程置为就绪态;采用写时复制技术复制其他进程资源;设置子进程pid.
dup_task_struct函数

 
305static struct task_struct *dup_task_struct(struct task_struct *orig)
306{
307	struct task_struct *tsk;
308	struct thread_info *ti;
309	int node = tsk_fork_get_node(orig);
310	int err;
311
312	tsk = alloc_task_struct_node(node);//为子进程创建进程描述符分配存储空间
313	if (!tsk)
314		return NULL;
315
316	ti = alloc_thread_info_node(tsk, node);//创建两个页,一部分存放thread_info,另一部分存放内核堆栈
317	if (!ti)
318		goto free_tsk;
319
320	err = arch_dup_task_struct(tsk, orig);//复制父进程task_struct
321	if (err)
322		goto free_ti;
323
324	tsk->stack = ti;//将栈底的值赋给新节点的stack
325#ifdef CONFIG_SECCOMP
326	/*
327	 * We must handle setting up seccomp filters once we're under
328	 * the sighand lock in case orig has changed between now and
329	 * then. Until then, filter must be NULL to avoid messing up
330	 * the usage counts on the error path calling free_task.
331	 */
332	tsk->seccomp.filter = NULL;
333#endif
334//初始化子进程的thread_info
335	setup_thread_stack(tsk, orig);
336	clear_user_return_notifier(tsk);
337	clear_tsk_need_resched(tsk);
338	set_task_stack_end_magic(tsk);
339
340#ifdef CONFIG_CC_STACKPROTECTOR
341	tsk->stack_canary = get_random_int();
342#endif
343
344	/*
345	 * One for us, one for whoever does the "release_task()" (usually
346	 * parent)
347	 */
348	atomic_set(&tsk->usage, 2);
349#ifdef CONFIG_BLK_DEV_IO_TRACE
350	tsk->btrace_seq = 0;
351#endif
352	tsk->splice_pipe = NULL;
353	tsk->task_frag.page = NULL;
354
355	account_kernel_stack(ti, 1);
356
357	return tsk;
358
359free_ti:
360	free_thread_info(ti);
361free_tsk:
362	free_task_struct(tsk);
363	return NULL;
364}

dup_task_struct函数创建两个页,一部分存放thread_info,另一部分存放内核堆栈,复制父进程task_struct ,thread_info结构,然后将task指针指向子进程的进程描述符。
copy_thread函数

int copy_thread(unsigned long clone_flags, unsigned long sp,
133	unsigned long arg, struct task_struct *p)
134{
135	struct pt_regs *childregs = task_pt_regs(p);
136	struct task_struct *tsk;
137	int err;
138
139	p->thread.sp = (unsigned long) childregs;
140	p->thread.sp0 = (unsigned long) (childregs+1);
141	memset(p->thread.ptrace_bps, 0, sizeof(p->thread.ptrace_bps));
142
143	if (unlikely(p->flags & PF_KTHREAD)) {//内核线程
144		/* kernel thread */
145		memset(childregs, 0, sizeof(struct pt_regs));
        //内核进程,从ret_from_kernel_thread开始执行
146		p->thread.ip = (unsigned long) ret_from_kernel_thread;
147		task_user_gs(p) = __KERNEL_STACK_CANARY;
148		childregs->ds = __USER_DS;
149		childregs->es = __USER_DS;
150		childregs->fs = __KERNEL_PERCPU;
151		childregs->bx = sp;	/* function */
152		childregs->bp = arg;
153		childregs->orig_ax = -1;
154		childregs->cs = __KERNEL_CS | get_kernel_rpl();
155		childregs->flags = X86_EFLAGS_IF | X86_EFLAGS_FIXED;
156		p->thread.io_bitmap_ptr = NULL;
157		return 0;
158	}
//复制内核堆栈(复制父进程寄存器信息)
159	*childregs = *current_pt_regs();
160	childregs->ax = 0;//子进程eax =0,返回值为0
161	if (sp)
162		childregs->sp = sp;
163//从ret_from_fork执行
164	 p->thread.ip = (unsigned long) ret_from_fork;
165	task_user_gs(p) = get_user_gs(current_pt_regs());
166
167	p->thread.io_bitmap_ptr = NULL;
168	tsk = current;
169	err = -ENOMEM;
170
171	if (unlikely(test_tsk_thread_flag(tsk, TIF_IO_BITMAP))) {
172		p->thread.io_bitmap_ptr = kmemdup(tsk->thread.io_bitmap_ptr,
173						IO_BITMAP_BYTES, GFP_KERNEL);
174		if (!p->thread.io_bitmap_ptr) {
175			p->thread.io_bitmap_max = 0;
176			return -ENOMEM;
177		}
178		set_tsk_thread_flag(p, TIF_IO_BITMAP);
179	}
180
181	err = 0;
182
183	/*
184	 * Set a new TLS for the child thread?
185	 */
186	if (clone_flags & CLONE_SETTLS)
187		err = do_set_thread_area(p, -1,
188			(struct user_desc __user *)childregs->si, 0);
189
190	if (err && p->thread.io_bitmap_ptr) {
191		kfree(p->thread.io_bitmap_ptr);
192		p->thread.io_bitmap_max = 0;
193	}
194	return err;
195}
196

copy_thread函数是对内核信息的初始化。子进程开始执行的起点分为两种情况,内核进程从ret_from_kernel_thread开始执行,用户态进程从ret_from_fork执行。
使用gdb调试fork
使用以下指令启动menuOS

cd LinuxKernel   
rm menu -rf
git clone https://github.com/mengning/menu.git
cd menu
mv test_fork.c test.c
make rootfs

在这里插入图片描述
打开另一个shell窗口,将当前目录设为LinuxKernel,进行gdb调试

 cd LinuxKernel
 gdb
 file linux-3.18.6/vmlinux
 target remote:1234

在这里插入图片描述
分别在sys_clone、do_fork、dup_task_struct、copy_process、copy_thread、ret_from_fork出设置断点。
在这里插入图片描述停在断点 sys_clone处。
在这里插入图片描述
停在断点do_fork处在这里插入图片描述
停在断点dup_task_struct处、断点copy_process处 、断点copy_thread处
在这里插入图片描述在这里插入图片描述
在这里插入图片描述
编程使用exec*库函数加载一个可执行文件
编辑文件myexec.c,生成预处理文件myexec.cpp,编译成汇编代码myexec.s,编译成目标代码,即二进制文件myexec.o,链接成可执行文件myexec,运行./myexec。
在这里插入图片描述
在这里插入图片描述
ELF可执行文件格式
可重定位文件:一般是中间文件,需要和其他文件一起来创建可执行文件、静态库文件、共享目标文件。
可执行文件:文件中保存着一个用来执行的文件。
共享目标文件:指可以被可执行文件或其他库文件使用的目标。
execve函数过程描述
整体调用关系为 execve->sys_execve->do_execve() -> do_execve_common()->exec_binprm()->search_binary_handler()->load_elf_binary()->start_thread()。
大致处理过程如下:在这里插入图片描述
sys_execve中的do_execve() 读取128个字节的文件头部,判断可执行文件的类型
调用search_binary_handler()搜索和匹配合适的可执行文件装载处理过程。
ELF文件由load_elf_binary()函数负责装载,load_elf_binary()函数调用了start_thread函数,创建新进程的堆栈。
使用gdb跟踪分析一个execve系统调用内核处理函数do_execve。
新的可执行程序执行起点以及为什么execve系统调用返回后新的可执行程序能顺利执行?
新的可执行程序开始执行的起点在于修改的调用 execve系统调用时压入内核堆栈的EIP的值,此时标志着当前进程可执行文件已经完全替换成新的可执行文件。
对于静态链接的可执行程序和动态链接的可执行程序execve系统调用返回时会有什么不同?
静态链接:elf_entry指向可执行文件的头部,一般是main函数,是新程序执行的起点,一般地址为0x8048XXX的位置。
动态链接:elf_entry指向ld即动态链接器的起点load_elf_interp。
schedule调用_schedule,_schedule调用pick_next_task,context_switch函数,context_switch函数调用__switch_to。pick_next_task函数是根据调度策略和调度算法选择下一进程,context_switch函数负责进程的切换。
分析switch_to中的汇编代码,理解进程上下文的切换机制,以及与中断上下文切换的关系

#define switch_to(prev, next, last)					\
32do {									\
33	/*								\
34	 * Context-switching clobbers all registers, so we clobber	\
35	 * them explicitly, via unused output variables.		\
36	 * (EAX and EBP is not listed because EBP is saved/restored	\
37	 * explicitly for wchan access and EAX is the return value of	\
38	 * __switch_to())						\
39	 */								\
40	unsigned long ebx, ecx, edx, esi, edi;				\
41									\
42	asm volatile("pushfl\n\t"		/* 保存当前进程flags */	
43		     "pushl %%ebp\n\t"		/* 保存当前进程的堆栈基址EBP    */	
44		     "movl %%esp,%[prev_sp]\n\t"	/* 保存当前栈顶ESP */ 
45		     "movl %[next_sp],%%esp\n\t"	/* 将下一栈栈顶保存到esp中  */ 
//完成内核堆栈的切换
46		     "movl $1f,%[prev_ip]\n\t"	/* 保存当前进程的EIP    */	
47		     "pushl %[next_ip]\n\t"	/* 把next进程的起点EIP压入堆栈   */	
48		     __switch_canary		
			//next_ip一般是$if,对于新创建的子进程是ret_from_fork
49		     "jmp __switch_to\n"	/* prev进程中,设置next进程堆栈  */	
 
50		     "1:\t"		       //next进程开始执行		
51		     "popl %%ebp\n\t"		/* restore EBP   */	\
52		     "popfl\n"			/* restore flags */	\
53									\
54		     /* output parameters */				\
55		     : [prev_sp] "=m" (prev->thread.sp),		\
56		       [prev_ip] "=m" (prev->thread.ip),		\
57		       "=a" (last),					\
58									\
59		       /* clobbered output registers: */		\
60		       "=b" (ebx), "=c" (ecx), "=d" (edx),		\
61		       "=S" (esi), "=D" (edi)				\
62		       							\
63		       __switch_canary_oparam				\
64									\
65		       /* input parameters: */				\
66		     : [next_sp]  "m" (next->thread.sp),		\
67		       [next_ip]  "m" (next->thread.ip),		\
68		       							\
69		       /* regparm parameters for __switch_to(): */	\
70		       [prev]     "a" (prev),				\
71		       [next]     "d" (next)				\
72									\
73		       __switch_canary_iparam				\
74									\
75		     : /* reloaded segment registers */			\
76			"memory");					\
77} while (0)

首先在当前进程prev的内核栈中保存esi,edi及ebp寄存器的内容。然后将prev的内核堆栈指针esp存入prev->thread.esp中。
把将next进程的内核栈指针next->thread.esp置入esp寄存器中,将当前进程的地址保存在prev->thread.eip中,这个地址就是prev下一次被调度,通过jmp指令转入一个函数__switch_to,__switch_to中jmp与return的匹配,return 会弹出返回地址,因为jmp不会压栈,return弹出的则是栈顶地址即$1f标识之处。恢复next上次被调离时推进堆栈的内容。next进程开始执行。

进程上下文及与中断上下文切换的关系
进程上下文切换需要保存切换进程的相关信息(thread.sp和thread.ip);中断上下文的切换是在一个进程的用户态到一个进程的内核态,或从进程的内核态到用户态,切换进程需要在不同的进程间切换,但一般进程上下文切换是套在中断上下文切换中的。例如,系统调用作为中断陷入内核,,调用schedule函数发生进程上下文切换,系统调用返回,完成中断上下文的切换。

三、总结
linux执行过程

运行用户态进程U
发生中断:save cs:eip/ss:eip/eflags,加载当前进程内核堆栈,跳转至中断处理程序
SAVE_ALL,保存现场,完成中断上下文的切换。
中断处理过程若调用了schedule函数,其中switch_to做进程上下文的切换。(假设由进程U到进程M)
$1f之后,运行用户态进程M
restore_all,恢复现场
iret 从U进程内核堆栈弹出硬件完成的压栈内容,完成中断上下文的切换,即U的内核态到用户态。
继续运行U。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值