优秀的编程知识分享平台

网站首页 > 技术文章 正文

90后程序员小伙分享—Linux内核kernel启动分析(下篇)精品推荐

nanyue 2024-11-10 10:17:55 技术文章 1 ℃

上篇中给大家介绍了 makefile如何生成image、内核自解压代码分析,下篇继续给大家分享内核启动分析

三、VMLINUX内核启动代码分析

从下面开始就进入了真正的内核了,首先执行的代码是在

arch/arm/kernel/head.S中的汇编代码,然后就进入了内核的c代码部分

(init/Main. c),注意,此刻内核的编译链接地址不再是0x0 了,而是0xC0008000

了。

内核汇编部分:(关于该部分参考我的另一篇详细的分析)

刚刚进入内核汇编时的一些系统状态和寄存器值如下

Mmu 关闭,I Cache 和D Cache 清除并关闭,r0=0, rl=architecture ID。

从arch/arm/kernel/vmlinux. Ids文件中可以看出,解压后内核是从stext

段开始执行的。

1. 确保系统处于SVC模式并且FIR、IRQ都已经关闭、

2. 调用_lookup_processor_type函数,通过协处理器cpl5读取出系统cpu

的id,然后通过查無 [^核映像中 .proc. info, init段的所有

proc_info_list数据结构,以判断该内核是否支持该款cpu。

3. 调用_lookup_machine_type函数,检査由uboot传递进来的machine

architecture number是否被内核支持,也就是在内核

的 .arch. info, init段中查找看是否有对应 machine number的

machine_desc数据结构体存在。


下面进入start_kernel函数去执行,in init/Main. c文件

1. printk(linu:x_banner)打印内核的一些信息,版本,作者,编译器版本,日

期等信息

2. setup_arch(&command_line); /氺 in arm/kernel/setup. c */

函数原型:void —init |setup_arch|(char **cmdline』)

批注[si4]:嗯,重中之重啊

a. setup_processor()+lookup_processor_type〇 获取对应处理器 id 的

proc_info_list 结构体 list,取出 cpu_name,打印关于 epuname,id,

proc_arch 的信息,设置上 system_utsname= list->arch_name(armv4t),

elf_platform= list->elf_name (v4), elf_hwcap = list->elf_hwcap;

/* l|2|4 */,接着执行 cpu_proc_init()函数

Cpu-single. h中有如下定义

#define cpu_proc_init —cpu_fn(CPU_NAME, _proc_init)

#def ine —cpu_fn (name, x) —catify_fn (name, x)

#def ine —catify_fn (name, x) narae##x

CPU_NAME = cpu_arm920

所以 cpu_proc_init();函数实际上是函数 cpu_arm920_proc_init()函

数,在pr〇c_arm920. S文件中定义的。

b. mdesc = setup—machine (machine—arch—type)函数

取出当前系统的在内核.arch. info, init段内对应的machine_desc数据

结构体的地址list,并打印出list-name内容

c. machine_name = mdesc->name 设置全局变量 machine_name

d. tags = phys_to_virt(mdesc_>boot_params),修改默认的参数地址,使

用uboot传运进^的参数的真实地址0x30000100并将其转换成虚拟地址

OxcOOOOlOO

e. 参数解析

if (tags->hdr. tag == ATAG_C0RE) {

if (meminfo. nr—banks != 0) /* meminfo defined in setup, c */

squash_mem_tags(tags);

parse_tags(tags);

} _

static struct meminfo meminfo —initdata = { 0, };in steup. c

所以这里会调用parse_tags(tags) (in steup. c)函数。


Steup_arch ()-->parse_tags ()-->parse_tag (),

all function in steup. c

_tagtable_begin, _tagtable_end 这两个参数是在

arch/arm/kernel/vmlinux. Ids文件中确定的,这个区间存放了各种参

数的解析函数,可以直接引用

parSe_tag〇函数就是查找这个区间的各种参数头,来和传递寄来的参数

头进行匹配,找到了之后就利用其中的函数指针parse来进行参数解析,

具体怎么解析需要分析各种参数的作用了。如果匹g不到的话,会打印

信息说 Ignoring unrecognised tag 0x%08x\n,表示 uboot 传递进来了

一个内核识别不了的参数。

f. struct mm_struct init_mm = INIT_MM(init_mm);

start_code = (unsigned long) &_text;

init_mm. end_code = (unsigned long) &_etext;

init_mm. end_data - (unsigned long) &_edata;

init mm. brk = (unsigned long) &—end;

_text, _etext, _edata, _end 都是在 arch/arm/kernel/vmlinux. Ids

文件中#定的

g. parse_cmdline(cmdline_p,from)解析 uboot 传递寄来的 commond_line

字符串,通过from指针内的参数解析到command_line这个全局数组里,

然后将该数组的地址赋值给上上级传进来的参数 command_line

(start_kernerl)。这里只分析了当中的mem、initrd部分,另外一处是

start_kernel()->parse_option〇用于解析其余部分。将 mem 和 initrd

参数从原始 cormnand_line中扣出,留下的部分放在 *cmdline_p中

(start_kernel传进来的参数,第二次分析会用到)。因为接下来是建立

内存的映射页表,需要用到mem、initrd两个参数的内容,所以在这里

提前解析出来。

h. paging_init(&meminfo,mdesc);/*这部分的主要工作建立页表,初始

化内存 */ in arch/arm/imn/init. c 文件中

memtable_init (mi)为系统内存创建页表,是一个比较重要的函数,

详细的参考网址

http://bbs. sjtu. edu. cn/bbstcon, board, Embedded, reid, 1165977462.

html

的内容,需要注意的是这个函数中将中断向量表映射到了 OxffffOOOO

开始处

调用 mdesc->map_io() 函数,在 smdk2410 中位于

arch/arm/mach-s3c2410/mach-sindk2410. c文件中的 smdk2410_map_io()

函数,关于这部分的外设的静态映射分析请_考易松华老师

Linux静态映射分析,smdk2410_map_io()函数主要做了下M冗祥攀十青:

1 )、iotable_init(s3c_iodesc,ARRAY_SIZE(s3c_iodesc))建立

GPIO, IRQ, MEMCTRL, UART 的#态映射表


2)

、(cpu->map_io) (mach_desc, size)建立其他外设的静态映射表,

包括网卡,LCD,看门狗等

这里需要明确一点的是在map. h文件中的

S3C2410_ADDR(x) ((void _iomem *)0xF0000000 + (x))

表明我们处理的10和外设的寄存器被静态映射到了 OxFOOOOOOO开

始地方,其中每一种外设的空间占1M大小。

这里必须得提到的是内存的三种映射和使用方式:1.处理器的GPI0,各

种外设(IRQ, UART, MEMCRTL, WATCHDOG, USB等)的寄存器的静态映射,

也就是上面红色字体体现出来的。这是映射到内核空间内,所以用户程

序是不能访问的,只能由内核来访问。2.内核空间访问的虚拟地址(比

如是代码的地址或者是kmalloc空间的地址)都是在0x30008000开始后

的空间,也就是说这中虚拟地址是通过静态映射得到的,这部分也只能

由内核访问。phys_to_virt和virt_to_phys,3.除此之外的物理地址

都是通过mmu的规射出来的 ,TTB已"^确定在0x30004000开始的16K

空间就是一级页表的空间。通过这中方式映射的虚拟地址用户程序和内

核程序都可以访问,不过解析式通过 ramu来完成的而已。参考

arch/arm/kernel/head. S中建立的4M空间手工页表就是按照mmu的规则

建立的(只不过是按照sector的方式建立)

3) 、对时钟,Uart的简单初始化。

h.request_standard_resources(&meminfo, mdesc) 为 memory ,

kernel_text, kernel_data,video_ram在资源树种申请标准资源。它完成

实际的备源分配工作,如果参数new所描述的资源中的一部分或全部已经被

其它节点所占用,则函数返回与new相冲突的resource结构的指针。否则

就返回NULL

i. cpu_init()函数分析 in arch/arm/kernel/setup. c

打印一些关于cpu的信息,比如cpu id, cache大小等。另外重要的是

设置了 IRQ、ABT、UND三种模式的stack空间,分别都是12个字节。最后

将系统切换到svc模式。

i.用_mach_desc_SMDK2410_type结构体中特定成员来初始化系统的这么

写全局变量 init_arch_irq, init_machine, system_timer

3. sched_init()函数

初始&每个处理器的可运行队列,设置系统初始化进程即0号进程。也就是

调用了 init_idie (current, smp_processor_id〇)初始化 idle 当前进程。

4. pre empt_d i sab 1 e ()禁止抢占

5. 建立系统内存页区(zone)链表build_all_zonelists〇

6. printk(KERN_N0TICE 〃Kernel command line: %s\n〃,saved_command_line);

打印出从uboot传递过来的command_line字符串,在setup_arch函数中获

得的。

7. parse_early_param〇,这里分析的是系统能够辨别的一些早期参数(这个函数甚至可以去掉,_ s e t u p的形式的参数),而且在分析的时候并不是以

setup_arch (&command_line)传出来的 command_line 为基础,而是以最原

