您的位置:首页 > 运维架构 > Linux

全面解析Linux 内核 3.10.x - 内核入口函数__kernel_entry

2015-11-22 14:19 1011 查看
From: 全面解析Linux 内核 3.10.x - 本文章完全基于MIPS架构

找对了入口,才有可能找对出口 – 佚名

在前面几节内容中我简单将Linux ,以及Linux Kernel的概念做了总结,然后又将编译以及内核镜像也做了也总结! 从本节内容开始,我将真正的进入到内核代码中去!加油吧,Keven!

从上一节中我已经知道了vmlinux.lds链接文件中指定了内核的入口函数kernel_entry,此函数被定义在head.S文件中!,请跟着我去看看此函数到底做了什么!

1、head.S - kernel_entry

NESTED(kernel_entry, 16, sp)            # kernel entry point

kernel_entry_setup          # cpu specific setup

setup_c0_status_pri

/* We might not get launched at the address the kernel is linked to,
so we jump there.  */
PTR_LA  t0, 0f      #Loading 数字标号0中的地址
jr  t0
0:
PTR_LA      t0, __bss_start             #Loading __bss_start(0xffffffff80dc0000) 到 t0
LONG_S      zero, (t0)                  #对0xffffffff80dc0000这个地址内容清零(清除bss)
PTR_LA      t1, __bss_stop - LONGSIZE   #Loading __bss_stop(0xffffffff80dc0000 + 0x00cc2ac0 - unsinged(8)<0x40> 8*8) =
0xffffffff80106ae8 此地址就是kernel_entry函数地址 到 t1
1:
PTR_ADDIU   t0, LONGSIZE                #t0 = t0 + LONGSIZE (64位操作) = 0xffffffff80dc0000 + 0x40
LONG_S      zero, (t0)                  #清零地址内容
bne     t0, t1, 1b                      #判断 t0 是否等于 t1,如果不等于到当前位置前面的第一个标号1,循环加到等于后,执行下面指令

# firmware arguments
LONG_S      a0, fw_arg0     #子程序的前4个参数存到a0~a3中
LONG_S      a1, fw_arg1
LONG_S      a2, fw_arg2
LONG_S      a3, fw_arg3

MTC0        zero, CP0_CONTEXT           # clear context register 清除 寄存器CP0的$4,这个寄存器保存的是页表的起始地址
PTR_LA      $28, init_thread_union 		#将init_thread_union地址Loading $28中(Ps.$28是全局指针寄存器),关于全局指针见 Note 1:
/* Set the SP after an empty pt_regs.  */
PTR_LI      sp, _THREAD_SIZE - 32 - PT_SIZE  #加载常数_THREAD_SIZE(0x10000) - 32(0x100) - PT_SIZE(0xc40) = 0xf2c0  详解见Note 2:
到sp寄存器(Ps.sp为堆栈寄存器)
PTR_ADDU    sp, $28						#$sp = $sp + $28 (64位操作) ,sp指向union结构的0xf2c0 + 0xffffffff80c90000 详解见Note 2:
back_to_back_c0_hazard                  # Note 3
set_saved_sp    sp, t0, t1              # Note 4
PTR_SUBU    sp, 4 * SZREG       # init stack pointer $sp = $sp - (4 * 8)<0x100> = ffffffff80cae480 ???

j       start_kernel
END(kernel_entry)

__CPUINIT


上述汇编指令的含义(64位指令):

PTR_LA          dla
LONG_S          sd
PTR_ADDIU       daddiu
MTC0            dmtc0
PTR_LI          dli
PTR_ADDU        daddu
PTR_SUBU        dsubu


对于上述代码还需要下面几点解述!

Note 1:

gp - 全局指针寄存器 – 为何gp指向
init_thread_union


