Linux内核源码分析(三)--启动汇编下篇

废话少说,接着上一篇继续分析我们的__create_page_tables函数。回顾一下前面的分析可以知道目前r8中存放的是machine_desc结构的地址,r9中存放的是cpu主标识,r10中存放的是proc_info_list结构的地址。


__create_page_tables:
 pgtbl r4
内核加载地址之前的16KB内存用来存放一级页表,由pgtbl r4可知我们现在把一级页表的地址保存在r4中。
 mov r0, r4
 mov r3, #0
 add r6, r0, #0x4000
1: str r3, [r0], #4
 str r3, [r0], #4
 str r3, [r0], #4
 str r3, [r0], #4
 teq r0, r6
 bne 1b
将r4到r4+16K之间的内存清零,即清空一级页表。
 ldr r7, [r10, #PROCINFO_MM_MMUFLAGS]
将proc_info_list结构中__cpu_mm_mmu_flags的值保存到r7中,对于我们来说即PMD_TYPE_SECT | PMD_SECT_BUFFERABLE | PMD_SECT_CACHEABLE | PMD_BIT4 | PMD_SECT_AP_WRITE | PMD_SECT_AP_READ,由 PMD_TYPE_SECT可知开始会进行段映射。
 mov r6, pc, lsr #20
 orr r3, r7, r6, lsl #20
 str r3, [r4, r6, lsl #2]
cpu使用的虚拟地址,前12位是在一级页表内的偏移,再后面的偏移会根据具体的映射方式划分。这里将内核起始的1MB空间映射到相等的虚拟空间,注意一个页表项占了4个地址,所以一级偏移量要乘以4才能找到对应一级页表项的地址。
 add r0, r4,  #(KERNEL_START & 0xff000000) >> 18
 str r3, [r0, #(KERNEL_START & 0x00f00000) >> 18]!
 ldr r6, =(KERNEL_END - 1)
 add r0, r0, #4
 add r6, r4, r6, lsr #18
1: cmp r0, r6
 add r3, r3, #1 << 20
 strls r3, [r0], #4
 bls 1b
除了一对一映射的1MB空间,还需要进行内核链接时的虚拟地址到物理地址的映射,上面的代码就是做这个的,开始的两条指令不能合二为一的原因是arm的立即数是8位的。可以看到,r0中开始存放的是内核虚拟起始地址在一级页表中对应表项的地址,r6中最终存放的是内核虚拟结束地址在一级页表中对应表项的结束地址,r6只右移了18位没有把低2位清零说明找的不是页表项的起始地址,记住一个页表 项32位占了4个地址。
 add r0, r4, #PAGE_OFFSET >> 18
 orr r6, r7, #(PHYS_OFFSET & 0xff000000)
 .if (PHYS_OFFSET & 0x00f00000)
 orr r6, r6, #(PHYS_OFFSET & 0x00f00000)
 .endif
 str r6, [r0]
 mov pc, lr
PAGE_OFFSET是内核高端内存的起始虚拟地址,PHYS_OFFSET是物理地址的起始地址,看样子只是映射了1MB的空间,因为这段空间里有bootloader传过来的参数。整个映射关系如下所示。注意图中标注的地址位段地址,页表和内核起始地址相差16KB在同一个段内。



函数返回后会寻找proc_info_list结构中的__cpu_flush并执行此处的指令,我们的proc_info_list的__cpu_flush中存的是一条跳转指令b __arm920_setup。而跳转之前我们需要知道lr寄存器中存的是__enable_mmu的物理地址,r13中存的是__switch_data的虚拟地址。下面看arch/arm/mm/proc-arm920.S中的__arm920_setup函数。
__arm920_setup:
        mov     r0, #0
        mcr     p15, 0, r0, c7, c7              @ invalidate I,D caches on v4
        mcr     p15, 0, r0, c7, c10, 4          @ drain write buffer on v4
#ifdef CONFIG_MMU
        mcr     p15, 0, r0, c8, c7              @ invalidate I,D TLBs on v4
#endif
        adr     r5, arm920_crval
        ldmia   r5, {r5, r6}
        mrc     p15, 0, r0, c1, c0              @ get control register v4
        bic     r0, r0, r5
        orr     r0, r0, r6
        mov     pc, lr
cp15的c7寄存器控制着高速缓存和写缓存,c8控制TLB。这里先是指令和数据cache内容无效,清除写缓存,使快表无效。接着从arm920_crval处取出两个字分别放入r5和r6中。arm920_crval的定义如下。
        .type   arm920_crval, #object
arm920_crval:
        crval   clear=0x00003f3f, mmuset=0x00003135, ucset=0x00001130
crval是一个宏,定义在arch/arm/mm/proc-macros.S文件中。
        .macro  crval, clear, mmuset, ucset
#ifdef CONFIG_MMU
        .word   \clear
        .word   \mmuset
#else
        .word   \clear
        .word   \ucset
#endif
        .endm
如此r5中放的是要清除cp15中c1的位,r6中是设置位,最终要设到cp15的c1寄存器的值计算好后保存在r0寄存器中,r0中使能mmu的位被设置了,所以不能在这里设置c1,要等到后面在__enable_mmu函数中进行设置。

cp15中c1寄存器的编码格式如下所示。

C1中的控制位

含义

M(bit[0])

0 :禁止 MMU 或者 PU 

1 :使能 MMU 或者 PU

如果系统中没有MMU及PU,读取时该位返回0,写入时忽略该位

A(bit[1])

0 :禁止地址对齐检查

1 :使能地址对齐检查

C(bit[2])

当数据cache和指令cache分开时,本控制位禁止/使能数据cache。当数据cache和指令cache统一时,该控制位禁止/使能整个cache。

0 :禁止数据 / 整个 cache 

1 :使能数据 / 整个 cache

如果系统中不含cache,读取时该位返回0.写入时忽略

当系统中不能禁止cache 时,读取时返回1.写入时忽略

W(bit[3])

0 :禁止写缓冲

1 :使能写缓冲

如果系统中不含写缓冲时,读取时该位返回0.写入时忽略

当系统中不能禁止写缓冲时,读取时返回1.写入时忽略

P(bit[4])

对于向前兼容26位地址的ARM处理器,本控制位控制PROG32控制信号

0 :异常中断处理程序进入 32 位地址模式

1 :异常中断处理程序进入26 位地址模式

如果本系统中不支持向前兼容26位地址,读取该位时返回1,写入时忽略

D(bit[5])

对于向前兼容26位地址的ARM处理器,本控制位控制DATA32控制信号

0 :禁止 26 位地址异常检查

1 :使能 26 位地址异常检查

如果本系统中不支持向前兼容26位地址,读取该位时返回1,写入时忽略

L(bit[6])

对于ARMv3及以前的版本,本控制位可以控制处理器的中止模型

0 :选择早期中止模型

1 :选择后期中止模型

B(bit[7])

对于存储系统同时支持big-endian和little-endian的ARM系统,本控制位配置系统的存储模式

0 : little endian  

1 : big endian

对于只支持little-endian的系统,读取时该位返回0,写入时忽略

对于只支持big-endian的系统,读取时该位返回1,写入时忽略

S(bit[8])

在基于 MMU 的存储系统中,本位用作系统保护

R(bit[9])

在基于 MMU 的存储系统中,本位用作 ROM 保护

F(bit[10])

由生产商定义

Z(bit[11])

对于支持跳转预测的ARM系统,本控制位禁止/使能跳转预测功能

0 :禁止跳转预测功能 

1 :使能跳转预测功能

对于不支持跳转预测的ARM系统,读取该位时返回0,写入时忽略

I(bit[12])

当数据cache和指令cache是分开的,本控制位禁止/使能指令cache

0 :禁止指令 cache  

1 :使能指令 cache

如果系统中使用统一的指令cache和数据cache或者系统中不含cache,读取该位时返回0,写入时忽略。当系统中的指令cache不能禁止时,读取时该位返回1,写入时忽略

V(bit[13])

对于支持高端异常向量表的系统,本控制位控制向量表的位置

0 :选择低端异常中断向量 0x0~0x1c 

1 :选择高端异常中断向量0xffff0000~ 0xffff001c

对于不支持高端异常向量表的系统,读取时该位返回0,写入时忽略

PR(bit[14])

如果系统中的cache的淘汰算法可以选择的话,本控制位选择淘汰算法

0 :常规的 cache 淘汰算法,如随机淘汰 

1 :预测性淘汰算法,如round-robin 淘汰算法

如果系统中cache的淘汰算法不可选择,写入该位时忽略。读取该位时,根据其淘汰算法是否可以比较简单地预测最坏情况返回0或者1

L4(bit[15])

对于ARM版本5及以上的版本,本控制位可以提供兼容以前的ARM版本的功能

0 :保持 ARMv5 以上版本的正常功能

1 :将 ARMv5 以上版本与以前版本处理器 兼容,不根据跳转地址的 bit[0] 进行 ARM 指令和 Thumb 状态切换: bit[0] 等于 0 表示 ARM 指令,等于 1 表示 Thumb 指令

Bits[31:16])

这些位保留将来使用,应为UNP/SBZP



接下来可以看__enable_mmu函数了。
 .type __enable_mmu, %function
__enable_mmu:
#ifdef CONFIG_ALIGNMENT_TRAP
 orr r0, r0, #CR_A
#else
 bic r0, r0, #CR_A
#endif
#ifdef CONFIG_CPU_DCACHE_DISABLE
 bic r0, r0, #CR_C
#endif
#ifdef CONFIG_CPU_BPREDICT_DISABLE
 bic r0, r0, #CR_Z
#endif
#ifdef CONFIG_CPU_ICACHE_DISABLE
 bic r0, r0, #CR_I
#endif
 mov r5, #(domain_val(DOMAIN_USER, DOMAIN_MANAGER) | \
        domain_val(DOMAIN_KERNEL, DOMAIN_MANAGER) | \
        domain_val(DOMAIN_TABLE, DOMAIN_MANAGER) | \
        domain_val(DOMAIN_IO, DOMAIN_CLIENT))
 mcr p15, 0, r5, c3, c0, 0  @ load domain access register
 mcr p15, 0, r4, c2, c0, 0  @ load page table pointer
 b __turn_mmu_on

CR_A、CR_C、CR_Z、CR_I定义在include/asm-arm/system.h中
#define CR_A    (1 << 1)        /* Alignment abort enable               */
#define CR_C    (1 << 2)        /* Dcache enable                        */
#define CR_Z    (1 << 11)       /* Implementation defined               */
#define CR_I    (1 << 12)       /* Icache enable                        */
在__turn_mmu_on中r0的值会被设置到cp15的c1寄存器中。
另外cp15的c2寄存器存放一级页目录地址,c3寄存器定义了arm处理器16域的访问权限。
c3中每两位表示一个域,描述分别如下。
00:当前级别下,该内存区域不允许被访问,任何的访问都会引起一个domain fault,这是AP位无效
01:当前级别下,该内存区域的访问必须配合该内存区域的段描述符中AP位进行权限检查
10:保留状态
11:当前级别下,对该内存区域的访问都不进行权限检查,此时AP位无效
再看include/asm-arm/domain.h文件中domain相关的宏定义。
#define DOMAIN_KERNEL   2
#define DOMAIN_TABLE    2
#define DOMAIN_USER     1
#define DOMAIN_IO       0
可见开始除了IO地址,其他都不需要权限检查。
#define DOMAIN_NOACCESS 0
#define DOMAIN_CLIENT   1
#define DOMAIN_MANAGER  3
#define domain_val(dom,type)    ((type) << (2*(dom)))

最后跳到__turn_mmu_on函数真正的打开mmu,接着跳到r13所指向的地址处执行。
__turn_mmu_on:
 mov r0, r0
 mcr p15, 0, r0, c1, c0, 0  @ write control reg
 mrc p15, 0, r3, c0, c0, 0  @ read id reg
 mov r3, r3
 mov r3, r3
 mov pc, r13
r13中存放的是__switch_data的链接地址,mmu已经打开程序无须再做地址转换。
__switch_data:
 .long __mmap_switched
 .long __data_loc   @ r4 存放数据的位置
 .long __data_start   @ r5 数据段起始的位置
 .long __bss_start   @ r6 bss段起始地址
 .long _end    @ r7 bss段结束地址
以上定义在链接脚本中。
 .long processor_id   @ r4
 .long __machine_arch_type  @ r5
 .long cr_alignment   @ r6
 .long init_thread_union + THREAD_START_SP @ sp
__switch_data可以看成一个数据结构,首成员是一个函数指针(指向__mmap_switched函数)。
__mmap_switched:
 adr r3, __switch_data + 4
 ldmia r3!, {r4, r5, r6, r7}
 cmp r4, r5    @ 比较数据存放位置和数据起始位置
1: cmpne r5, r6 @不相等的话,比较数据段和bss段的起始地址是否相同
 ldrne fp, [r4], #4 @如果数据存放位置和数据起始位置不等且数据段起始位置
 strne fp, [r5], #4 @和bss段起始位置也不等则搬运数据
 bne 1b
 mov fp, #0         @ 清
1: cmp r6, r7       @ 空
 strcc fp, [r6],#4  @ bss
 bcc 1b               @ 段
 ldmia r3, {r4, r5, r6, sp} @r3现在指向__switch_data结构的processor_id成员
 str r9, [r4]   @ r9中是processor_id,先存放到__switch_data结构的processor_id成员指向的地址
 str r1, [r5]   @ r1是bootloader传过来的machine type,现存放到__switch_data结构的__machine_arch_type成员指向的地址
                   @ processor_id和__machine_arch_type是arch/arm/kernel/setup.c中定义的全局变量
 bic r4, r0, #CR_A   @ r0中存放的cp15中c1寄存器的值,现在打开地址对齐并保存到r4中
 stmia r6, {r0, r4}   @ cr_aligment和cr_no_alignment是定义在arch/arm/kernel/entry-armv.S中地址连续的两个全局变量
 b start_kernel
此后跳入start_kernel开启了真正的万里长征。



本PDF电子书包含上下两册,共1576页,带目录,高清非扫描版本。 作者: 毛德操 胡希明 丛书名: Linux内核源代码情景分析 出版社:浙江大学出版社 目录 第1章 预备知识 1.1 Linux内核简介. 1.2 Intel X86 CPU系列的寻址方式 1.3 i386的页式内存管理机制 1.4 Linux内核源代码中的C语言代码 1.5 Linux内核源代码中的汇编语言代码 第2章 存储管理 2.1 Linux内存管理的基本框架 2.2 地址映射的全过程 2.3 几个重要的数据结构和函数 2.4 越界访问 2.5 用户堆栈的扩展 2.6 物理页面的使用和周转 2.7 物理页面的分配 2.8 页面的定期换出 2.9 页面的换入 2.10 内核缓冲区的管理 2.11 外部设备存储空间的地址映射 2.12 系统调用brk() 2.13 系统调用mmap() 第3章 中断、异常和系统调用 3.1 X86 CPU对中断的硬件支持 3.2 中断向量表IDT的初始化 3.3 中断请求队列的初始化 3.4 中断的响应和服务 3.5 软中断与Bottom Half 3.6 页面异常的进入和返回 3.7 时钟中断 3.8 系统调用 3.9 系统调用号与跳转表 第4章 进程与进程调度 4.1 进程四要素 4.2 进程部曲:创建、执行与消亡 4.3 系统调用fork()、vfork()与clone() 4.4 系统调用execve() 4.5 系统调用exit()与wait4() 4.6 进程的调度与切换 4.7 强制性调度 4.8 系统调用nanosleep()和pause() 4.9 内核中的互斥操作 第5章 文件系统 5.1 概述 5.2 从路径名到目标节点 5.3 访问权限与文件安全性 5.4 文件系统的安装和拆卸 5.5 文件的打开与关闭 5.6 文件的写与读 5.7 其他文件操作 5.8 特殊文件系统/proc 第6章 传统的Unix进程间通信 6.1 概述 6.2 管道和系统调用pipe() 6.3 命名管道 6.4 信号 6.5 系统调用ptrace()和进程跟踪 6.6 报文传递 6.7 共享内存 6.8 信号量 第7章基于socket的进程间通信 7.1系统调用socket() 7.2函数sys—socket()——创建插口 7.3函数sys—bind()——指定插口地址 7.4函数sys—listen()——设定server插口 7.5函数sys—accept()——接受连接请求 7.6函数sys—connect()——请求连接 7.7报文的接收与发送 7.8插口的关闭 7.9其他 第8章设备驱动 8.1概述 8.2系统调用mknod() 8.3可安装模块 8.4PCI总线 8.5块设备的驱动 8.6字符设备驱动概述 8.7终端设备与汉字信息处理 8.8控制台的驱动 8.9通用串行外部总线USB 8.10系统调用select()以及异步输入/输出 8.11设备文件系统devfs 第9章多处理器SMP系统结构 9.1概述 9.2SMP结构中的互斥问题 9.3高速缓存与内存的一致性 9.4SMP结构中的中断机制 9.5SMP结构中的进程调度 9.6SMP系统的引导 第10章系统引导和初始化 10.1系统引导过程概述 10.2系统初始化(第一阶段) 10.3系统初始化(第二阶段) 10.4系统初始化(第阶段) 10.5系统的关闭和重引导
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值