2018-2019-1 20189219《Linux内核原理与分析》第六周作业

回顾entry_32.S

本章实验主要讲的是sys_call的具体流程,谈到具体流程,我们必须要结合代码进行分析。这里书上已经帮我们总结好了,我们只就简要的记录一下。

  • 精简的entry_32.S
490 ENTRY(system_call)
491	RING0_INT_FRAME			# can't unwind into user space anyway
492	ASM_CLAC
493	pushl_cfi %eax			# save orig_eax
494	SAVE_ALL
495	GET_THREAD_INFO(%ebp)
496					# system call tracing in operation / emulation
497	testl $_TIF_WORK_SYSCALL_ENTRY,TI_flags(%ebp)
498	jnz syscall_trace_entry
499	cmpl $(NR_syscalls), %eax
500	jae syscall_badsys
501syscall_call:
502	call *sys_call_table(,%eax,4)
503syscall_after_call:
504	movl %eax,PT_EAX(%esp)		# store the return value
505syscall_exit:

512	testl $_TIF_ALLWORK_MASK, %ecx	# current->work
513	jne syscall_exit_work
514
515restore_all:
516	TRACE_IRQS_IRET

532irq_return:
533	INTERRUPT_RETURN

精简之后这个sys_call流程就明朗很多了。具体的流程我们留到下面结合实践来理解。这里我们对书上的内容做一点补充,对于syscall_badsys函数:

/linux-3.18.6/arch/x86/kernel/entry_32.S
676syscall_badsys:
677	movl $-ENOSYS,%eax
678	jmp syscall_after_call
679END(syscall_badsys)

可以看到,这个异常处理就是将eax寄存器赋予一个异常值,然后跳到代码的syscall_after_call段,即跳过call阶段。
这里我们可以归纳出整个的流程图(借用了书上的图并进行了一些改正):

系统调用link函数

下载menu文件夹,发现里面包含了多个文件,找到Makefile打开:

#
# Makefile for Menu Program
#

CC_PTHREAD_FLAGS			 = -lpthread
CC_FLAGS                     = -c 
CC_OUTPUT_FLAGS				 = -o
CC                           = gcc-4.8
RM                           = rm
RM_FLAGS                     = -f

TARGET  =   test
OBJS    =   linktable.o  menu.o test.o

all:	$(OBJS)
	$(CC) $(CC_OUTPUT_FLAGS) $(TARGET) $(OBJS) 
rootfs:
	gcc -o init linktable.c menu.c test.c -m32 -static -lpthread
	gcc -o hello hello.c -m32 -static
	find init hello | cpio -o -Hnewc |gzip -9 > ../rootfs.img
	qemu -kernel ../linux-3.18.6/arch/x86/boot/bzImage -initrd ../rootfs.img
.c.o:
	$(CC) $(CC_FLAGS) $<

clean:
	$(RM) $(RM_FLAGS)  $(OBJS) $(TARGET) *.bak

从rootfs可以看出,老师已经将所有的必要文件全部编译并打包进rootfs.img镜像中,然后使用qemu打开此镜像。所以我们只需要使用make rootfs命令就可以完成所有的操作并启动menuOS了。这里我们打开并编辑test.c文件,将上一篇提到的link函数加入menuOS进行实现。当然,这里需要做一些小调整。

对test.c&Makefile的小调整

  • test.c
int Link(){
    int ret;
    char * oldpath = "time-asm";
    char * newpath = "timetest";
    ret = link(oldpath,newpath);
    printf("%d\n",ret);
    printf("errno is: %d\n",errno);
    if(ret==0)printf("link successfully\n");
    else printf("Unable to link the file\n");
    return 0;
}

int LinkAsm(){
    int ret;
    char * oldpath = "time-asm";
    char * newpath = "timetest-asm";
    asm volatile(
        "movl %2, %%ecx\n\t"
        "movl %1, %%ebx\n\t"
        "movl $0x09, %%eax\n\t"
        "int $0x80\n\t"
        "movl %%eax, %0"
        :"=m"(ret)
        :"b" (oldpath),"c"(newpath)
    );
    printf("%d\n",ret);
    printf("errno is: %d\n",errno);
    if(ret==0)printf("link successfully\n");
    else printf("Unable to link the file\n");
    return 0;
}