首先gp为全局指针寄存器(x86中并没有此寄存器,而是用ss和sp来做处理,为何没有呢,据我所知应该是x86的cpu寄存器太少的缘故,故而没有专门设置这样的寄存器(这里请熟悉x86的朋友指正),他的作用就是在进程切换的时候保存当前进程的
thread_info
指针到当前gp中!此寄存器对于调试而言好处大大的!那么为什么在启动阶段首先要将
init_thread_union
放入gp中呢?从
init_thread_union
说起,其实这就是是0号进程所在(Ps.0号进程见下面)!

Note 2:

sp 指向栈的第一个元素(栈底)(其实并不是真真意义上的栈底)!(占大小 - 32 -
PT_SIZE
== 当前0号进程所在位置)

Note 3:

此时具体执行了一个汇编指令_ehb(ehb).Ps.关于
ehb
为exception hazard barrier,由MIPS的流水线引起,大致可以理解为由于MIPS采用的流水线结构,即使在异常处理代码中(这里由于改变了状态寄存器情况类似),由于流水线的作用,异常处理结束时,其下一条(可能超过一条,依赖流水线的设计)仍然被预取执行,这样由于CPU的特权级别发生了改变,但被流水线预取的指令并不知道这些,因而导致严重的安全性问题。为了避免这种情况发生,MIPS专门使用了
ehb
指令。还包括
eret
,即从异常(原子的,atomically)返回!

Note 4:

将sp地址保存到
kernelsp[NR_CPUS]
中!

2、内核的0号进程

关于进程的相关信息,因为比较庞杂我这里暂时先不去做总结!简单的说一下pid 0。

keven@keven-2015:~/kernel/linux-3.10.92$ ps -el
F S   UID   PID  PPID  C PRI  NI ADDR SZ WCHAN  TTY          TIME CMD
4 S     0     1     0  0  80   0 -  6108 poll_s ?        00:00:01 init


ps -el 可查看当前所有的进程详细的信息。

我们熟悉的1号进程的父亲是0号进程,那么0号进程是怎么来的呢?作用是什么呢?为什么不把0号进程释放呢?

在上面kernel_entry中,我说了
init_thread_union
就是0号进程所在!那么我去看一下
init_thread_union
究竟是什么东东?

堆栈定义:

#define init_thread_info    (init_thread_union.thread_info)
/* Initial task structure */
struct task_struct init_task = INIT_TASK(init_task);
/*
* Initial thread structure. Alignment of this is handled by a special
* linker map entry.
*/
union thread_union init_thread_union __init_task_data =
{ INIT_THREAD_INFO(init_task) };
union thread_union {
struct thread_info thread_info;
unsigned long stack[THREAD_SIZE/sizeof(long)];
};


上面的代码表示,对于每一个进程,内核为其单独分配了一个内存区域,这个区域存储的是内核栈和该进程所对应的一个轻量级的描述符 - thread_info!

struct thread_info {
struct task_struct  *task;      /* main task structure */
struct exec_domain  *exec_domain;   /* execution domain */
unsigned long       flags;      /* low level flags */
unsigned long       tp_value;   /* thread pointer */
__u32           cpu;        /* current CPU */
int         preempt_count;  /* 0 => preemptable, <0 => BUG */

mm_segment_t        addr_limit; /*
* thread address space limit:
* 0x7fffffff for user-thead
* 0xffffffff for kernel-thread
*/
struct restart_block    restart_block;
struct pt_regs      *regs;
};


但是此结构并没有直接包含与进程处理相关的字段,而是通过task指向进程描述符,这里它的大小为2M!

Ps。。关于内核大页放到后面在详解!

THREAD_SIZE
是怎么分配的呢?

对于这个问题,我一开始其实是比较拒绝的!haha..

我们来看下内核页表(CONFIG_PAGE_SIZE_xKB)的配置会影响到什么?

全局搜索了一下,大抵会影响到下面的几个宏<我这里只列出4K和64K的配置>:

a.默认页大小配置 <<通过内核配置

#ifdef CONFIG_PAGE_SIZE_4KB

#define PM_DEFAULT_MASK PM_4K

#elif defined(CONFIG_PAGE_SIZE_64KB)

#define PM_DEFAULT_MASK PM_64K

#else

#error Bad page size configuration!

#endif

b.默认tlb大小配置 <<通过内核配置

/*
* Default huge tlb size for a given kernel configuration
*/
#ifdef CONFIG_PAGE_SIZE_4KB
#define PM_HUGE_MASK    PM_1M
#elif defined(CONFIG_PAGE_SIZE_64KB)
#define PM_HUGE_MASK    PM_256M
#elif defined(CONFIG_MIPS_HUGE_TLB_SUPPORT)
#error Bad page size configuration for hugetlbfs!
#endif


c.PAGE_SHIFT 通过此宏和另外一个宏配置默认的
THREAD_SIZE


/*
* PAGE_SHIFT determines the page size
*/
#ifdef CONFIG_PAGE_SIZE_4KB
#define PAGE_SHIFT  12
#endif
#ifdef CONFIG_PAGE_SIZE_64KB
#define PAGE_SHIFT  16


d.线程信息配置宏,此宏觉得了
THREAD_SIZE
的大小

/* thread information allocation */
#if defined(CONFIG_PAGE_SIZE_4KB) && defined(CONFIG_32BIT)
#define THREAD_SIZE_ORDER (1)
#endif
#if defined(CONFIG_PAGE_SIZE_4KB) && defined(CONFIG_64BIT)
#define THREAD_SIZE_ORDER (2)
#ifdef CONFIG_PAGE_SIZE_64KB
#define THREAD_SIZE_ORDER (0)
#endif


基本上感觉和我要去探索的问题相关的宏就以上几个了!

上面一直在提到
THREAD_SIZE
的大小,它的定义如下:

#define THREAD_SIZE (PAGE_SIZE << THREAD_SIZE_ORDER)


PAGE_SIZE的大小则是PAGE_SHIFT来觉得:

#ifdef __ASSEMBLY__
#define PAGE_SIZE   (1 << PAGE_SHIFT)
#else
#define PAGE_SIZE   (1UL << PAGE_SHIFT)
#endif


如果内核页表大小配置为4K且为32位OS的话:

THREAD_SIZE
= (1 << 12) << 1; == 8k

4K&&64位OS的话:

THREAD_SIZE
= (1 << 12) << 2; == 16k

如果是64K的话:

THREAD_SIZE
= (1 << 16) << 0; == 64k

大抵通过代码将4K以及64的区别是决定了THREAD_SIZE的大小!

THREAD_SIZE 为谁所用?

上面我们说了
init_thread_union
的定义:

union thread_union {
struct thread_info thread_info;
unsigned long stack[THREAD_SIZE/sizeof(long)];
};


加入当前我们配置的是64K,那么stack的大小就是8192(8k)!

也就是需要使用两个物理页来存储..

来一张经典的图:



由上图可知,内核栈是逆增长的(从高地址 - 低地址),而
thread_info
结构则是从该区域的开始处正(低地址 - 高地址)增长。内核栈的栈顶地址存储在gp寄存器中。所以,当进程从用户态切换到内核态后,gp寄存器指向这个区域的末端!

THREAD_SIZE决定了stack数组的大小,那么为何要将内核栈和thread_info(其实也就相当于task_struct,只不过使用thread_info结构更节省空间)放在一起?

原因就是内核可以很容易的通过gp寄存器的值获得当前正在运行进程的thread_info结构的地址,进而获得当前进程描述符的地址!

/* How to get the thread information struct from C.  */
static inline struct thread_info *current_thread_info(void)
{
register struct thread_info *__current_thread_info __asm__("$28");

return __current_thread_info;
}


我们最常用的current宏其实返回就是thread_info的task成员.

#define get_current() (current_thread_info()->task)
#define current get_current()


好,这几个问题算是弄清除了!

那么问题以及思考的事情就来了..

深入理解Linux 内核3.10.x Q&A - 内存管理

1、内核页表4k,64k的区别在哪里?

2、思考:内核页表的大小到底和什么因素有关?如何选择页面大小?

以后所有的问题单会单独在一篇文章中进行解答:

Ps.如果对MIPS汇编指令有兴趣,参考MIPS指令集Wiki

By: Keven - 点滴积累
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: