首页 | 新闻 | 新品 | 文库 | 方案 | 视频 | 下载 | 商城 | 开发板 | 数据中心 | 座谈新版 | 培训 | 工具 | 博客 | 论坛 | 百科 | GEC | 活动 | 主题月 | 电子展
返回列表 回复 发帖

linux启动代码设计参考

linux启动代码设计参考

     基于ARM的芯片多数为复杂的片上系统,这种复杂系统里的多数硬件模块都是可配置的,需要由软件来设置其需要的工作状态。因此在用户的应用程序之前,需要由专门的一段代码来完成对系统的初始化。由于这类代码直接面对处理器内核和硬件控制器进行编程,一般都是用汇编语言。一般通用的内容包括:
中断向量表


初始化存储器系统


初始化堆栈


初始化有特殊要求的断口,设备


初始化用户程序执行环境


改变处理器模式


呼叫主应用程序


1. 中断向量表


ARM要求中断向量表必须放置在从0地址开始,连续8X4字节的空间内。


每当一个中断发生以后,ARM处理器便强制把PC指针置为向量表中对应中断类型的地址值。因为每个中断只占据向量表中1个字的存储空间,只能放置一条ARM指令,使程序跳转到存储器的其他地方,再执行中断处理。


中断向量表的程序实现通常如下表示:


AREA Boot ,CODE, READONLY


ENTRY


B    ResetHandler


B    UndefHandler


B    SWIHandler


B    PreAbortHandler


B    DataAbortHandler


B


B    IRQHandler


B    FIQHandler


其中关键字ENTRY是指定编译器保留这段代码,因为编译器可能会认为这是一段亢余代码而加以优化。链接的时候要确保这段代码被链接在0地址处,并且作为整个程序的入口。


2. 初始化存储器系统


(1)存储器类型和时序配置


通常Flash和SRAM同属于静态存储器类型,可以合用同一个存储器端口;而DRAM因为有动态刷新和地址线复用等特性,通常配有专用的存储器端口。


存储器端口的接口时序优化是非常重要的,这会影响到整个系统的性能。因为一般系统运行的速度瓶颈都存在于存储器访问,所以存储器访问时序应尽可能的快;而同时又要考虑到由此带来的稳定性问题。


(2)存储器地址分布


一种典型的情况是启动ROM的地址重映射。

51 c8051f(f020,f040) msp430 arm(2410,2510) fpga(xc3s4000) dsp(5116 dm642) keilc vc++ matlab linux protel Ten_layerPCB mpegx h.26x Rscode Turbocode ofdm VideoBroadcasting ldpc_code(now!)
3. 初始化堆栈

因为ARM有7种执行状态,每一种状态的堆栈指针寄存器(SP)都是独立的。因此,对程序中需要用到的每一种模式都要给SP定义一个堆栈地址。方法是改变状态寄存器内的状态位,使处理器切换到不同的状态,让后给SP赋值。注意:不要切换到User模式进行User模式的堆栈设置,因为进入User模式后就不能再操作CPSR回到别的模式了,可能会对接下去的程序执行造成影响。

这是一段堆栈初始化的代码示例,其中只定义了三种模式的SP指针:

MRS R0,CPSR

BIC R0,R0,#MODEMASK 安全起见,屏蔽模式位以外的其他位

ORR R1,R0,#IRQMODE

MSR CPSR_cxfs,R1

LDR SP,=UndefStack



ORR R1,R0,#FIQMODE

MSR CPSR_cxsf,R1

LDR SP,=FIQStack



ORR R1,R0,#SVCMODE

MSR CPSR_cxsf,R1

LDR SP,=SVCStack

4. 初始化有特殊要求的端口,设备

5. 初始化应用程序执行环境

映像一开始总是存储在ROM/Flash里面的,其RO部分即可以在ROM/Flash里面执行,也可以转移到速度更快的RAM中执行;而RW和ZI这两部分是必须转移到可写的RAM里去。所谓应用程序执行环境的初始化,就是完成必要的从ROM到RAM的数据传输和内容清零。

下面是在ADS下,一种常用存储器模型的直接实现:

LDR r0,=|Image$$RO$$Limit| ;得到RW数据源的起始地址

LDR r1,=|Image$$RW$$Base| ;RW区在RAM里的执行区起始地址

LDR r2,=|Image$$ZI$$Base| ;ZI区在RAM里面的起始地址

CMP r0,r1 ;比较它们是否相等

BEQ %F1

0 CMP r1,r3

LDRCC r2,[r0],#4

STRCC r2,[r1],#4

BCC %B0

1 LDR r1,=|Image$$ZI$$Limit|

MOV r2,#0

2 CMP r3,r1