int main()
{
    PrintMenuOS();
    SetPrompt("MenuOS>>");
    MenuConfig("version","MenuOS V1.0(Based on Linux 3.18.6)",NULL);
    MenuConfig("quit","Quit from MenuOS",Quit);
    MenuConfig("time","Show System Time",Time);
    MenuConfig("time-asm","Show System Time(asm)",TimeAsm);
    MenuConfig("link","Make a Hard link for file'time-asm'",Link);
    MenuConfig("link-asm","Make a Hard link for file'time-asm'(asm)",LinkAsm);
    ExecuteMenu();
}

这里我们只将进行改变的部分代码贴出,为了能够更好的判断link是否link成功,我们输出了ret与erron值。以上是对test.c的编辑。

  • Makefile
#
# Makefile for Menu Program
#

CC_PTHREAD_FLAGS			 = -lpthread
CC_FLAGS                     = -c 
CC_OUTPUT_FLAGS				 = -o
CC                           = gcc-4.8
RM                           = rm
RM_FLAGS                     = -f

TARGET  =   test
OBJS    =   linktable.o  menu.o test.o

all:	$(OBJS)
	$(CC) $(CC_OUTPUT_FLAGS) $(TARGET) $(OBJS) 
rootfs:
	gcc-4.8 -o init linktable.c menu.c test.c -m32 -static -lpthread
	gcc-4.8 -o hello hello.c -m32 -static
	find init hello time-asm | cpio -o -Hnewc |gzip -9 > ../rootfs.img
	qemu -kernel ../linux-3.18.6/arch/x86/boot/bzImage -initrd ../rootfs.img
gdb:
	qemu -kernel ../linux-3.18.6/arch/x86/boot/bzImage -initrd ../rootfs.img -s -S
  • gcc版本
    依然是上一篇提到的问题,对于不同版本的gcc汇编代码并不是完全兼容的,如果我们使用默认的gcc版本(7.3.0)进行编译的话menuOS无法正确执行time-asm命令,因此我们在Makefile中更改使用4.8版本
  • 增加gdb暂停调试
    既然老师已经将命令都简化到了Makefile中,那么我们也可以添加一个gdb选项,免去每次都打上一大串代码的麻烦。
  • 将time-asm打包到镜像
    这里time-asm文件是用来给link函数进行连接的目标文件,在最初没有将其打包到镜像的时候,menuOS总是报错,于是我增加了erron值的输出,发现值为2,表示系统没有找到此文件:

    但是这个link函数如果不放在menuOS中是可以实现的,思考之后发现menuOS其实都是使用的bzImage与rootfs.img镜像中的内容,如果没有将time-asm这个文件打包进入的话menuOS是没有办法找到这个文件。

    进行了上述更改之后我们发现link成功了。但是这里依旧存在一个问题,就是link后的这个硬链接不知道存放在什么位置了,这个问题需要知道menuOS的执行目录,找了很久未找到,留着以后慢慢解决。

使用gdb跟踪

  • 进入entry_32.S
    从系统调用函数的内嵌汇编中我们知道,如link(),C语言中使用link()的时候,首先是将该函数的系统调用号存入eax寄存器,然后使用int 0x80唤起中断操作,从进入到sys_call,即entry_32.S。这里我们使用gdb设置一下断点:
b start_kernel
b sys_link
b * 0xc174d0c7 //the address of line 493 of "arch/x86/kernel/entry_32.S"

随后调试进入entry_32.S

  • 跟踪entry_32.S
    进入之后我们首先使用info reg eax查看eax寄存器的值,发现为0x9,与link函数的系统调用号一致。随后我们使用disp /i $pc&ni来进行逐步执行和信息验证。

从图中我们可以看到,系统做了如下工作:

  • 1.push %eax保存系统调用号。

  • 2.进行SAVE_ALL操作。

  • 3.cmp $0x166,%eax检查系统调用号。此处可以看到进行比对的值为0x166=358,而我们的系统调用函数正好是从0-357,即为358个系统调用函数。

  • 4.jae 0xc174d21a略过异常处理。此处我们可以看到异常处理的地址为0xc174d21a,(结合上面我们提到的syscall_badsys定义,此地址就是这个syscall_badsys所在的地址)而从下一步执行的地址中发现并不是此地址,因此判断跳过该异常处理。

  • 5.call *-0x3e8aafdc0(,%eax,4)在系统调用表中查找系统函数并进行调用。我们使用p sys_call_table[9]可以看到,这正是我们要的sys_link()。随后我们可以看到gdb的消息显示,此时已经成功进入到了sys_link函数入口,从此刻开始,系统便真正进入到了系统调用函数sys_link()。

  • 6.逐步执行直到ret。后面便是收尾工作了,至此,整个系统调用流程结束。