生态的saved_coramand_line为基础的。

8. parse_args (’’Booting kernel' command_line, —start________par am,

—stop___param - —start______par am,

&unknown_bootoption);

对于比较新的版本真正起作用的函数,与parse_early_param〇;相比,此处对解析

列表的处理范围加大了,解析列表中除了包括系统以setup定义的启动参数,还包括模

块中定义的param参数以及系统不能辨别的参数。

c〇mmand_line是setup_arch函数传递出来的值;

_start___ param是param参数的起始地址,在System, map文件中能看到

_stop___param - _start___param是参数个数

unknown_bootoption是对应与启动参数不是param的相应处理函数(查看

parse_one ()就知道怎么回事)

函数parse_one()既可以处理param参数,又可以处理_setup的形式的参

数,还可以处理不能识别的参数。当然这些都是依靠传递进来的参数进行分支处

理的。

参数的处理详情参考网络上另一篇文章:Linux启动bootargs参数分析

9. sort_main_extable ()

将放在—start___ ex_table 到—stop____ ex_table 之间的*(_ex_table)区

域中的struct exception_table_entry型全局结构变量按insn成员变量值

从小到大排序,即将可能_致缺&异常的指令按其指令二进制代码值从小到

大排序。

10.在前面的 setup_arch-■>paging_init-■> memtable_init 函数中为系统创建

页表的时候,中断向量表的虚地址 init_ maps,是用

alloc_ bootmem_ low_ pages分配的,ARM规定中断向量表的地址只能是0或

OxFFFFOOOO,#以该函数里有部分代码的作用就是映射一页到0或

OxFFFFOOOO。

trap_init函数做了一下的工作:把放在.Lcvectors处的系统8个意外入口

跳转指令搬到高端中断向量 OxffffOOOO处,再将_ stubs_start到

_S tubs_e nd之间的各种意外初始化代码搬到0xffff0200处。将系统调用的

返回句^拷贝到〇x ffff〇5〇〇处。刷新OxffffOOOO处1页范围的指令cache,

将 D0MAINJJSER 的访问权限由 DOMAIN_MANAGER 改成 D0MAIN_CLIENT 权限。

11. rcu_init 〇函数初始化当前cpu的读、复制、更新数据结构(struct rcu_data)

全局变量 per cpu rcu data 和 per cpu rcu bh data.

12. init_IRQ 函数

初始化系统中支持的最大可能中断数的中断描述结构struct irqdesc变量数

组ir(L<lesc[NR_IRQS],把每个结构变量irq_desc[n]都初始化成预先定义好

的坏中断描述结"构变量bacUrcLdesc,并初始化该中断的链表表头成员结构

变量pend。

执行init_arch_irq函数,该函数是在setup_arch函数最后初始化的一个全

局函数指针,指向了 smdk2410_init_irq 函数(inmach-smdk2410. c),实际

上是调用了 s3C 24xx_init_irq_函数在该函数中,首先清除所有的中断未决

标志,之后就初始化中断的触发方式和屏蔽位,还有中断句柄初始化,这里

不是最终用户的中断函数,而是do_level_IRQ或者do_edge_IRQ函数,在这

两个函数中都使用过_do_irq函数来找到真正最终驱动程序注册在系统中

的中断处理函数。

status = 0;

do {

if (ret == IRQ

HANDLED)

status |= action->flags;

retval |= ret:

action = action->next;

} while (action);

接着初始化外部中断的一些参数,最后补充初始化uartADC中断。

13. pidhash_init ()函数

设置系泰中每种 pidjiash表中的 hash链表数的移位值全局变量

pidhash_shift, 将 pidhash_shift 设置成min (12)。分别为每种 hash 表的

连续hash链表表头结构^请内存,把申请到的内存虚拟基址分别传给

pid_hash[n] (n=l~3),并将每种hash表中的每个hash链表表头结构struct

hlist_head中的first成员指针设置成NULL

14. init_timers ()函数

初始Ifc当前处理器的时间向量基本结构struct tveC_t_baSe_S全局变量

per_cpu_tvec_bases,初始化 per_cpu_tvec_bases 的自选锁成员变量 lock

5: void _init jllit tilTISrS (void)

1: {

) : //这个函数就是 ti rrers_nb这个结构体的 cal I函 数

>: timer_cpu_notify(& timers—nb, (unsigned long)CPU—UP—PREPARE,

1: (void * ) (long) smp_processor_id ());

l

h / /这个是用的机制和 cpuf r eq的机制是一样的,通过 not i f i er_chai n_r egi st er (&cpu_chai n, nb)注册以

): / /只不过这里的链是 cpu_chai n,而 cpufreq是其他的链