STRCC r2,[r3],#4

BCC %B2

程序实现了RW数据的拷贝和ZI区域的清零功能。其中引用到的4个符号是由链接器第一输出的。

|Image$$RO$$Limit|:表示RO区末地址后面的地址,即RW数据源的起始地址

|Image$$RW$$Base|:RW区在RAM里的执行区起始地址,也就是编译器选项RW_Base指定的地址

|Image$$ZI$$Base|:ZI区在RAM里面的起始地址

|Image$$ZI$$Limit|:ZI区在RAM里面的结束地址后面的一个地址

程序先把ROM里|Image$$RO$$Limt|开始的RW初始数据拷贝到RAM里面|Image$$RW$$Base|开始的地址,当RAM这边的目标地址到达|Image$$ZI$$Base|后就表示RW区的结束和ZI区的开始,接下去就对这片ZI区进行清零操作,直到遇到结束地址|Image$$ZI$$Limit|

6. 改变处理器模式

因为在初始化过程中,许多操作需要在特权模式下才能进行(比如对CPSR的修改),所以要特别注意不能过早的进入用户模式。

内核级的中断使能也可以考虑在这一步进行。如果系统中另外存在一个专门的中断控制器,这么做总是安全的。

7. 呼叫主应用程序

当所有的系统初始化工作完成之后,就需要把程序流程转入主应用程序。最简单的一种情况是:

IMPORT main

B main

直接从启动代码跳转到应用程序的主函数入口,当然主函数名字可以由用户随便定义。

在ARM ADS环境中,还另外提供了一套系统级的呼叫机制。

IMPORT __main

B __main

__main()是编译系统提供的一个函数,负责完成库函数的初始化和初始化应用程序执行环境,最后自动跳转到main()函数。
51 c8051f(f020,f040) msp430 arm(2410,2510) fpga(xc3s4000) dsp(5116 dm642) keilc vc++ matlab linux protel Ten_layerPCB mpegx h.26x Rscode Turbocode ofdm VideoBroadcasting ldpc_code(now!)
这里面有一个有趣的悖论: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

我们在前面介绍过这个结构,不过这次用它可是玩真的了。
51 c8051f(f020,f040) msp430 arm(2410,2510) fpga(xc3s4000) dsp(5116 dm642) keilc vc++ matlab linux protel Ten_layerPCB mpegx h.26x Rscode Turbocode ofdm VideoBroadcasting ldpc_code(now!)
书接上回,
下面是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
51 c8051f(f020,f040) msp430 arm(2410,2510) fpga(xc3s4000) dsp(5116 dm642) keilc vc++ matlab linux protel Ten_layerPCB mpegx h.26x Rscode Turbocode ofdm VideoBroadcasting ldpc_code(now!)
上回我们说到在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的介绍。
51 c8051f(f020,f040) msp430 arm(2410,2510) fpga(xc3s4000) dsp(5116 dm642) keilc vc++ matlab linux protel Ten_layerPCB mpegx h.26x Rscode Turbocode ofdm VideoBroadcasting ldpc_code(now!)
下面对meminfo的区段进行遍历,同时填写init_maps
中的各项内容:
for (i = 0; i < mi->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 < q->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);
51 c8051f(f020,f040) msp430 arm(2410,2510) fpga(xc3s4000) dsp(5116 dm642) keilc vc++ matlab linux protel Ten_layerPCB mpegx h.26x Rscode Turbocode ofdm VideoBroadcasting ldpc_code(now!)
上次说到memtable_init中初始化页表的循环,
这个过程比较重要,我们看仔细些:


q = init_maps;
do {
if (address < q->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标志位。
51 c8051f(f020,f040) msp430 arm(2410,2510) fpga(xc3s4000) dsp(5116 dm642) keilc vc++ matlab linux protel Ten_layerPCB mpegx h.26x Rscode Turbocode ofdm VideoBroadcasting ldpc_code(now!)
有一段时间没有写了,道歉先,前一段时间在做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循环。
51 c8051f(f020,f040) msp430 arm(2410,2510) fpga(xc3s4000) dsp(5116 dm642) keilc vc++ matlab linux protel Ten_layerPCB mpegx h.26x Rscode Turbocode ofdm VideoBroadcasting ldpc_code(now!)
alloc_init_page分配并填充中间页表项
static inline void
alloc_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,下面我们还会看到,中断处理程序中的汇编部分也被拷贝到
这一页中。
51 c8051f(f020,f040) msp430 arm(2410,2510) fpga(xc3s4000) dsp(5116 dm642) keilc vc++ matlab linux protel Ten_layerPCB mpegx h.26x Rscode Turbocode ofdm VideoBroadcasting ldpc_code(now!)
返回列表