感兴趣的地方:关于sys_call与sys_call_table的关系

从书中我们得到,在进行sys_call时,系统会执行entry_32.S中的call *sys_call_table(,%eax,4)语句,即调用sys_call_table中的%eax*4的系统调用内核函数。我们知道,eax寄存器中存放的是要调用的系统函数的调用号,那为何要*4?

sys_call_table

要了解上述的问题,我们必须要了解的是sys_call_table究竟是何方神圣。我们在/linux-3.18.6/arch/x86/kernel/syscall_32.c里找到了sys_call_table的定义:

/* System call table for i386. */

#include <linux/linkage.h>
#include <linux/sys.h>
#include <linux/cache.h>
#include <asm/asm-offsets.h>

#define __SYSCALL_I386(nr, sym, compat) extern asmlinkage void sym(void) ;
#include <asm/syscalls_32.h>
#undef __SYSCALL_I386

#define __SYSCALL_I386(nr, sym, compat) [nr] = sym,

typedef asmlinkage void (*sys_call_ptr_t)(void);

extern asmlinkage void sys_ni_syscall(void);

__visible const sys_call_ptr_t sys_call_table[__NR_syscall_max+1] = {
	/*
	 * Smells like a compiler bug -- it doesn't work
	 * when the & below is removed.
	 */
	[0 ... __NR_syscall_max] = &sys_ni_syscall,
#include <asm/syscalls_32.h>
};

开头第一行注释就告诉我们,这就是系统调用表。在这里我们看到了两个关键点:

  • sys_call_table定义
__visible const sys_call_ptr_t sys_call_table[__NR_syscall_max+1] = {
	/*
	 * Smells like a compiler bug -- it doesn't work
	 * when the & below is removed.
	 */
	[0 ... __NR_syscall_max] = &sys_ni_syscall,
#include <asm/syscalls_32.h>
};

从这个函数中,我们看出table其实是个数组,包含两个部分:

  • &sys_ni_syscall
    将数组的每个元素都定义了一个sys_ni_syscall地址,
asmlinkage long sys_ni_syscall(void);

/*
 * Non-implemented system calls get redirected here.
 */
asmlinkage long sys_ni_syscall(void)
{
	return -ENOSYS;
}

从注释中可以看出,这个函数是用来处理未实现的系统调用,让其重新进行在此处进行重定向,可以理解为是提高代码的容错率而提供的一种机制。

  • #include <asm/syscalls_32.h>
    这可谓是table中的重头戏,为什么要包含一个头文件在数组中?这个头文件里面的内容是什么?到现在为止我们无从得知,所以我们要继续查看相关的其他内核代码。
  • #define __SYSCALL_I386(nr, sym, compat) [nr] = sym & asmlinkage void sym(void)
    这里我们看到了这个看似奇怪的定义,直觉告诉我们,这个定义或许和syscalls_32.h头文件中的内容有关,但是我们依然无法得知具体的关系和意义,那么我们来找找这个头文件吧。

sys_call_table中的重要头文件

找了半天发现,在相关的内核代码文件夹中根本没有发现syscalls_32.h的详细定义。这说明syscalls_32.h有可能是在内核启动的过程中才生成的,于是,跟着书上的提示,我们找到syscalls文件夹,发现其中内容有如下:

逐一打开:

  • Makefile:
1   out := $(obj)/../include/generated/asm

8   syscall32 := $(srctree)/$(src)/syscall_32.tbl
9   syscall64 := $(srctree)/$(src)/syscall_64.tbl
10
11  syshdr := $(srctree)/$(src)/syscallhdr.sh
12  systbl := $(srctree)/$(src)/syscalltbl.sh

45  $(out)/syscalls_32.h: $(syscall32) $(systbl)
46	$(call if_changed,systbl)
47  $(out)/syscalls_64.h: $(syscall64) $(systbl)
48	$(call if_changed,systbl)

我们精简一下Makefile中的内容,得到如上内容。豁然开朗!这里不就告诉我们$(out)/syscalls_32.h的依然就是$(syscall32)$(systbl)么!补全之后就是说$(obj)/../include/generated/asm/syscalls_32.h是由$(srctree)/$(src)/syscall_32.tbl&$(srctree)/$(src)/syscalltbl.sh生成的啊!这不就是我们苦苦寻找的syscalls_32.h么。二话不说,我们看看syscalls文件夹下的其他两个有关的文件:

  • syscall_32.tbl:
#
# 32-bit system call numbers and entry vectors
#
# The format is:
# <number> <abi> <name> <entry point> <compat entry point>
#
# The abi is always "i386" for this file.
#
0	i386	restart_syscall		sys_restart_syscall
1	i386	exit			sys_exit
2	i386	fork			sys_fork			stub32_fork
3	i386	read			sys_read
4	i386	write			sys_write
5	i386	open			sys_open			compat_sys_open
6	i386	close			sys_close
7	i386	waitpid			sys_waitpid			sys32_waitpid
8	i386	creat			sys_creat
9	i386	link			sys_link
10	i386	unlink			sys_unlink
11	i386	execve			sys_execve			stub32_execve
12	i386	chdir			sys_chdir
13	i386	time			sys_time			compat_sys_time
  • syscalltbl.sh:
#!/bin/sh

in="$1"
out="$2"

grep '^[0-9]' "$in" | sort -n | (
    while read nr abi name entry compat; do
	abi=`echo "$abi" | tr '[a-z]' '[A-Z]'`
	if [ -n "$compat" ]; then
	    echo "__SYSCALL_${abi}($nr, $entry, $compat)"
	elif [ -n "$entry" ]; then
	    echo "__SYSCALL_${abi}($nr, $entry, $entry)"
	fi
    done
) > "$out"

可以看到tbl(tablelist)中包含即为我们所有的系统调用内核函数,这里因为数量过多就没有完全列出。看到上述代码之后大家应该就彻底明白了,在整个syscalls文件夹中系统完成了对$(obj)/../include/generated/asm/syscalls_32.h的生成。下面我们模拟一下,更好的理解这个头文件的生成过程。

.
├── Makefile
├── syscall_32.tbl
└── syscalltbl.sh

由于内核代码的复杂性,其中我们改写了Makefile,其余都保持和内核代码一样,这样更符合一般的编写习惯:

  • Makefile
syscall32 := ./syscall_32.tbl
systbl := ./syscalltbl.sh
./syscalls_32.h: $(syscall32) $(systbl)
	sh ./syscalltbl.sh "$(syscall32)" "./syscalls_32.h"

在当前目录下make之后发现生成了syscalls_32.h文件:

.
├── Makefile
├── syscall_32.tbl
├── syscalls_32.h
└── syscalltbl.sh
  • syscalls_32.h:
__SYSCALL_I386(0, sys_restart_syscall, sys_restart_syscall)
__SYSCALL_I386(1, sys_exit, sys_exit)
__SYSCALL_I386(2, sys_fork, stub32_fork)
__SYSCALL_I386(3, sys_read, sys_read)
__SYSCALL_I386(4, sys_write, sys_write)
__SYSCALL_I386(5, sys_open, compat_sys_open)
__SYSCALL_I386(6, sys_close, sys_close)
__SYSCALL_I386(7, sys_waitpid, sys32_waitpid)
__SYSCALL_I386(8, sys_creat, sys_creat)
__SYSCALL_I386(9, sys_link, sys_link)
__SYSCALL_I386(10, sys_unlink, sys_unlink)
__SYSCALL_I386(11, sys_execve, stub32_execve)
__SYSCALL_I386(12, sys_chdir, sys_chdir)
__SYSCALL_I386(13, sys_time, compat_sys_time)

这个头文件包含了完整的32位库中的357个系统调用函数,这里我们只展示前13个。那么到这里我们可以很明朗,这个头文件下包含的其实就是各个系统调用函数的调用号,函数名和程序调用口。看到这,我们上面的疑问也都解决了,原来在这个重要的头文件中保存的就是__SYSCALL_I386(nr, sym, compat),即每个系统调用函数的关键信息。所以需要使用#define __SYSCALL_I386(nr, sym, compat) [nr] = sym & asmlinkage void sym(void)来把这个三个关键信息存入table数组中。
至此,我们的所有疑问都解开了。sys_call_table是一个数组类型,而此数组中的每个元素存放着用于重定向的函数地址和每个系统调用函数的三个关键信息,即每个元素占用了4个字节。这就是为什么在调用table中的项时需要使用%eax*4

posted on 2018-11-18 14:07  archemiya  阅读(326)  评论(0编辑  收藏  举报

导航