`
isiqi
  • 浏览: 16562915 次
  • 性别: Icon_minigender_1
  • 来自: 济南
社区版块
存档分类
最新评论

ARM-linux启动的流程

阅读更多

ARM-linux启动的流程----转载
下面是我收藏的一篇好文章,就是忘记是那位仁兄,写的了,如果您正好看到这篇文章,请补注您的作者信息
------------------------------------
首先,porting linux的时候要规划内存影像,如小弟的系统有
64m SDRAM,
地址从0x 0800 0000 -0x0bff ffff,32m flash,地址从
0x0c00 0000-0x0dff ffff.
规划如下:bootloader, linux kernel, rootdisk放在flash里。

具体从 0x0c00 0000开始的第一个1M放bootloader,

0x0c10 0000
开始的2m放linux kernel,从 0x0c30 0000开始都给rootdisk。

启动:

首先,启动后arm920T将地址0x0c00 0000映射到0(可通过跳线设置),

实际上从0x0c00 0000启动,进入我们的bootloader,但由于flash速度慢,

所以bootloader前面有一小段程序把bootloader拷贝到SDRAM 中的0x0AFE0100,

再从0x 0800 0000 运行bootloader,我们叫这段小程序为
flashloader,
flashloader
必须要首先初始化SDRAM,不然往那放那些东东:

.equ SOURCE, 0x0C000100 bootloader
的存放地址

.equ TARGET, 0x0AFE0100
目标地址

.equ SDCTL0, 0x221000 SDRAM
控制器寄存器

// size is stored in location 0x0C0000FC
.global _start
_start: //
入口点

//;***************************************
//;* Init SDRAM
//;***************************************
// ;***************
// ;* SDRAM
// ;***************
LDR r1, =SDCTL0 //
// ; Set Precharge Command
LDR r3, =0x92120200
//ldr r3,=0x92120251
STR r3, [r1]
// ; Issue Precharge All Commad
LDR r3, =0x8200000
LDR r2, [r3]
// ; Set AutoRefresh Command
LDR r3, =0xA2120200
STR r3, [r1]
// ; Issue AutoRefresh Command
LDR r3, =0x8000000
LDR r2, [r3]
LDR r2, [r3]
LDR r2, [r3]
LDR r2, [r3]
LDR r2, [r3]
LDR r2, [r3]
LDR r2, [r3]
LDR r2, [r3]
// ; Set Mode Register
LDR r3, =0xB2120200
STR r3, [r1]
// ; Issue Mode Register Command
LDR r3, =0x08111800 //; Mode Register Value
LDR r2, [r3]
// ; Set Normal Mode
LDR r3, =0x82124200
STR r3, [r1]
//;***************************************
//;* End of SDRAM and SyncFlash Init *
//;***************************************
// copy code from FLASH to SRAM
_CopyCodes:
ldr r0,=SOURCE
ldr r1,=TARGET
sub r3,r0,#4
ldr r2,[r3]
_CopyLoop:
ldr r3,[r0]
str r3,[r1]
add r0,r0,#4
add r1,r1,#4
sub r2,r2,#4
teq r2,#0
beq _EndCopy
b _CopyLoop
_EndCopy:
ldr r0,=TARGET
mov pc,r0
上回书说到flashloader把bootloader load到0x0AFE0100, 然回跳了过去,

其实0x0AFE0100 就是烧在flash 0x0C000100中的真正的
bootloader:
bootloader
有几个文件组成,先是START.s,也是唯一的一个汇编程序,其余的都是C写成的,START.s主要初始化堆栈:

_start:
ldr r1,=StackInit
ldr sp,[r1]
b main
//
此处我们跳到了C代码的main函数,当C代码执行完后,还要调用

//
下面的JumpToKernel0x跳到LINXU kernel运行

.equ StackInitValue, __end_data+0x1000 // 4K __end_data
在连结脚本中指定

StackInit:
.long StackInitValue
.global JumpToKernel
JumpToKernel:
// jump to the copy code (get the arguments right)
mov pc, r0
.global JumpToKernel0x
// r0 = jump address
// r1-r4 = arguments to use (these get shifted)
JumpToKernel0x:
// jump to the copy code (get the arguments right)
mov r8, r0
mov r0, r1
mov r1, r2
mov r2, r3
mov r3, r4
mov pc, r8
.section ".data.boot"
.section ".bss.boot"
下面让我们看看bootloader的c代码干了些什么。main函数比较长,让我们分段慢慢看。

int main()
{
U32 *pSource, *pDestin, count;
U8 countDown, bootOption;
U32 delayCount;
U32 fileSize, i;
char c;
char *pCmdLine;
char *pMem;
init(); //
初始化FLASH控制器和CPU时钟

EUARTinit(); //
串口初始化

EUARTputString("\n\nDBMX1 Linux Bootloader ver 0.2.0\n");
EUARTputString("Copyright (C) 2002 Motorola Ltd.\n\n");
EUARTputString((U8 *)cmdLine);
EUARTputString("\n\n");
EUARTputString("Press any key for alternate boot-up options ... ");
小弟的bootloader主要干这么几件事:init(); 初始化硬件,打印一些信息和提供一些操作选项:

0. Program bootloader image
1. Program kernel image
2. Program root-disk image
3. Download kernel and boot from RAM
4. Download kernel and boot with ver 0.1.x bootloader format
5. Boot a ver0.1.x kernel
6. Boot with a different command line
也就是说,可以在bootloader里选择重新下载kernel,rootdisk并写入
flash,
下载的方法是用usb连接,10m的rootdisk也就刷的一下。关于usb下载的讨论请参看先前的贴子“为arm开发平台增加usb下载接口“。

如果不选,直接回车,就开始把整个linux的内核拷贝到SDRAM中运行。

列位看官,可能有人要问,在flashloader中不是已经初始化过sdram控制器了吗?怎么init(); 中还要初始化呢,各位有所不知,小弟用的是syncflash,

可以直接使用sdram控制器的接口,切记:在flash中运行的代码是不能初始化连接flash的sdram控制器的,不然绝对死掉了。所以,当程序在flash中运行的时候,去初始化sdram,而现在在sdram中运行,可放心大胆地初始化flash了,主要是设定字宽,行列延时,因为缺省都是最大的。

另外,如果列位看官的cpu有足够的片内ram,完全可以先把bootloader放在片内ram,干完一切后再跳到LINUX,小弟着也是不得已而为之啊。
如果直接输入回车,进入kernel拷贝工作:

EUARTputString("Copying kernel from Flash to RAM ...\n");
count = 0x200000; // 2 Mbytes
pSource = (U32 *)0x0C100000;
pDestin = (U32 *)0x08008000;
do
{
*(pDestin++) = *(pSource++);
count -= 4;
} while (count > 0);
}
EUARTputString("Booting kernel ...\n\n");
这一段没有什么可说的,运行完后kernel就在0x08008000了,至于为什么要

空出0x8000的一段,主要是放kelnel的一些全局数据结构,如内核页表,arm的页目录要有16k大。

我们知道,linux内核启动的时候可以传入参数,如在PC上,如果使用
LILO,
当出现LILO:,我们可以输入root=/dev/hda1.或mem=128M等指定文件系统的设备或内存大小,在嵌入式系统上,参数的传入是要靠bootloader完成的,

pMem = (char *)0x083FF000; //
参数字符串的目标存放地址

pCmdLine = (char *)&cmdLine; //
定义的静态字符串

while ((*(pMem++)=*(pCmdLine++)) != 0);//
拷贝

JumpToKernel((void *)0x8008000, 0x083FF000) ;//
跳转到内核

return (0);
JumpToKernel
在前文中的start.S定义过:

JumpToKernel:
// jump to the copy code (get the arguments right)
mov pc, r0
.global JumpToKernel0x
// r0 = jump address
// r1 = arguments to use (these get shifted)
由于arm-GCC的c参数调用的顺序是从左到右R0开始,所以R0是KERNKEL的地址,

r1
是参数字符串的地址:

到此为止,为linux引导做的准备工作就结束了,下一回我们就正式进入linux的代码。
好,从本节开始,我们走过了bootloader的漫长征途,开始进入linux的内核:

说实话,linux宝典的确高深莫测,洋人花了十几年修炼,各种内功心法层处不穷。有些地方反复推敲也领悟不了其中奥妙,炼不到第九重啊。。

linux
的入口是一段汇编代码,用于基本的硬件设置和建立临时页表,对于

ARM LINUX
是 linux/arch/arm/kernle/head-armv.S, 走!

#if defined(CONFIG_MX1)
mov r1, #MACH_TYPE_MX1
#endif
这第一句话好像就让人看不懂,好像葵花宝典开头的八个字:欲练神功。。。。那来的MACH_TYPE_MX1?其实,在head-armv.S 中的一项重要工作就是设置内核的临时页表,不然mmu开起来也玩不转,但是内核怎么知道如何映射内存呢?linux的内核将映射到虚地址0xCxxx xxxx处,但他怎么知道把哪一片ram映射过去呢? 因为不同的系统有不通的内存影像,所以,LINUX约定,内核代码开始的时候,R1放的是系统目标平台的代号,对于一些常见的,标准的平台,内核已经提供了支持,只要在编译的时候选中就行了,例如对X86平台,内核是从物理地址1M开始映射的。如果老兄是自己攒的平台,只好麻烦你自己写了。小弟拿人钱财,与人消灾,用的是摩托的MX1,只好自己写了,定义了#MACH_TYPE_MX1,当然,还要写一个描述平台的数据结构:

MACHINE_START(MX1ADS, "Motorola MX1ADS")
MAINTAINER("SPS Motorola")
BOOT_MEM(0x08000000, 0x00200000, 0xf0200000)
FIXUP(mx1ads_fixup)
MAPIO(mx1ads_map_io)
INITIRQ(mx1ads_init_irq)
MACHINE_END
看起来怪怪的,但现在大家只要知道他定义了基本的内存映象:RAM从0x08000000开始,i/o空间从0x00200000开始,i/o空间映射到虚拟地址空间 0xf0200000开始处。摩托的芯片i/o和内存是统一编址的。

其他的项,在下面的初始化过程中会逐个介绍到。

好了好了,再看下面的指令:

mov r0, #F_BIT | I_BIT | MODE_SVC @ make sure svc mode //
设置为SVC模式,允许中断和快速中断

//
此处设定系统的工作状态,arm有7种状态

//
每种状态有自己的堆栈

msr cpsr_c, r0 @ and all irqs diabled
bl __lookup_processor_type
//
定义处理器相关信息,如value, mask, mmuflags,

//
放在proc.info段中

//__lookup_processor_type
取得这些信息,在下面

//__lookup_architecture_type
中用

这一段是查询处理器的种类,大家知道arm有arm7, arm9等类型,如何区分呢?arm协处理器中有一个只读寄存器,存放处理器相关信息。__lookup_processor_type将返回如下的结构:

__arm920_proc_info:
.long 0x41009200 //CPU id
.long 0xff00fff0 //cpu mask
.long 0x00000c1e @ mmuflags
b __arm920_setup
.long cpu_arch_name
.long cpu_elf_name
.long HWCAP_SWP | HWCAP_HALF | HWCAP_26BIT
.long cpu_arm920_info
.long arm920_processor_functions
第一项是CPU id,将与协处理器中读出的id作比较,其余的都是与处理器相关的信息,到下面初始化的过程中自然会用到。。
查询到了处理器类型和系统的内存映像后就要进入初始化过程中比较关键的一步了,开始设置mmu,但首先要设置一个临时的内核页表,映射4m的内存,这在初始化过程中是足够了:

//r5=0800 0000 ram
起始地址 r6=0020 0000 io地址,r7=f020 0000 虚
io
teq r7, #0 @ invalid architecture?
moveq r0, #'a' @ yes, error 'a'
beq __error
bl __create_page_tables
其中__create_page_tables为:

__create_page_tables:
pgtbl r4
//r4=0800 4000
临时页表的起始地址

//r5=0800 0000, ram
的起始地址

//r6=0020 0000, i/o
寄存器空间的起始地址

//r7=0000 3c08
//r8=0000 0c1e
//the page table in 0800 4000 is just temp base page, when init_task's sweaper_page_dir ready,the temp page will be useless
// the high 12 bit of virtual address is base table index, so we need 4kx4 = 16k temp base page,
mov r0, r4
mov r3, #0
add r2, r0, #0x4000 @ 16k of page table
1: str r3, [r0], #4 @ Clear page table
str r3, [r0], #4
str r3, [r0], #4
str r3, [r0], #4
teq r0, r2
bne 1b
/*
* Create identity mapping for first MB of kernel.
* This is marked cacheable and bufferable.
*
* The identity mapping will be removed by
*/
//
由于linux编译的地址是0xC0008000,load的地址是0x08008000,我们需要将虚地址0xC0008000映射到0800800一段

//
同时,由于部分代码也要直接访问0x08008000,所以0x08008000对应的表项也要填充

//
页表中的表象为section,AP=11表示任何模式下可访问,domain为0。

add r3, r8, r5 @ mmuflags + start of RAM
//r3=0800 0c1e
add r0, r4, r5, lsr #18
//r0=0800 4200
str r3, [r0] @ identity mapping
//*0800 4200 = 0800 0c1e 0x200
表象 对应的是0800 0000 的
1m
/*
* Now setup the pagetables for our kernel direct
* mapped region. We round TEXTADDR down to the
* nearest megabyte boundary.
*/
//
下面是映射
4M
add r0, r4, #(TEXTADDR & 0xfff00000) >> 18 @ start of kernel
//r0 = r4+ 0x3000 = 0800 4000 + 3000 = 0800 7000
str r3, [r0], #4 @ PAGE_OFFSET + 0MB
//*0800 7004 = 0800 0c1e
add r3, r3, #1 子程序返回。

下一回就要开始打开mmu的操作了
上回书讲到已经设置好了内核的页表,然后要跳转到__arm920_setup

这个函数在
arch/arm/mm/proc-arm929.s
__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
mcr p15, 0, r0, c8, c7 @ invalidate I,D TLBs on v4
mcr p15, 0, r4, c2, c0 @ load page table pointer
mov r0, #0x1f @ Domains 0, 1 = client
mcr p15, 0, r0, c3, c0 @ load domain access register
mrc p15, 0, r0, c1, c0 @ get control register v4
/*
* Clear out 'unwanted' bits (then put them in if we need them)
*/
@ VI ZFRS BLDP WCAM
bic r0, r0, #0x0e00
bic r0, r0, #0x0002
bic r0, r0, #0x000c
bic r0, r0, #0x1000 @ ...0 000. .... 000.
/*
* Turn on what we want
*/
orr r0, r0, #0x0031
orr r0, r0, #0x2100 @ ..1. ...1 ..11 ...1
#ifdef CONFIG_CPU_ARM920_D_CACHE_ON
orr r0, r0, #0x0004 @ .... .... .... .1..
#endif
#ifdef CONFIG_CPU_ARM920_I_CACHE_ON
orr r0, r0, #0x1000 @ ...1 .... .... ....
#endif
mov pc, lr
这一段首先关闭i,d cache,清除write buffer ,然后设置页目录地址,设置 domain的保护,在上节中,注意到页目录项的domain都是0,domain寄存器中的domain 0 对应的是0b11,表示访问模式为manager,不受限制。

接下来设置控制寄存器,打开d,i cache和mmu 注意arm的d cache必须和mmu一起打开,而i cache可以单独打开 。其实,cache和mmu的关系实在是紧密,每一个页表项都有标志标示是否是 cacheable的,可以说本来就是设计一起使用的

最后,自函数返回后,有一句

mcr p15, 0, r0, c1, c0
使设置生效。
上回我们讲到arm靠初始化完成了,打开了cache, 到此为止,汇编部分的初始化代码就差不多了,最后还有几件事情做:

1
。初始化BSS段,全部清零,BSS是全局变量区域。

2
。保存与系统相关的信息:如

.long SYMBOL_NAME(compat)
.long SYMBOL_NAME(__bss_start)
.long SYMBOL_NAME(_end)
.long SYMBOL_NAME(processor_id)
.long SYMBOL_NAME(__machine_arch_type)
.long SYMBOL_NAME(cr_alignment)
.long SYMBOL_NAME(init_task_union)+8192
不用讲,大家一看就明白意思

3
。重新设置堆栈指针,指向init_task的堆栈。init_task是系统的第一个任务,init_task的堆栈在task structure的后8K,我们后面会看到。

4
。最后就要跳到C代码的start_kernel。
b SYMBOL_NAME(start_kernel)
现在让我们来回忆一下目前的系统状态:

@
临时页表已经建立,在0X08004000处,映射了4M,虚地址0XC000000被映射到
0X08000000.
@CACHE,MMU
都已经打开。

@
堆栈用的是任务init_task的堆栈。

如果以为到了c代码可以松一口气的话,就大错特措了,linux的c也不比汇编好懂多少,相反到掩盖了汇编的一些和机器相关的部分,有时候更难懂。其实作为编写操作系统的c代码,只不过是汇编的另一种写法,和机器代码的联系是很紧密的。

start_kernel
在 /linux/init/main.c中定义:

asmlinkage void __init start_kernel(void)
{
char * command_line;
unsigned long mempages;
extern char saved_command_line[];
lock_kernel();
printk(linux_banner);
setup_arch(&command_line); //arm/kernel/setup.c
printk("Kernel command line: %s\n", saved_command_line);
parse_options(command_line);
trap_init(); // arm/kernle/traps.c install
。。。。。。。。。

start_kernel
中的函数个个都是重量级的,首先用printk(linux_banner);打出系统版本号,这里面就大有文章,系统才刚开张,你让他打印到哪里去呢?先给大家交个底,以后到console的部分自然清楚,printk和printf不同,他首先输出到系统的一个缓冲区内,大约4k,如果登记了console,则调用console->wirte函数输出,否则就一直在buffer里呆着。所以,用printk输出的信息,如果超出了4k,会冲掉前面的。在系统引导起来后,用dmesg看的也就是这个buffer中的东东。
下面就是一个重量级的函数:

setup_arch(&command_line); //arm/kernel/setup.c
完成内存映像的初始化,其中command_line是从bootloader中传下来的。

void __init setup_arch(char **cmdline_p)
{
struct param_struct *params = NULL;
struct machine_desc *mdesc; //arch structure, for your ads, defined in include/arm-asm/mach/arch.h very long
struct meminfo meminfo;
char *from = default_command_line;
memset(&meminfo, 0, sizeof(meminfo));
首先把meminfo清零,有个背景介绍一下,从linux 2.4的内核开始,支持内存的节点(node),也就是可支持不连续的物理内存区域。这一点在嵌入式系统中很有用,例如对于SDRAM和FALSH,性质不同,可作为不同的内存节点。

meminfo
结构定义如下:

/******************************************************/
#define NR_BANKS 4
//define the systen mem region, not consistent
struct meminfo {
int nr_banks;
unsigned long end;
struct {
unsigned long start;
unsigned long size;
int node;
} bank[NR_BANKS];
};
/******************************************************/
下面是:
ROOT_DEV = MKDEV(0, 255);
ROOT_DEV
是宏,指明启动的设备,嵌入式系统中通常是
flash disk.
这里面有一个有趣的悖论:linux的设备都是在/dev/下,访问这些设备文件需要设备驱动程序支持,而访问设备文件才能取得设备号,才能加载驱动程序,那么第一个设备驱动程序是怎么加载呢?就是ROOT_DEV, 不需要访问设备文件,直接指定设备号。

下面我们准备初始化真正的内核页表,而不再是临时的了。

首先还是取得当前系统的内存映像:

mdesc = setup_architecture(machine_arch_type);
//find the machine type in mach-integrator/arch.c
//the ads name, mem map, io map
返回如下结构:

mach-integrator/arch.c
MACHINE_START(INTEGRATOR, "Motorola MX1ADS")
MAINTAINER("ARM Ltd/Deep Blue Solutions Ltd")
BOOT_MEM(0x08000000, 0x00200000, 0xf0200000)
FIXUP(integrator_fixup)
MAPIO(integrator_map_io)
INITIRQ(integrator_init_irq)
MACHINE_END
我们在前面介绍过这个结构,不过这次用它可是玩真的了。
书接上回,

下面是init_mm的初始化,init_mm定义在/arch/arm/kernel/init_task.c:

struct mm_struct init_mm = INIT_MM(init_mm);
从本回开始的相当一部分内容是和内存管理相关的,凭心而论,操作系统的 内存管理是很复杂的,牵扯到处理器的硬件细节和软件算法,限于篇幅所限制,请大家先仔细读一读arm mmu的部分,中文参考资料:linux内核源代码情景对话,linux2.4.18原代码分析。

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;
内核数据段结束

每一个任务都有一个mm_struct结构管理任务内存空间,init_mm是内核的mm_struct,其中设置成员变量* mmap指向自己,意味着内核只有一个内存管理结构,设置* pgd=swapper_pg_dir,

swapper_pg_dir
是内核的页目录,在arm体系结构有16k,所以init_mm定义了整个kernel的内存空间,下面我们会碰到内核线程,所有的内核线程都使用内核空间,拥有和内核同样的访问

权限。
memcpy(saved_command_line, from, COMMAND_LINE_SIZE);
//clear command array
saved_command_line[COMMAND_LINE_SIZE-1] = '\0';
//set the end flag
parse_cmdline(&meminfo, cmdline_p, from);
//
将bootloader的参数拷贝到cmdline_p,

bootmem_init(&meminfo);
定义在
arm/mm/init.c
这个函数在内核结尾分一页出来作位图,根据具体系统的内存大小映射整个ram

下面是一个非常重要的函数

paging_init(&meminfo, mdesc);
定义在arm/mm/init.c 创建内核页表,映射所有物理内存和io空间,对于不同的处理器,这个函数差别很大,
void __init paging_init(struct meminfo *mi, struct machine_desc *mdesc)
{
void *zero_page, *bad_page, *bad_table;
int node;
//static struct meminfo meminfo __initdata = { 0, };
memcpy(&meminfo, mi, sizeof(meminfo));
/*
* allocate what we need for the bad pages.
* note that we count on this going ok.
*/
zero_page = alloc_bootmem_low_pages(PAGE_SIZE);
bad_page = alloc_bootmem_low_pages(PAGE_SIZE);
bad_table = alloc_bootmem_low_pages(TABLE_SIZE);
分配三个页出来,用于处理异常过程,在armlinux中,得到如下

地址:

zero_page=0xc0000000
bad page=0xc0001000
bad_table=0xc0002000
上回我们说到在paging_init中分配了三个页:

zero_page=0xc0000000
bad page=0xc0001000
bad_table=0xc0002000
但是奇怪的很,在更新的linux代码中只分配了一个zero_page,而且在源代码中找不到zero_page用在什么地方了,大家讨论讨论吧。

paging_init
的主要工作是在
void __init memtable_init(struct meminfo *mi)
中完成的,为系统内存创建页表:
meminfo结构如下:

struct meminfo {
int nr_banks;
unsigned long end;
struct {
unsigned long start;
unsigned long size;
int node;
} bank[NR_BANKS];
};
是用来纪录系统中的内存区段的,因为在嵌入式 系统中并不是所有的内存都能映射,例如sdram只有64m,flash 32m,而且不见得是连续的,所以用meminfo纪录这些区段。

void __init memtable_init(struct meminfo *mi)
{
struct map_desc *init_maps, *p, *q;
unsigned long address = 0;
int i;
init_maps = p = alloc_bootmem_low_pages(PAGE_SIZE);
其中map_desc定义为:

struct map_desc {
unsigned long virtual;
unsigned long physical;
unsigned long length;
int domain:4, //
页表的
domain
prot_read:1, //
保护标志

prot_write:1, //
写保护标志

cacheable:1, //
是否
cache
bufferable:1, //
是否用
write buffer
last:1; //

};init_maps
map_desc
是区段及其属性的定义,属性位的意义请参考ARM MMU的介绍。

下面对meminfo的区段进行遍历,同时填写init_maps 中的各项内容:

for (i = 0; i nr_banks; i++) {
if (mi->bank.size == 0)
continue;
p->physical = mi->bank.start;
p->virtual = __phys_to_virt(p->physical);
p->length = mi->bank.size;
p->domain = DOMAIN_KERNEL;
p->prot_read = 0;
p->prot_write = 1;
p->cacheable = 1; //
可以
CACHE
p->bufferable = 1; //
使用
write buffer
p ++; //
下一个区段

}
如果系统有
flash,
#ifdef FLUSH_BASE
p->physical = FLUSH_BASE_PHYS;
p->virtual = FLUSH_BASE;
p->length = PGDIR_SIZE;
p->domain = DOMAIN_KERNEL;
p->prot_read = 1;
p->prot_write = 0;
p->cacheable = 1;
p->bufferable = 1;
p ++;
#endif
其中的prot_read和prot_write是用来设置页表的domain的,

下面就是逐个区段建立页表:

q = init_maps;
do {
if (address virtual || q == p) {
clear_mapping(address);
address += PGDIR_SIZE;
} else {
create_mapping(q);
address = q->virtual + q->length;
address = (address + PGDIR_SIZE - 1) & PGDIR_MASK;
q ++;
}
} while (address != 0);
上次说到memtable_init中初始化页表的循环,

这个过程比较重要,我们看仔细些:

q = init_maps;
do {
if (address virtual || q == p) {
//
由于内核空间是从c000 0000开始,所以
c000 0000
//
以前的页表项全部清空

clear_mapping(address);
address += PGDIR_SIZE;
//
每个表项增加1m,这里感到了section的好处

}
其中clear_mapping()是个宏,根据处理器的不同,在920下被展开为

cpu_arm920_set_pmd(((pmd_t *)(((&init_mm )->pgd+
(( virt) >> 20 )))),((pmd_t){( 0 )}));
其中init_mm为内核的mm_struct,pgd指向swapper_pg_dir,在arch/arm/kernel/init_task.c中定义
ENTRY(cpu_arm920_set_pmd)
#ifdef CONFIG_CPU_ARM920_WRITETHROUGH
eor r2, r1, #0x0a
tst r2, #0x0b
biceq r1, r1, #4
#endif
str r1, [r0]
把pmd_t填写到页表项中,由于pmd_t=0,

实际等于清除了这一项,由于d cache打开,

这一条指令实际并没有写回内存,而是写到cache中

mcr p15, 0, r0, c7, c10, 1
把cache中 地址r0对应的内容写回内存中,

这一条语句实际是写到了write buffer中
,
还没有真正写回内存。

mcr p15, 0, r0, c7, c10, 4
等待把write buffer中的内容写回内存。在这之前core等待

mov pc, lr
在这里我们看到,由于页表的内容十分关键,为了确保写回内存,

采用了直接操作cache的方法。由于在arm core中,打开了
d cache
则必定要用write buffer.所以还有wb的回写问题。

由于考虑到效率,我们使用了cache和
buffer,
所以在某些地方要用指令保证数据被及时写回。

下面映射c000 0000后面的页表

else {
create_mapping(q);
address = q->virtual + q->length;
address = (address + PGDIR_SIZE - 1) & PGDIR_MASK;
q ++;
}
} while (address != 0);
create_mapping
也在mm-armv.c中定义;

static void __init create_mapping(struct map_desc *md)
{
unsigned long virt, length;
int prot_sect, prot_pte;
long off;
prot_pte = L_PTE_PRESENT | L_PTE_YOUNG | L_PTE_DIRTY |
(md->prot_read ? L_PTE_USER : 0) |
(md->prot_write ? L_PTE_WRITE : 0) |
(md->cacheable ? L_PTE_CACHEABLE : 0) |
(md->bufferable ? L_PTE_BUFFERABLE : 0);
prot_sect = PMD_TYPE_SECT | PMD_DOMAIN(md->domain) |
(md->prot_read ? PMD_SECT_AP_READ : 0) |
(md->prot_write ? PMD_SECT_AP_WRITE : 0) |
(md->cacheable ? PMD_SECT_CACHEABLE : 0) |
(md->bufferable ? PMD_SECT_BUFFERABLE : 0);
由于arm中section表项的权限位和page表项的位置不同,

所以根据struct map_desc 中的保护标志,分别计算页表项

中的AP,domain,CB标志位。
有一段时间没有写了,道歉先,前一段时间在做arm linux的xip,终于找到了

在flash中运行kernel的方法,同时对系统的存储管理
的理解更深了一层,我们继续从上回的create_mapping往下看:
while ((virt & 0xfffff || (virt + off) & 0xfffff) && length >= PAGE_SIZE)
{
alloc_init_page(virt, virt + off, md->domain, prot_pte);
virt += PAGE_SIZE;
length -= PAGE_SIZE;
}
while (length >= PGDIR_SIZE)

{
alloc_init_section(virt, virt + off, prot_sect);
virt += PGDIR_SIZE;
length -= PGDIR_SIZE;
}
while (length >= PAGE_SIZE)

{
alloc_init_page(virt, virt + off, md->domain, prot_pte);
virt += PAGE_SIZE;
length -= PAGE_SIZE;
}
这3个循环的设计还是很巧妙的,create_mapping的作用是设置虚地址virt 到物理地址virt + off的映射页目录和页表。arm提供了4种尺寸的页表:

1M,4K,16K,64K,armlinux
只用到了1M和4K两种。
这3个while的作用分别是“掐头“,“去尾“,“砍中间“。

第一个while是判断要映射的地址长度是否大于1m,且是不是1m对齐,如果不是,则需要创建页表,例如,如果要映射的长度为1m零4k,则先要将“零头“去掉,4k的一段需要中间页表,通过第一个while创建中间页表,而剩下的1M则交给第二个while循环。最后剩下的交给第三个while循环。
alloc_init_page分配并填充中间页表项

static inline voidalloc_init_page(unsigned long virt, unsigned long phys, int domain, int prot)
{
pmd_t *pmdp;
pte_t *ptep;
pmdp = pmd_offset(pgd_offset_k(virt), virt);//
返回页目录中virt对应的表项

if (pmd_none(*pmdp))

{//如果表项是空的,则分配一个中间页表

pte_t *ptep = alloc_bootmem_low_pages(2 * PTRS_PER_PTE *sizeof(pte_t));
ptep += PTRS_PER_PTE;
//
设置页目录表项

set_pmd(pmdp, __mk_pmd(ptep, PMD_TYPE_TABLE | PMD_DOMAIN(domain)));

}
ptep = pte_offset(pmdp, virt);
//如果表项不是空的,则表项已经存在,只需要设置中间页表表项
set_pte(ptep, mk_pte_phys(phys, __pgprot(prot)));
}

alloc_init_section只需要填充页目录项

alloc_init_section(unsigned long virt, unsigned long phys, int prot)
{
pmd_t pmd;
pmd_val(pmd) = phys | prot; //
将物理地址和保护标志合成页目录项

set_pmd(pmd_offset(pgd_offset_k(virt), virt), pmd);
}
通过create_mapping可为内核建立所有的地址映射,最后是映射中断向量表所在的区域:

init_maps->physical = virt_to_phys(init_maps);
init_maps->virtual = vectors_base();
init_maps->length = PAGE_SIZE;
init_maps->domain = DOMAIN_USER;
init_maps->prot_read = 0;
init_maps->prot_write = 0;
init_maps->cacheable = 1;
init_maps->bufferable = 0;
create_mapping(init_maps);
中断向量表的虚地址init_maps,是用alloc_bootmem_low_pages分配的,通常是在c000 8000前面的某一页,vectors_base()是个宏,arm规定中断向量表的地址只能是0或ffff0000,在cp15中设置。所以上述代码映射一页到0或ffff0000,下面我们还会看到,中断处理程序中的汇编部分也被拷贝到这一页中。
-=-=-=-=-=>

分享到:
评论

相关推荐

    arm-linux启动流程分析.pdf

    arm-linux 启动流程分析 一、概述 arm-linux 启动流程分析是指从 ARM 处理器上电到进入用户 shell 模式的整个过程。该过程可以分为几个阶段,包括内核启动、init 进程启动、inittab 文件解析、系统服务启动等。 ...

    arm-linux-gnueabihf-gdb-8.2_Ubuntu16.04.tar.gz

    此外,arm-linux-gnueabihf-gdb还支持GDB服务器模式,可以在本地调试远程设备上的程序,简化了调试流程。配合gdbserver工具,可以在ARM设备上启动一个GDB服务,然后在主机上的arm-linux-gnueabihf-gdb中连接这个服务...

    arm-linux-gcc 裸机程序

    4. u-boot启动流程和引导加载程序原理 5. 嵌入式程序的下载与执行 6. GPIO驱动编程和硬件控制 7. SDRAM的使用和内存管理 8. JTAG调试技术 这些内容对于学习嵌入式系统开发,特别是ARM平台的软件和硬件交互具有重要...

    arm-linux-gcc_4.5.1

    2. **在主机上启动GDB**: `arm-linux-gdb program`,加载源码,并连接到目标设备的gdbserver:`target remote device_ip:port`。 3. **设置断点、查看内存、单步执行等操作**: 通过GDB提供的命令,可以对目标程序...

    arm-linux-gdb

    ### ARM-Linux-GDB 构建与调试指南 #### 概述 在嵌入式开发领域,ARM架构因其低功耗、高性能等特性被广泛应用。为了有效地进行ARM平台上的程序调试,开发人员通常会选择使用ARM-Linux-GDB(以下简称GDB)。本文将...

    arm-linux-gdb(v7.11)

    然后,在开发主机上启动`arm-linux-gdb`,连接到目标系统,加载要调试的程序的调试信息(`.debug`或`.pdb`文件),设置断点,开始单步执行或运行程序,查看变量值,分析调用栈,等等。 6. **配合其他工具**:`arm-...

    arm-linux入门

    Bootloader 是引导加载器,负责启动 ARM 设备并加载 Linux 内核;Linux 内核是操作系统的核心,处理硬件资源管理和调度任务;Rootfile 系统则包含了操作系统启动所需的基础文件和服务。理解这三个部分的交互,是成功...

    arm-linux-uboot

    《arm-linux-uboot》是针对嵌入式系统开发者的一本实用教程,主要聚焦于U-Boot(统一的bootloader)在ARM架构Linux系统中的移植与应用。这本书以实践为导向,旨在帮助读者深入理解U-Boot的工作原理,并掌握在实际...

    ucosii mini2440 arm-linux-gcc 编译

    《深入解析ucosii在Mini2440上的arm-linux-gcc编译实践》 在嵌入式系统开发中,UCOSII操作系统以其轻量级、高效的特点被广泛应用。本篇将详细介绍如何在Mini2440开发板上,利用arm-linux-gcc工具链进行UCOSII的编译...

    图解ARM-Linux的启动全过程

    图解ARM-Linux的启动全过程:从内核的自解压到引导阶段,再到内核初始化,自后是文件系统的初始化。

    arm-linux开发环境搭建

    以上就是ARM-Linux开发环境的基本搭建流程。这个过程中可能会遇到各种问题,如驱动不兼容、编译错误等,需要耐心调试和查阅文档。随着经验的积累,你会逐渐熟悉整个流程,并能快速应对各种挑战。希望这篇指南对你在...

    arm-linux-gcc 裸机程序(Nandflash启动)

    **标题:“arm-linux-gcc 裸机程序(Nandflash启动)”** 该标题指出,我们关注的是一个使用`arm-linux-gcc`编译器为`Mini2440`开发板创建的裸机程序,这个程序设计为从Nandflash启动。`arm-linux-gcc`是用于ARM...

    浅谈分析Arm-linux分析

    此外,对于Linux内核的裁剪和驱动开发,理解系统初始化流程也有助于优化系统资源利用,提高运行效率。 在进行内核移植时,需要关注`/arch/arm/configs`目录下的配置文件,它们定义了特定硬件平台的内核配置。同时,...

    ARM-Linux内核启动的分析

    ARM-Linux内核启动是一个复杂而精细的过程,本文将重点分析`arch/arm/kernel/head-armv.S`文件,该文件作为整个内核启动的入口点,在Bootloader执行完毕后直接控制着后续的执行流程。在分析过程中,还会涉及其他关键...

    做Arm-Linux时,用的Linux 课件

    理解Bootloader的工作流程和如何为Arm平台编写或配置Bootloader是Arm-Linux开发的基础。 4. 文件系统: Linux文件系统是组织和存储数据的核心部分。在Arm-Linux中,通常会使用各种文件系统类型,如ext4、FAT32等。...

    如何快速上手嵌入式Arm-Linux.pdf

    ### 如何快速上手嵌入式Arm-Linux #### 前言 嵌入式Arm-Linux技术在当今社会的应用非常广泛,从智能手机、平板电脑到各种物联网设备,几乎无处不在。对于初学者而言,如何高效地学习并掌握这项技术显得尤为重要。...

Global site tag (gtag.js) - Google Analytics