.: register_cpu_noti fier(&

timors_nb );

>

?

ji //设置软中断行动函数描述结构变量softirq_ueC[z1](系统定时器)的设置

1: //也就是设置timer■定时器到之后的处理函数

): open—softirq (TIMER—SOFTIRQ, run_timer_sof tirq , NULL);

15. softirqjnit 函数

内核的软中断机制初始化函数,void _____ init softirq_init (void)

{ 一

/*HI_SCFTI RQffl于实现 bottom hal f, TASKLET_SCFTI RQffl 于公共的 t ask 丨 et V

open_so f t irq ( TASKLET_SOFTIR〇 , tasklet_act ion , NULL );

open_sof tirq (HI_SOFTIRQJ tasklet_hi_act ion , NULL);

}

16. time_init()这个函数是用来做体系相关的timer的初始化

void ___init time init(v

id)

if (

timer - >offset == NULL)

system_ timer - >offset = dummy—get t imeo f fset;

system_ timer - > i n i t ();

还记得在setup_arch最后初始化了三个全局的变量吗?下图所示

/?

x Set up uarious architecture-specific pointers

/ /运 行 rrach-smdk2410- c文件中定义的结构体 __nBch_desc_SNEK2410中的特定函数和结构 |

init_arch_irq - mdesc->init_irq;/x smdk2410_init_irq

system一 timer - mdesc- > t imer ; /? 3c24xx_tim9r ?/

inj t_machine - mdesc-> ini t_machine ;/? smdk2410_init_fs2410 x/

static void ____ init

s3c2410 timer init

(void)

{

s3c2410_timer_setup();

setup—irq ^ s3c241 D_ timer_irq );

struct sys_timer

.init =

.offset =

.resume =

s3 c2 4xx_t imer = {

s3 c2 410_t imer_ini t,

s3 c2 410_get t imeo f fset,

s3 c2 410—t imer—setup

很显然,t i m e_i n i t函数中的i f条件不成立,所以就是直接执行了函数

s3c2410_timer_init函数。可以看出,系统使用了 timer4来作为系统的定

时器。

17. console_init 函数

初始化系统的控制台结构,该函数执行后调用printk函数将log_buf中所有

符合打印级别的系统信息打印到控制台上

a. tty_register_ldisc ()注册默认的TTY线路规程

大象研究tty的驱动就会发现了在用户和硬件之间tty的驱动是分了三

层最底层当然是tty驱动程序了,主要负责从硬件接受数据,和格式化

上层发下来的数据后给硬件。在驱动程序之上就是线路规程了,他负责

把从tty核心层或者tty驱动层接受的数据进行特殊的按着某个协议的格式化,就像是Pf5p或者蓝牙协议,然后在分发出去的。在tty线路规

程之上就是tty核心层了。

b.接下来就是执行平台相关的控制台初始化代码

s3c24xx_serial_initconsole

/*

在vmlinux. Ids. S中连接脚本汇编中有这段代码

—con_initcall_start =.;

*(. con_initcall. init)

一con initcall—end =.;

因此我们再此处调用的就是C〇n_initcall. init段的代码,

将fn函数放到.con_initcall. init的输入段中,如下:

^define console_initcall(fn) \

static initcall_t —initcall_##fn \

—attribute_used_

—attribute—((—section—(〃. con_initcall. init")))=fn

j

//在我们串口驱动里面有

这么一个注册语句:console_initcall (s3c24xx_serial_initconsole);

//因此我们的控制台初始化流

程就是:start_kernel->console_init_>s3c24xx_serial_initconsole

call = 一con_initcall_start; /* console_initcall */

while (call < —con_initcall_end) {

(*call)();

call++;

18_ profile_init ()函数

/*对系&剖析做相关初始化,系统剖析用于系统调用*/

//profile是用来对系统剖析的,在系统调试的时候有用

//需要打开内核选项,并且在bootargs中有profile这一项才能开启这个功

/*

profile只是内核的一个调试性能的工具,这个可以通过menuconfig

中 profiling support 打开。

1. 如何使用profile:

首先确认内核支持profile,然后在内核启动时加入以下参数:

profile=l或者其它参数,新的内核支持profile=schedule 1

2.

内核启动后会创建/ proc/ profile文件,这个文件可以通过

readprofile 读取,

$口 readprofile -m / proc/kallsyms | sort -nr >

Vcur_ profile. log,

或者 readprofile _r _m / proc/kallsyms | sort - nr,

或者 readprofile _r && sleep 1 && readprofile _ m/ proc/kallsyms








Tags:

最近发表
标签列表