⑴ 内存映射的初始化
内核在进入保护模式前,还没有启用分页功能,在这之前内核要先建立一个临时内核页表,因为在进入保护模式后,内核继续初始化直到建
立完整的内存映射机制之前,仍然需要用到页表来映射相应的内存地址。临时页表的初始化是在arch/i386/kernel/head.S中进行的:
swapper_pg_dir是临时页全局目录表,它是在内核编译过程中静态初始化的.
pg0是第一个页表开始的地方,它也是内核编译过程中静态初始化的.
内核通过以下代码建立临时页表:
ENTRY(startup_32)
…………
/*得到开始目录项的索引,从这可以看出内核是在swapper_pg_dir的768个表项开始进行建立的,其对应的线性地址就是0xc0000000以上的地
址,也就是内核在初始化它自己的页表*/
page_pde_offset=(__PAGE_OFFSET>>20);
/*pg0地址在内核编译的时候,已经是加上0xc0000000了,减去0xc00000000得到对应的物理地址*/
movl$(pg0-__PAGE_OFFSET),%edi
/*将目录表的地址传给edx,表明内核也要从0x00000000开始建立页表,这样可以保证从以物理地址取指令到以线性地址在系统空间取指令
的平稳过渡,下面会详细解释*/
movl$(swapper_pg_dir-__PAGE_OFFSET),%edx
movl$0x007,%eax
leal0x007(%edi),%ecx
Movl%ecx,(%edx)
movl%ecx,page_pde_offset(%edx)
addl$4,%edx
movl$1024,%ecx
11:
stosladdl$0x1000,%eax
loop11b
/*内核到底要建立多少页表,也就是要映射多少内存空间,取决于这个判断条件。在内核初始化程中内核只要保证能映射到包括内
核的代码段,数据段,初始页表和用于存放动态数据结构的128k大小的空间就行*/
leal(INIT_MAP_BEYOND_END+0x007)(%edi),%ebp
cmpl%ebp,%eax
jb10b
movl%edi,(init_pg_tables_end-__PAGE_OFFSET)
在上述代码中,内核为什么要把用户空间和内核空间的前几个目录项映射到相同的页表中去呢,虽然在head.S中内核已经进入保护模式,但是
内核现在是处于保护模式的段式寻址方式下,因为内核还没有启用分页映射机制,现在都是以物理地址来取指令,如果代码中遇到了符号地址
,只能减去0xc0000000才行,当开启了映射机制后就不用了现在cpu中的取指令指针eip仍指向低区,如果只建立内核空间中的映射,那么当
内核开启映射机制后,低区中的地址就没办法寻址了,应为没有对应的页表,除非遇到某个符号地址作为绝对转移或调用子程序为止。因此
要尽快开启CPU的页式映射机制.
movl$swapper_pg_dir-__PAGE_OFFSET,%eax
movl%eax,%cr3/*cr3控制寄存器保存的是目录表地址*/
movl%cr0,%eax/*向cr0的最高位置1来开启映射机制*/
orl$0x80000000,%eax
movl%eax,%cr0
ljmp$__BOOT_CS,$1f/*Clearprefetchandnormalize%eip*/
1:
lssstack_start,%esp
通过ljmp$__BOOT_CS,$1f这条指令使CPU进入了系统空间继续执行因为__BOOT_CS是个符号地址,地址在0xc0000000以上。
在head.S完成了内核临时页表的建立后,它继续进行初始化,包括初始化INIT_TASK,也就是系统开启后的第一个进程;建立完整的中断处理程
序,然后重新加载GDT描述符,最后跳转到init/main.c中的start_kernel函数继续初始化.
3.3内核页表的完整建立
内核在start_kernel()中继续做第二阶段的初始化,因为在这个阶段中,内核已经处于保护模式下,前面只是简单的设置了内核页表,内核
必须首先要建立一个完整的页表才能继续运行,因为内存寻址是内核继续运行的前提。
pagetable_init()的代码在mm/init.c中:
[start_kernel()>setup_arch()>paging_init()>pagetable_init()]
为了简单起见,我忽略了对PAE选项的支持。
staticvoid__initpagetable_init(void)
{
……
pgd_t*pgd_base=swapper_pg_dir;
……
kernel_physical_mapping_init(pgd_base);
……
}
在这个函数中pgd_base变量指向了swapper_pg_dir,这正是内核目录表的开始地址,pagetable_init()函数在通过
kernel_physical_mapping_init()函数完成内核页表的完整建立。
kernel_physical_mapping_init函数同样在mm/init.c中,我略去了与PAE模式相关的代码:
staticvoid__initkernel_physical_mapping_init(pgd_t*pgd_base)
{
unsignedlongpfn;
pgd_t*pgd;
pmd_t*pmd;
pte_t*pte;
intpgd_idx,pmd_idx,pte_ofs;
pgd_idx=pgd_index(PAGE_OFFSET);
pgd=pgd_base+pgd_idx;
pfn=0;
for(;pgd_idx<PTRS_PER_PGD;pgd++,pgd_idx++){
pmd=one_md_table_init(pgd);
if(pfn>=max_low_pfn)
continue;
for(pmd_idx=0;pmd_idx<PTRS_PER_PMD&&pfn<max_low_pfn;pmd++,pmd_idx++){
unsignedintaddress=pfn*PAGE_SIZE+PAGE_OFFSET;
……
pte=one_page_table_init(pmd);
for(pte_ofs=0;pte_ofs<PTRS_PER_PTE&&pfn<max_low_pfn;pte++,pfn++,pte_ofs++){
if(is_kernel_text(address))
set_pte(pte,pfn_pte(pfn,PAGE_KERNEL_EXEC));
else
set_pte(pte,pfn_pte(pfn,PAGE_KERNEL));
……
}
}
通过作者的注释,可以了解到这个函数的作用是把整个物理内存地址都映射到从内核空间的开始地址,即从0xc0000000的整个内核空间中,
直到物理内存映射完毕为止。这个函数比较长,而且用到很多关于内存管理方面的宏定义,理解了这个函数,就能大概理解内核是如何建立
页表的,将这个抽象的模型完全的理解。下面将详细分析这个函数:
函数开始定义了4个变量pgd_t*pgd,pmd_t*pmd,pte_t*pte,pfn;
pgd指向一个目录项开始的地址,pmd指向一个中间目录开始的地址,pte指向一个页表开始的地址pfn是页框号被初始为0.pgd_idx根据
pgd_index宏计算结果为768,也是内核要从目录表中第768个表项开始进行设置。从768到1024这个256个表项被linux内核设置成内核目录项,
低768个目录项被用户空间使用.pgd=pgd_base+pgd_idx;pgd便指向了第768个表项。
然后函数开始一个循环即开始填充从768到1024这256个目录项的内容。
one_md_table_init()函数根据pgd找到指向的pmd表。
它同样在mm/init.c中定义:
staticpmd_t*__initone_md_table_init(pgd_t*pgd)
{
pmd_t*pmd_table;
#ifdefCONFIG_X86_PAE
pmd_table=(pmd_t*)alloc_bootmem_low_pages(PAGE_SIZE);
set_pgd(pgd,__pgd(__pa(pmd_table)|_PAGE_PRESENT));
if(pmd_table!=pmd_offset(pgd,0))
BUG();
#else
pmd_table=pmd_offset(pgd,0);
#endif
returnpmd_table;
}
可以看出,如果内核不启用PAE选项,函数将通过pmd_offset返回pgd的地址。因为linux的二级映射模型,本来就是忽略pmd中间目录表的。
接着又个判断语句:
>>if(pfn>=max_low_pfn)
>>continue;
这个很关键,max_low_pfn代表着整个物理内存一共有多少页框。当pfn大于max_low_pfn的时候,表明内核已经把整个物理内存都映射到了系
统空间中,所以剩下有没被填充的表项就直接忽略了。因为内核已经可以映射整个物理空间了,没必要继续填充剩下的表项。
紧接着的第2个for循环,在linux的3级映射模型中,是要设置pmd表的,但在2级映射中忽略,只循环一次,直接进行页表pte的设置。
>>address=pfn*PAGE_SIZE+PAGE_OFFSET;
address是个线性地址,根据上面的语句可以看出address是从0xc000000开始的,也就是从内核空间开始,后面在设置页表项属性的时候会用
到它.
>>pte=one_page_table_init(pmd);
根据pmd分配一个页表,代码同样在mm/init.c中:
staticpte_t*__initone_page_table_init(pmd_t*pmd)
{
if(pmd_none(*pmd)){
pte_t*page_table=(pte_t*)alloc_bootmem_low_pages(PAGE_SIZE);
set_pmd(pmd,__pmd(__pa(page_table)|_PAGE_TABLE));
if(page_table!=pte_offset_kernel(pmd,0))
BUG();
returnpage_table;
}
returnpte_offset_kernel(pmd,0);
}
pmd_none宏判断pmd表是否为空,如果为空则要利用alloc_bootmem_low_pages分配一个4k大小的物理页面。然后通过set_pmd(pmd,__pmd
(__pa(page_table)|_PAGE_TABLE));来设置pmd表项。page_table显然属于线性地址,先通过__pa宏转化为物理地址,在与上_PAGE_TABLE宏,
此时它们还是无符号整数,在通过__pmd把无符号整数转化为pmd类型,经过这些转换,就得到了一个具有属性的表项,然后通过set_pmd宏设
置pmd表项.
接着又是一个循环,设置1024个页表项。
is_kernel_text函数根据前面提到的address来判断address线性地址是否属于内核代码段,它同样在mm/init.c中定义:
staticinlineintis_kernel_text(unsignedlongaddr)
{
if(addr>=(unsignedlong)_stext&&addr<=(unsignedlong)__init_end)
return1;
return0;
}
_stext,__init_end是个内核符号,在内核链接的时候生成的,分别表示内核代码段的开始和终止地址.
如果address属于内核代码段,那么在设置页表项的时候就要加个PAGE_KERNEL_EXEC属性,如果不是,则加个PAGE_KERNEL属性.
#define_PAGE_KERNEL_EXEC
(_PAGE_PRESENT|_PAGE_RW|_PAGE_DIRTY|_PAGE_ACCESSED)
#define_PAGE_KERNEL
(_PAGE_PRESENT|_PAGE_RW|_PAGE_DIRTY|_PAGE_ACCESSED|_PAGE_NX)
最后通过set_pte(pte,pfn_pte(pfn,PAGE_KERNEL));来设置页表项,先通过pfn_pte宏根据页框号和页表项的属性值合并成一个页表项值,
然户在用set_pte宏把页表项值写到页表项里。
当pagetable_init()函数返回后,内核已经设置好了内核页表,紧着调用load_cr3(swapper_pg_dir);
#defineload_cr3(pgdir)
asmvolatile(movl%0,%%cr3::r(__pa(pgdir)))
将控制swapper_pg_dir送入控制寄存器cr3.每当重新设置cr3时,CPU就会将页面映射目录所在的页面装入CPU内部高速缓存中的TLB部分.现
在内存中(实际上是高速缓存中)的映射目录变了,就要再让CPU装入一次。由于页面映射机制本来就是开启着的,所以从这条指令以后就扩大
了系统空间中有映射区域的大小,使整个映射覆盖到整个物理内存(高端内存)除外.实际上此时swapper_pg_dir中已经改变的目录项很可能还
在高速缓存中,所以还要通过__flush_tlb_all()将高速缓存中的内容冲刷到内存中,这样才能保证内存中映射目录内容的一致性。
3.4对如何构建页表的总结
通过上述对pagetable_init()的剖析,我们可以清晰的看到,构建内核页表,无非就是向相应的表项写入下一级地址和属性。在内核空间
保留着一部分内存专门用来存放内核页表.当cpu要进行寻址的时候,无论在内核空间,还是在用户空间,都会通过这个页表来进行映射。对于
这个函数,内核把整个物理内存空间都映射完了,当用户空间的进程要使用物理内存时,岂不是不能做相应的映射了?其实不会的,内核
只是做了映射,映射不代表使用,这样做是内核为了方便管理内存而已。
⑵ 单片机是什么意思。
单片机定义
[编辑本段]
单片机是指一个集成在一块芯片上的完整计算机系统。尽管他的大部分功能集成在一块小芯片上,但是它具有一个完整计算机所需要的大部分部件:CPU、内存、内部和外部总线系统,目前大部分还会具有外存。同时集成诸如通讯接口、定时器,实时时钟等外围设备。而现在最强大的单片机系统甚至可以将声音、图像、网络、复杂的输入输出系统集成在一块芯片上。
单片机也被称为微控制器(Microcontroler),是因为它最早被用在工业控制领域。单片机由芯片内仅有CPU的专用处理器发展而来。最早的设计理念是通过将大量外围设备和CPU集成在一个芯片中,使计算机系统更小,更容易集成进复杂的而对提及要求严格的控制设备当中。INTEL的Z80是最早按照这种思想设计出的处理器,从此以后,单片机和专用处理器的发展便分道扬镳。
早期的单片机都是8位或4位的。其中最成功的是INTEL的8031,因为简单可靠而性能不错获得了很大的好评。此后在8031上发展出了MCS51系列单片机系统。基于这一系统的单片机系统直到现在还在广泛使用。随着工业控制领域要求的提高,开始出现了16位单片机,但因为性价比不理想并未得到很广泛的应用。90年代后随着消费电子产品大发展,单片机技术得到了巨大的提高。随着INTEL i960系列特别是后来的ARM系列的广泛应用,32位单片机迅速取代16位单片机的高端地位,并且进入主流市场。而传统的8位单片机的性能也得到了飞速提高,处理能力比起80年代提高了数百倍。目前,高端的32位单片机主频已经超过300MHz,性能直追90年代中期的专用处理器,而普通的型号出厂价格跌落至1美元,最高端的型号也只有10美元。当代单片机系统已经不再只在裸机环境下开发和使用,大量专用的嵌入式操作系统被广泛应用在全系列的单片机上。而在作为掌上电脑和手机核心处理的高端单片机甚至可以直接使用专用的Windows和Linux操作系统。
单片机比专用处理器最适合应用于嵌入式系统,因此它得到了最多的应用。事实上单片机是世界上数量最多的计算机。现代人类生活中所用的几乎每件电子和机械产品中都会集成有单片机。手机、电话、计算器、家用电器、电子玩具、掌上电脑以及鼠标等电脑配件中都配有1-2部单片机。而个人电脑中也会有为数不少的单片机在工作。汽车上一般配备40多部单片机,复杂的工业控制系统上甚至可能有数百台单片机在同时工作!单片机的数量不仅远超过PC机和其他计算的综合,甚至比人类的数量还要多。
单片机介绍
[编辑本段]
单片机又称单片微控制器,它不是完成某一个逻辑功能的芯片,而是把一个计算机系统集成到一个芯片上。概括的讲:一块芯片就成了一台计算机。它的体积小、质量轻、价格便宜、为学习、应用和开发提供了便利条件。同时,学习使用单片机是了解计算机原理与结构的最佳选择。
单片机内部也用和电脑功能类似的模块,比如CPU,内存,并行总线,还有和硬盘作用相同的存储器件,不同的是它的这些部件性能都相对我们的家用电脑弱很多,不过价钱也是低的,一般不超过10元即可......用它来做一些控制电器一类不是很复杂的工作足矣了。我们现在用的全自动滚筒洗衣机、排烟罩、VCD等等的家电里面都可以看到它的身影!......它主要是作为控制部分的核心部件。
它是一种在线式实时控制计算机,在线式就是现场控制,需要的是有较强的抗干扰能力,较低的成本,这也是和离线式计算机的(比如家用PC)的主要区别。
单片机是靠程序的,并且可以修改。通过不同的程序实现不同的功能,尤其是特殊的独特的一些功能,这是别的器件需要费很大力气才能做到的,有些则是花大力气也很难做到的。一个不是很复杂的功能要是用美国50年代开发的74系列,或者60年代的CD4000系列这些纯硬件来搞定的话,电路一定是一块大PCB板!但是如果要是用美国70年代成功投放市场的系列单片机,结果就会有天壤之别!只因为单片机的通过你编写的程序可以实现高智能,高效率,以及高可靠性!
由于单片机对成本是敏感的,所以目前占统治地位的软件还是最低级汇编语言,它是除了二进制机器码以上最低级的语言了,既然这么低级为什么还要用呢?很多高级的语言已经达到了可视化编程的水平为什么不用呢?原因很简单,就是单片机没有家用计算机那样的CPU,也没有像硬盘那样的海量存储设备。一个可视化高级语言编写的小程序里面即使只有一个按钮,也会达到几十K的尺寸!对于家用PC的硬盘来讲没什么,可是对于单片机来讲是不能接受的。 单片机在硬件资源方面的利用率必须很高才行,所以汇编虽然原始却还是在大量使用。一样的道理,如果把巨型计算机上的操作系统和应用软件拿到家用PC上来运行,家用PC的也是承受不了的。
可以说,二十世纪跨越了三个“电”的时代,即电气时代、电子时代和现已进入的电脑时代。不过,这种电脑,通常是指个人计算机,简称PC机。它由主机、键盘、显示器等组成。还有一类计算机,大多数人却不怎么熟悉。这种计算机就是把智能赋予各种机械的单片机(亦称微控制器)。顾名思义,这种计算机的最小系统只用了一片集成电路,即可进行简单运算和控制。因为它体积小,通常都藏在被控机械的“肚子”里。它在整个装置中,起着有如人类头脑的作用,它出了毛病,整个装置就瘫痪了。现在,这种单片机的使用领域已十分广泛,如智能仪表、实时工控、通讯设备、导航系统、家用电器等。各种产品一旦用上了单片机,就能起到使产品升级换代的功效,常在产品名称前冠以形容词——“智能型”,如智能型洗衣机等。现在有些工厂的技术人员或其它业余电子开发者搞出来的某些产品,不是电路太复杂,就是功能太简单且极易被仿制。究其原因,可能就卡在产品未使用单片机或其它可编程逻辑器件上。
单片机历史
单片机诞生于20世纪70年代末,经历了SCM、MCU、SoC三大阶段。
1.SCM即单片微型计算机(Single Chip Microcomputer)阶段,主要是寻求最佳的单片形态嵌入式系统的最佳体系结构。“创新模式”获得成功,奠定了SCM与通用计算机完全不同的发展道路。在开创嵌入式系统独立发展道路上,Intel公司功不可没。
2.MCU即微控制器(Micro Controller Unit)阶段,主要的技术发展方向是:不断扩展满足嵌入式应用时,对象系统要求的各种外围电路与接口电路,突显其对象的智能化控制能力。它所涉及的领域都与对象系统相关,因此,发展MCU的重任不可避免地落在电气、电子技术厂家。从这一角度来看,Intel逐渐淡出MCU的发展也有其客观因素。在发展MCU方面,最著名的厂家当数Philips公司。
Philips公司以其在嵌入式应用方面的巨大优势,将MCS-51从单片微型计算机迅速发展到微控制器。因此,当我们回顾嵌入式系统发展道路时,不要忘记Intel和Philips的历史功绩。
3.单片机是嵌入式系统的独立发展之路,向MCU阶段发展的重要因素,就是寻求应用系统在芯片上的最大化解决;因此,专用单片机的发展自然形成了SoC化趋势。随着微电子技术、IC设计、EDA工具的发展,基于SoC的单片机应用系统设计会有较大的发展。因此,对单片机的理解可以从单片微型计算机、单片微控制器延伸到单片应用系统。
单片机的应用领域
[编辑本段]
目前单片机渗透到我们生活的各个领域,几乎很难找到哪个领域没有单片机的踪迹。导弹的导航装置,飞机上各种仪表的控制,计算机的网络通讯与数据传输,工业自动化过程的实时控制和数据处理,广泛使用的各种智能IC卡,民用豪华轿车的安全保障系统,录象机、摄象机、全自动洗衣机的控制,以及程控玩具、电子宠物等等,这些都离不开单片机。更不用说自动控制领域的机器人、智能仪表、医疗器械了。因此,单片机的学习、开发与应用将造就一批计算机应用与智能化控制的科学家、工程师。
单片机广泛应用于仪器仪表、家用电器、医用设备、航空航天、专用设备的智能化管理及过程控制等领域,大致可分如下几个范畴:
1.在智能仪器仪表上的应用
单片机具有体积小、功耗低、控制功能强、扩展灵活、微型化和使用方便等优点,广泛应用于仪器仪表中,结合不同类型的传感器,可实现诸如电压、功率、频率、湿度、温度、流量、速度、厚度、角度、长度、硬度、元素、压力等物理量的测量。采用单片机控制使得仪器仪表数字化、智能化、微型化,且功能比起采用电子或数字电路更加强大。例如精密的测量设备(功率计,示波器,各种分析仪)。
2.在工业控制中的应用
用单片机可以构成形式多样的控制系统、数据采集系统。例如工厂流水线的智能化管理,电梯智能化控制、各种报警系统,与计算机联网构成二级控制系统等。
3.在家用电器中的应用
可以这样说,现在的家用电器基本上都采用了单片机控制,从电饭褒、洗衣机、电冰箱、空调机、彩电、其他音响视频器材、再到电子秤量设备,五花八门,无所不在。
4.在计算机网络和通信领域中的应用
现代的单片机普遍具备通信接口,可以很方便地与计算机进行数据通信,为在计算机网络和通信设备间的应用提供了极好的物质条件,现在的通信设备基本上都实现了单片机智能控制,从手机,电话机、小型程控交换机、楼宇自动通信呼叫系统、列车无线通信、再到日常工作中随处可见的移动电话,集群移动通信,无线电对讲机等。
5.单片机在医用设备领域中的应用
单片机在医用设备中的用途亦相当广泛,例如医用呼吸机,各种分析仪,监护仪,超声诊断设备及病床呼叫系统等等。
此外,单片机在工商,金融,科研、教育,国防航空航天等领域都有着十分广泛的用途。
学习应中六大重要部分
[编辑本段]
单片机学习应中的六大重要部分
一、总线:我们知道,一个电路总是由元器件通过电线连接而成的,在模拟电路中,连线并不成为一个问题,因为各器件间一般是串行关系,各器件之间的连线并不很多,但计算机电路却不一样,它是以微处理器为核心,各器件都要与微处理器相连,各器件之间的工作必须相互协调,所以就需要的连线就很多了,如果仍如同模拟电路一样,在各微处理器和各器件间单独连线,则线的数量将多得惊人,所以在微处理机中引入了总线的概念,各个器件共同享用连线,所有器件的8根数据线全部接到8根公用的线上,即相当于各个器件并联起来,但仅这样还不行,如果有两个器件同时送出数据,一个为0,一个为1,那么,接收方接收到的究竟是什么呢?这种情况是不允许的,所以要通过控制线进行控制,使器件分时工作,任何时候只能有一个器件发送数据(可以有多个器件同时接收)。器件的数据线也就被称为数据总线,器件所有的控制线被称为控制总线。在单片机内部或者外部存储器及其它器件中有存储单元,这些存储单元要被分配地址,才能使用,分配地址当然也是以电信号的形式给出的,由于存储单元比较多,所以,用于地址分配的线也较多,这些线被称为地址总线。
二、数据、地址、指令:之所以将这三者放在一起,是因为这三者的本质都是一样的——数字,或者说都是一串‘0’和‘1’组成的序列。换言之,地址、指令也都是数据。指令:由单片机芯片的设计者规定的一种数字,它与我们常用的指令助记符有着严格的一一对应关系,不可以由单片机的开发者更改。地址:是寻找单片机内部、外部的存储单元、输入输出口的依据,内部单元的地址值已由芯片设计者规定好,不可更改,外部的单元可以由单片机开发者自行决定,但有一些地址单元是一定要有的(详见程序的执行过程)。数据:这是由微处理机处理的对象,在各种不同的应用电路中各不相同,一般而言,被处理的数据可能有这么几种情况:
1•地址(如MOV DPTR,#1000H),即地址1000H送入DPTR。
2•方式字或控制字(如MOV TMOD,#3),3即是控制字。
3•常数(如MOV TH0,#10H)10H即定时常数。
4•实际输出值(如P1口接彩灯,要灯全亮,则执行指令:MOV P1,#0FFH,要灯全暗,则执行指令:MOV P1,#00H)这里0FFH和00H都是实际输出值。又如用于LED的字形码,也是实际输出的值。
理解了地址、指令的本质,就不难理解程序运行过程中为什么会跑飞,会把数据当成指令来执行了。
三、P0口、P2口和P3的第二功能用法:初学时往往对P0口、P2口和P3口的第二功能用法迷惑不解,认为第二功能和原功能之间要有一个切换的过程,或者说要有一条指令,事实上,各端口的第二功能完全是自动的,不需要用指令来转换。如P3.6、P3.7分别是WR、RD信号,当微片理机外接RAM或有外部I/O口时,它们被用作第二功能,不能作为通用I/O口使用,只要一微处理机一执行到MOVX指令,就会有相应的信号从P3.6或P3.7送出,不需要事先用指令说明。事实上‘不能作为通用I/O口使用’也并不是‘不能’而是(使用者)‘不会’将其作为通用I/O口使用。你完全可以在指令中按排一条SETB P3.7的指令,并且当单片机执行到这条指令时,也会使P3.7变为高电平,但使用者不会这么去做,因为这通常这会导致系统的崩溃。
四、程序的执行过程: 单片机在通电复位后8051内的程序计数器(PC)中的值为‘0000’,所以程序总是从‘0000’单元开始执行,也就是说:在系统的ROM中一定要存在‘0000’这个单元,并且在‘0000’单元中存放的一定是一条指令。
五、堆栈: 堆栈是一个区域,是用来存放数据的,这个区域本身没有任何特殊之处,就是内部RAM的一部份,特殊的是它存放和取用数据的方式,即所谓的‘先进后出,后进先出’,并且堆栈有特殊的数据传输指令,即‘PUSH’和‘POP’,有一个特殊的专为其服务的单元,即堆栈指针SP,每当执一次PUSH指令时,SP就(在原来值的基础上)自动加1,每当执行一次POP指令,SP就(在原来值的基础上)自动减1。由于SP中的值可以用指令加以改变,所以只要在程序开始阶段更改了SP的值,就可以把堆栈设置在规定的内存单元中,如在程序开始时,用一条MOV SP,#5FH指令,就时把堆栈设置在从内存单元60H开始的单元中。一般程序的开头总有这么一条设置堆栈指针的指令,因为开机时,SP的初始值为07H,这样就使堆栈从08H单元开始往后,而08H到1FH这个区域正是8031的第二、三、四工作寄存器区,经常要被使用,这会造成数据的浑乱。不同作者编写程序时,初始化堆栈指令也不完全相同,这是作者的习惯问题。当设置好堆栈区后,并不意味着该区域成为一种专用内存,它还是可以象普通内存区域一样使用,只是一般情况下编程者不会把它当成普通内存用了。
六、单片机的开发过程: 这里所说的开发过程并不是一般书中所说的从任务分析开始,我们假设已设计并制作好硬件,下面就是编写软件的工作。在编写软件之前,首先要确定一些常数、地址,事实上这些常数、地址在设计阶段已被直接或间接地确定下来了。如当某器件的连线设计好后,其地址也就被确定了,当器件的功能被确定下来后,其控制字也就被确定了。然后用文本编辑器(如EDIT、CCED等)编写软件,编写好后,用编译器对源程序文件编译,查错,直到没有语法错误,除了极简单的程序外,一般应用仿真机对软件进行调试,直到程序运行正确为止。运行正确后,就可以写片(将程序固化在EPROM中)。在源程序被编译后,生成了扩展名为HEX的目标文件,一般编程器能够识别这种格式的文件,只要将此文件调入即可写片。在此,为使大家对整个过程有个认识,举一例说明:
ORG 0000H
LJMP START
ORG 040H
START:
MOV SP,#5FH ;设堆栈
LOOP:
NOP
LJMP LOOP ;循环
END ;结束
单片机学习
[编辑本段]
目前,很多人对汇编语言并不认可。可以说,掌握用C语言单片机编程很重要,可以大大提高开发的效率。不过初学者可以不了解单片机的汇编语言,但一定要了解单片机具体性能和特点,不然在单片机领域是比较致命的。如果不考虑单片机硬件资源,在KEIL中用C胡乱编程,结果只能是出了问题无法解决!可以肯定的说,最好的C语言单片机工程师都是从汇编走出来的编程者因为单片机的C语言虽然是高级语言,但是它不同于台式机个人电脑上的VC++什么的单片机的硬件资源不是非常强大,不同于我们用VC、VB等高级语言在台式PC上写程序毕竟台式电脑的硬件非常强大,所以才可以不考虑硬件资源的问题。
以8051单片机为例讲解单片机的引脚及相关功能;
《单片机引脚图》
40个引脚按引脚功能大致可分为4个种类:电源、时钟、控制和I/O引脚。
⒈ 电源:
⑴ VCC - 芯片电源,接+5V;
⑵ VSS - 接地端;
注:用万用表测试单片机引脚电流一般为0v或者5v,这是标准的TTL电平,但有时候在单片机程序正在工作时候测试结果并不是这个值而是介于0v-5v之间,其实这之是万用表反映没这么快而已,在某一个瞬间单片机引脚电流还是保持在0v或者5v的。
⒉ 时钟:XTAL1、XTAL2 - 晶体振荡电路反相输入端和输出端。
⒊ 控制线:控制线共有4根,
⑴ ALE/PROG:地址锁存允许/片内EPROM编程脉冲
① ALE功能:用来锁存P0口送出的低8位地址
② PROG功能:片内有EPROM的芯片,在EPROM编程期间,此引脚输入编程脉冲。
⑵ PSEN:外ROM读选通信号。
⑶ RST/VPD:复位/备用电源。
① RST(Reset)功能:复位信号输入端。
② VPD功能:在Vcc掉电情况下,接备用电源。
⑷ EA/Vpp:内外ROM选择/片内EPROM编程电源。
① EA功能:内外ROM选择端。
② Vpp功能:片内有EPROM的芯片,在EPROM编程期间,施加编程电源Vpp。
⒋ I/O线
80C51共有4个8位并行I/O端口:P0、P1、P2、P3口,共32个引脚。
P3口还具有第二功能,用于特殊信号输入输出和控制信号(属控制总线)
⑶ 什么是虚拟地址保护模式
保护模式
(Protected Mode,或有时简写为 pmode) 是一种 80286 系列和之后的 x86 兼容 CPU 操作模式。保护模式有一些新的特色,设计用来增强 多工 和系统稳定度,像是 内存保护,分页 系统,以及硬件支援的 虚拟内存。大部分的现今 x86 操作系统 都在保护模式下运行,包含 Linux、FreeBSD、以及 微软 Windows 2.0 和之后版本。
另外一种 286 和其之后 CPU 的操作模式是 真实模式,一种向前兼容且关闭这些特色的模式。设计用来让新的芯片可以执行旧的软件。依照设计的规格,所有的 x86 CPU 都是在真实模式下开机来确保传统操作系统的向前兼容性。在任何保护模式的特色可用前,他们必须要由某些程序手动地切换到保护模式。在现今的电脑,这种切换通常是由 操作系统 在开机时候必须完成的第一件工作的一个。它也可能当 CPU 在保护模式下运行时,使用 虚拟86模式 来执行设计给真实模式的程序码。
尽管用软件的方式也有某些可能在真实模式的系统下使用多工,但保护模式下内存保护的特色,可以避免有问题的程序破坏其他工作或是 操作系统 核心所拥有的内存。保护模式也有中断正在执行程序的硬件支援,可以把 execution content 交给其他工作,得以实现 先占式多工。
大部分可以使用保护模式的 CPU 也拥有 32 位元暂存器 的特色 (例如 80386 系列和其后任何的芯片),导入了融合保护模式而成为 32 位元处理的概念。80286 芯片虽有支援保护模式,但是仍然只有 16 位元暂存器。Windows 2.0 和之后版本中的保护模式增强称为 "386 增强模式",是因为他们除了保护模式外,还需要 32 位元的暂存器,并且无法在 286 上面执行 (即使 286 支援保护模式)。
即使在 32 位元芯片上已经打开了保护模式,但是 1 MB 以上的内存并无法存取,是由于一种仿照 IBM XT 系统设计特性的 memory wrap-around(内存连续) 的因素。这种限制可以由打开 A20 line 来回避。
在保护模式下,前面 32 个中断都是保留给 CPU 例外处理用。举个例子,中断 0D (十进制 13) 是 一般保护模式错物 和 中断 00 是 除以零。
在8086/8088时代,处理器只存在一种操作模式(Operation Mode),当时由于不存在其它操作模式,因此这种模式也没有被命名。自从80286到80386开始,处理器增加了另外两种操作模式——保护模式PM (Protected Mode)和系统管理模式SMM(System Management Mode),因此,8086/8088的模式被命名为实地址模式RM(Real-address Mode)。
PM是处理器的native模式,在这种模式下,处理器支持所有的指令和所有的体系结构特性,提供最高的性能和兼容性。对于所有的新型应用程序和操作系统来说,建议都使用这种模式。为了保证PM的兼容性,处理器允许在受保护的,多任务的环境下执行RM程序。这个特性被称做虚拟8086模式(Virtual -8086 Mode),尽管它并不是一个真正的处理器模式。Virtual-8086模式实际上是一个PM的属性,任何任务都可以使用它。
RM提供了Intel 8086处理器的编程环境,另外有一些扩展(比如切换到PM或SMM的能力)。当主机被Power-up或Reset后,处理器处于RM下。
SMM是一个对所有Intel处理器都统一的标准体系结构特性。出现于Intel386 SL芯片。这个模式为OS实现平台指定的功能(比如电源管理或系统安全)提供了一种透明的机制。当外部的SMM interrupt pin(SMI#)被激活或者从APIC(Advanced Programming Interrupt Controller)收到一个SMI,处理器将进入SMM。在SMM下,当保存当前正在运行程序的整个上下文(Context)时,处理器切换到一个分离的地址空间。然后SMM指定的代码或许被透明的执行。当从SMM返回时,处理器将回到被系统管理中断之前的状态。
由于机器在Power-up或Reset之后,处理器处于RM状态,而对于Intel 80386以及其后的芯片,只有使用PM才能发挥出最大的作用。所以我们就面临着一个从RM切换到PM的问题。
本文不讨论SMM,本节的重点集中于在Booting阶段如何从RM切换到PM,这里不会过多的讨论PM的细节,因为《Intel Architecture Software Developer’s Manual Volume 3: System Programming》中有非常详尽和准确的介绍。
1. What is GDT
在Protected Mode下,一个重要的必不可少的数据结构就是GDT(Global Descriptor Table)。
为什么要有GDT?我们首先考虑一下在Real Mode下的编程模型:
在Real Mode下,我们对一个内存地址的访问是通过Segment:Offset的方式来进行的,其中Segment是一个段的Base Address,一个Segment的最大长度是64 KB,这是16-bit系统所能表示的最大长度。而Offset则是相对于此Segment Base Address的偏移量。Base Address+Offset就是一个内存绝对地址。由此,我们可以看出,一个段具备两个因素:Base Address和Limit(段的最大长度),而对一个内存地址的访问,则是需要指出:使用哪个段?以及相对于这个段Base Address的Offset,这个Offset应该小于此段的Limit。当然对于16-bit系统,Limit不要指定,默认为最大长度64KB,而 16-bit的Offset也永远不可能大于此Limit。我们在实际编程的时候,使用16-bit段寄存器CS(Code Segment),DS(Data Segment),SS(Stack Segment)来指定Segment,CPU将段积存器中的数值向左偏移4-bit,放到20-bit的地址线上就成为20-bit的Base Address。
到了Protected Mode,内存的管理模式分为两种,段模式和页模式,其中页模式也是基于段模式的。也就是说,Protected Mode的内存管理模式事实上是:纯段模式和段页式。进一步说,段模式是必不可少的,而页模式则是可选的——如果使用页模式,则是段页式;否则这是纯段模式。
既然是这样,我们就先不去考虑页模式。对于段模式来讲,访问一个内存地址仍然使用Segment:Offset的方式,这是很自然的。由于 Protected Mode运行在32-bit系统上,那么Segment的两个因素:Base Address和Limit也都是32位的。IA-32允许将一个段的Base Address设为32-bit所能表示的任何值(Limit则可以被设为32-bit所能表示的,以2^12为倍数的任何指),而不象Real Mode下,一个段的Base Address只能是16的倍数(因为其低4-bit是通过左移运算得来的,只能为0,从而达到使用16-bit段寄存器表示20-bit Base Address的目的),而一个段的Limit只能为固定值64 KB。另外,Protected Mode,顾名思义,又为段模式提供了保护机制,也就说一个段的描述符需要规定对自身的访问权限(Access)。所以,在Protected Mode下,对一个段的描述则包括3方面因素:[Base Address, Limit, Access],它们加在一起被放在一个64-bit长的数据结构中,被称为段描述符。这种情况下,如果我们直接通过一个64-bit段描述符来引用一个段的时候,就必须使用一个64-bit长的段积存器装入这个段描述符。但Intel为了保持向后兼容,将段积存器仍然规定为16-bit(尽管每个段积存器事实上有一个64-bit长的不可见部分,但对于程序员来说,段积存器就是16-bit的),那么很明显,我们无法通过16-bit长度的段积存器来直接引用64-bit的段描述符。
怎么办?解决的方法就是把这些长度为64-bit的段描述符放入一个数组中,而将段寄存器中的值作为下标索引来间接引用(事实上,是将段寄存器中的高13 -bit的内容作为索引)。这个全局的数组就是GDT。事实上,在GDT中存放的不仅仅是段描述符,还有其它描述符,它们都是64-bit长,我们随后再讨论。
GDT可以被放在内存的任何位置,那么当程序员通过段寄存器来引用一个段描述符时,CPU必须知道GDT的入口,也就是基地址放在哪里,所以Intel的设计者门提供了一个寄存器GDTR用来存放GDT的入口地址,程序员将GDT设定在内存中某个位置之后,可以通过LGDT指令将GDT的入口地址装入此积存器,从此以后,CPU就根据此积存器中的内容作为GDT的入口来访问GDT了。
GDT是Protected Mode所必须的数据结构,也是唯一的——不应该,也不可能有多个。另外,正象它的名字(Global Descriptor Table)所揭示的,它是全局可见的,对任何一个任务而言都是这样。
除了GDT之外,IA-32还允许程序员构建与GDT类似的数据结构,它们被称作LDT(Local Descriptor Table),但与GDT不同的是,LDT在系统中可以存在多个,并且从LDT的名字可以得知,LDT不是全局可见的,它们只对引用它们的任务可见,每个任务最多可以拥有一个LDT。另外,每一个LDT自身作为一个段存在,它们的段描述符被放在GDT中。
IA-32为LDT的入口地址也提供了一个寄存器LDTR,因为在任何时刻只能有一个任务在运行,所以LDT寄存器全局也只需要有一个。如果一个任务拥有自身的LDT,那么当它需要引用自身的LDT时,它需要通过LLDT将其LDT的段描述符装入此寄存器。LLDT指令与LGDT指令不同的时,LGDT指令的操作数是一个32-bit的内存地址,这个内存地址处存放的是一个32-bit GDT的入口地址,以及16-bit的GDT Limit。而LLDT指令的操作数是一个16-bit的选择子,这个选择子主要内容是:被装入的LDT的段描述符在GDT中的索引值——这一点和刚才所讨论的通过段积存器引用段的模式是一样的。
LDT只是一个可选的数据结构,你完全可以不用它。使用它或许可以带来一些方便性,但同时也带来复杂性,如果你想让你的OS内核保持简洁性,以及可移植性,则最好不要使用它。
引用GDT和LDT中的段描述符所描述的段,是通过一个16-bit的数据结构来实现的,这个数据结构叫做Segment Selector——段选择子。它的高13位作为被引用的段描述符在GDT/LDT中的下标索引,bit 2用来指定被引用段描述符被放在GDT中还是到LDT中,bit 0和bit 1是RPL——请求特权等级,被用来做保护目的,我们这里不详细讨论它。
前面所讨论的装入段寄存器中作为GDT/LDT索引的就是Segment Selector,当需要引用一个内存地址时,使用的仍然是Segment:Offset模式,具体操作是:在相应的段寄存器装入Segment Selector,按照这个Segment Selector可以到GDT或LDT中找到相应的Segment Descriptor,这个Segment Descriptor中记录了此段的Base Address,然后加上Offset,就得到了最后的内存地址。如下图所示:
2. Setup GDT
由上一节的讨论得知,GDT是Protected Mode所必须的数据结构,那么我们在进入Protected Mode之前,必须设定好GDT,并通过LGDT将其装入相应的寄存器。
尽管GDT允许被放在内存的任何位置,但由于GDT中的元素——描述符——都是64-bit长,也就是说都是8个字节,所以为了让CPU对GDT的访问速度达到最快,我们应该将GDT的入口地址放在以8个字节对齐,也就是说是8的倍数的地址位置。
GDT中第一个描述符必须是一个空描述符,也就是它的内容应该全部为0。如果引用这个描述符进行内存访问,则是产生General Protection异常。
如果一个OS不使用虚拟内存,段模式会是一个不错的选择。但现代OS没有不使用虚拟内存的,而实现虚拟内存的比较方便和有效的内存管理方式是页式管理。但是在IA-32上如果我们想使用页式管理,我们只能使用段页式——没有方法可以完全禁止段模式。但我们可以尽力让段的效果降低的最小。
IA-32提供了一种被称作“Basic Flat Model”的分段模式可以达到这种效果。这种模式要求在GDT中至少要定义两个段描述符,一个用来引用Data Segment,另一个用来引用Code Segment。这2个Segment都包含整个线性空间,即Segment Limit = 4 GB,即使实际的物理内存远没有那么多,但这个空间定义是为了将来由页式管理来实现虚拟内存。
在这里,我们只是处于Booting阶段,所以我们只需要初步设置一下GDT,等真正进入Protected Mode,启动了OS Kernel之后,具体OS打算如何设置GDT,使用何种内存管理模式,由Kernel自身来设置,Booting只需要给Kernel的数据段和代码段设置全部线性空间就可以了。
段描述符的格式如下图所示:
具体到代码段和数据段,它们的格式如下图所示:
下面就是在Booting阶段为进入Protected Mode而设置的临时的gdt。这里定义了3个段描述符:第一个是系统规定的空描述符,第2个是引用4 GB线性空间的代码段,第3个是引用4 GB线性空间的数据段。这是"Basic Flat Model"所要求的最下GDT设置,但就booting阶段,只是为了进入Protected Mode,并为内核提供一个连续的,最大的线性空间这个目的而言,已经足够了。
# Descriptor tables
gdt:
.word 0, 0, 0, 0 # mmy
.word 0xFFFF # 4Gb - (0x100000*0x1000 = 4Gb)
.word 0 # base address = 0
.word 0x9A00 # code read/exec
.word 0x00CF # granularity = 4096, 386
# (+5th nibble of limit)
.word 0xFFFF # 4Gb - (0x100000*0x1000 = 4Gb)
.word 0 # base address = 0
.word 0x9200 # data read/write
.word 0x00CF # granularity = 4096, 386
# (+5th nibble of limit)
3. Load GDT
设置好GDT之后,我们需要通过LGDT指令将设定的gdt的入口地址和gdt表的大小装入GDTR寄存器。
GDTR寄存器包括两部分:32-bit的线性基地址,以及16-bit的GDT大小(以字节为单位)。需要注意的是,对于32-bit线性基地址,必须是32-bit绝对物理地址,而不是相对于某个段的偏移量。而我们在Booting阶段,在进入Protected Mode之前,我们CS和DS设置很可能不是0,所以我们必须计算出gdt的绝对物理地址。
为了执行LGDT指令,你需要把这两部分内容放在内存的某个位置,然后将这个位置的内存地址作为操作数传递给LGDT指令。然后LGDT指令会自动将保存在这个位置的这两部分值装入GDTR寄存器。
# 这是存放GDTR所需的两部分内容的位置
gdt_48:
.word 0x8000 # gdt limit=2048,
# 256 GDT entries
.word 0, 0 # gdt base (filled in later)
# 下面这段代码用来计算GDT的32-bit线性地址,并将其装入GDTR寄存器。
xorl %eax, %eax # Compute gdt_base
movw %ds, %ax # (Convert %ds:gdt to a linear ptr)
shll , %eax
addl $gdt, %eax
movl %eax, (gdt_48+2)
lgdt gdt_48 # load gdt with whatever is appropriate
4. Other Preparing Stuff
在进入Protected Mode之前,除了需要设置和装入GDT之外,还需要做如下一些事情:
屏蔽所有可屏蔽中断;
装入IDTR;
所有协处理器被正确的Reset。
由于在Real Mode和Protected Mode下的中断处理机制有一些不同,所以在进入Protected Mode之前,务必禁止所有可屏蔽中断,这可以通过下面两种方法之一:
使用CLI指令;
对8259A可编程中断控制器编程以屏蔽所有中断。
即使当我们进入Protected Mode之后,也不能马上将中断打开,这时因为我们必须在OS Kernel中对相关的Protected Mode中断处理所需的数据结构正确的初始化之后,才能打开中断,否则会产生处理器异常。
在Real Mode下,中断处理使用IVT(Interrupt Vector Table),在Protected Mode下,中断处理使用IDT(Interrupt Descriptor Table),所以,我们必须在进入Protected Mode之前设置IDTR。
IDTR的格式和GDTR相同,IDTR的装入方式和GDTR也相同。由于IDT中相关的中断处理程序需要让OS Kernel来设定,所以在Booting阶段,我们只需要将IDTR中IDT的基地址和Size都设为0就可以了,随后,等进入Protected Mode之后,由OS Kernel来真正设置它。
关于中断机制和中断处理,请参考 Interrupt & Exception ,这里不再赘述。
#
# 这是存放IDTR所需的两部分内容的位置
#
idt_48:
.word 0 # idt limit = 0
.word 0, 0 # idt base = 0L
# 对于IDTR的处理,只需要这一条指令即可
lidt idt_48 # load idt with 0,0
#
# 通过设置8259A PIC,屏蔽所有可屏蔽中断
#
movb xFF, %al # mask all interrupts for now
outb %al, xA1
call delay
movb xFB, %al # mask all irq's but irq2 which
outb %al, x21 # is cascaded
# 保证所有的协处理都被正确的Reset
xorw %ax, %ax
outb %al, xf0
call delay
outb %al, xf1
call delay
# Delay is needed after doing I/O
delay:
outb %al,x80
ret
5. Let's Go
好了,一切准备就绪,Fire!:)
进入Protected Mode,还是进入Real Mode,完全靠CR0寄存器的PE标志位来控制:如果PE=1,则CPU切换到PM,否则,则进入RM。
设置CR0-PE位的方法有两种:
第一种是80286所使用的LMSW指令,后来的80386及更高型号的CPU为了保持向后兼容,都保留了这个指令。这个指令只能影响最低的4 bit,即PE,MP,EM和TS,对其它的没有影响。
#
#通过LMSW指令进入Protected Mode
#
movw , %ax # protected mode (PE) bit
lmsw %ax # This is it!
第二种是Intel所建议的在80386以后的CPU上使用的进入PM的方式,即通过MOV指令。MOV指令可以设置CR0寄存器的所有域的值。
#
#通过MOV指令进入Protected Mode
#
movl %cr0, %eax
xorb , %al # set PE = 1
movl %eax, %cr0 # go!!
OK,现在已经进入Protected Mode了。
很简单,right?But It's not over yet!
6. Start Kernel
我们已经从Real Mode进入Protected Mode,现在我们马上就要启动OS Kernel了。
OS Kernel运行在32-bit段模式,而当前我们却仍然处于16-bit段模式。这是怎么回事?为了了解这个问题,我们需要仔细探讨一下IA-32的段模式的实现方法。
IA-32共提供了6个16-bit段寄存器:CS,DS,SS,ES,FS,GS。但事实上,这16-bit只是对程序员可见的部分,但每个寄存器仍然包括64-bit的不可见部分。
可见部分是为了供程序员装载段寄存器,但一旦装载完成,CPU真正使用的就只是不可见部分,可见部分就完全没有用了。
不可见部分存放的内容是什么?具体格式我没有看到相关资料,但可以确定的是隐藏部分的内容和段描述符的内容是一致的(请参考段描述的格式),只不过格式可能不完全相同。但格式对我们理解这一点并不重要,因为程序员不可能能够直接操作它。
我们以CS寄存器为例,对于其它寄存器也是一样的:
在Real Mode下,当我们执行一个装载CS寄存器的指令的时候(jmp,call,ret等),相关的值会被装入CS寄存器的可见部分,但同时CPU也会根据可见部分的内容来设置不可见部分。比如我们执行"ljmp x1234, $go "之后,CS寄存器的可见部分的内容就是1234h,同时,不可见部分的32-bit Base Address域被设置为00001234h,20-bit的Limit域被设置为固定值10000h,也就是64 KB,Access Information部分的其它值我们不去考虑,只考虑其D/B位,由于执行此指令时处于Real Mode模式,所以D/B被设置为0,表示此段是一个16-bit段。当对CS寄存器的可见部分和不可见部分的内容都被设置之后,CS寄存器的装载工作完成。随后当CPU需要通过CS的内容进行地址运算的时候,则仅仅引用不可见部分。
在Protected Mode下,当我们执行一个装载CS寄存器的指令的时候,段选择子(Segment Selector)被装入CS寄存器的可见部分,同时CPU根据此选择子到相应的描述符表中(GDT或LDT)找到相应的段描述符并将其内容装载入CS寄存器的不可见部分。随后CPU当需要通过CS的内容进行地址运算的时候,也仅仅引用不可见部分。
从上面的描述可以看出,事实上CPU在引用段寄存器的内容进行地址运算时,Real Mode和Protected Mode是一致的。另外,也明白了为什么我们在Real Mode下设置的段寄存器的内容到了Protected Mode下仍然引用的是16-bit段。
那么我们如何将CS设置为引用32-bit段?方法就像我们前面所讨论的,使用jmp或call指令,引用一个段选择子,到GDT中装载一个引用32-bit段的段描述符。
需要注意的是,如果CS寄存器的内容指出当前是一个16-bit段,那么当前的地址模式也就是16-bit地址模式,这与你当前是出于Real Mode还是Protected Mode无关。而我们装载32-bit段的jmp指令或call指令必须使用的是32-bit地址模式。而我们当前的boot部分代码是16-bit代码,所以我们必须在此jmp/call指令前加上地址转换前缀代码66h。
下面的例子就是使用jmp指令装入32-bit段。Jmpi指令的含义是段间跳转,其Opcode为Eah,其格式为:jmpi Offset, Segment Selector。
# 由于当前的代码是16-bit代码,而我们要执行32-bit地址模式的指令,指令前
# 需要有地址模式切换前缀66h,如果我们直接写jmp指令,由编译器来生成代码
# 的话,是无法作到这一点的,所以我们直接写相关数据。
.byte 0x66, 0xea # prefix + jmpi-opcode
.long 0x1000 # Offset
.word __KERNEL_CS # CS segment selector
上面的代码相当于32-bit指令:
jmpi 0x1000,__KERNEL_CS
如果__KERNEL_CS段选择子所引用的段描述符设置的段空间为线形地址[0,4 GB],而我们将OS Kernel放在物理地址1000h,那么此jmpi指令就跳转到OS Kernel的入口处,并开始执行它。
此时,Booting阶段结束,OS正式开始运行!
⑷ LJMP $ 什么意思
LJMP是单片机的跳转指令
ljmp是linux下AT$T格式的汇编指令
⑸ 怎样能将汇编语言转换成c语言
1、打开IAR FOR STM8工程。
⑹ cjne指令后面直接跟JC指令,请问这样怎么翻译到C语言,程序如下
这篇文章将首先介绍一些所需的基本知识,如操作系统对进程的内存管理以及相关的系统调用,然后逐步实现一个简单的malloc。为了简单起见,这篇文章将只考虑x86_64体系结构,操作系统为Linux。
1 什么是malloc
2 预备知识
2.2.1 内存排布
2.2.2 Heap内存模型
2.2.3 brk与sbrk
2.2.4 资源限制与rlimit
2.1.1 虚拟内存地址与物理内存地址
2.1.2 页与地址构成
2.1.3 内存页与磁盘页
2.1 Linux内存管理
2.2 Linux进程级内存管理
3 实现malloc
3.2.1 数据结构
3.2.2 寻找合适的block
3.2.3 开辟新的block
3.2.4 分裂block
3.2.5 malloc的实现
3.2.6 calloc的实现
3.2.7 free的实现
3.2.8 realloc的实现
3.1 玩具实现
3.2 正式实现
3.3 遗留问题和优化
4 其它参考
1 什么是malloc
在实现malloc之前,先要相对正式地对malloc做一个定义。
根据标准C库函数的定义,malloc具有如下原型:
void* malloc(size_t size);
这个函数要实现的功能是在系统中分配一段连续的可用的内存,具体有如下要求:
malloc分配的内存大小至少为size参数所指定的字节数
malloc的返回值是一个指针,指向一段可用内存的起始地址
多次调用malloc所分配的地址不能有重叠部分,除非某次malloc所分配的地址被释放掉
malloc应该尽快完成内存分配并返回(不能使用NP-hard的内存分配算法)
实现malloc时应同时实现内存大小调整和内存释放函数(即realloc和free)
对于malloc更多的说明可以在命令行中键入以下命令查看:
man malloc
2 预备知识
在实现malloc之前,需要先解释一些Linux系统内存相关的知识。
2.1 Linux内存管理
2.1.1 虚拟内存地址与物理内存地址
为了简单,现代操作系统在处理内存地址时,普遍采用虚拟内存地址技术。即在汇编程序(或机器语言)层面,当涉及内存地址时,都是使用虚拟内存地址。采用这种技术时,每个进程仿佛自己独享一片2N
字节的内存,其中N是机器位数。例如在64位CPU和64位操作系统下,每个进程的虚拟地址空间为264
Byte。
这种虚拟地址空间的作用主要是简化程序的编写及方便操作系统对进程间内存的隔离管理,真实中的进程不太可能(也用不到)如此大的内存空间,实际能用到的内存取决于物理内存大小。
由于在机器语言层面都是采用虚拟地址,当实际的机器码程序涉及到内存操作时,需要根据当前进程运行的实际上下文将虚拟地址转换为物理内存地址,才能实现对真实内存数据的操作。这个转换一般由一个叫MMU(Memory Management Unit)的硬件完成。
2.1.2 页与地址构成
在现代操作系统中,不论是虚拟内存还是物理内存,都不是以字节为单位进行管理的,而是以页(Page)为单位。一个内存页是一段固定大小的连续内存地址的总称,具体到Linux中,典型的内存页大小为4096Byte(4K)。
所以内存地址可以分为页号和页内偏移量。下面以64位机器,4G物理内存,4K页大小为例,虚拟内存地址和物理内存地址的组成如下:
实现代码:
void split_block(t_block b,size_t s){
t_block new;
new= b->data + s;
new->size = b->size - s - BLOCK_SIZE ;
new->next = b->next;
new->free =1;
b->size = s;
b->next =new;
}
3.2.5 malloc的实现
有了上面的代码,我们可以利用它们整合成一个简单但初步可用的malloc。注意首先我们要定义个block链表的头first_block,初始化为NULL;另外,我们需要剩余空间至少有BLOCK_SIZE + 8才执行分裂操作。
由于我们希望malloc分配的数据区是按8字节对齐,所以在size不为8的倍数时,我们需要将size调整为大于size的最小的8的倍数:
size_t align8(size_t s){
if(s &0x7==0)
return s;
return((s >>3)+1)<<3;
}
#define BLOCK_SIZE 24
void*first_block=NULL;
/* other functions... */
void*malloc(size_t size){
t_block b, last;
size_t s;
/* 对齐地址 */
s = align8(size);
if(first_block){
/* 查找合适的block */
last = first_block;
b = find_block(&last, s);
if(b){
/* 如果可以,则分裂 */
if((b->size - s)>=( BLOCK_SIZE +8))
split_block(b, s);
b->free =0;
}else{
/* 没有合适的block,开辟一个新的 */
b = extend_heap(last, s);
if(!b)
return NULL;
}
}else{
b = extend_heap(NULL, s);
if(!b)
return NULL;
first_block = b;
}
return b->data;
}
3.2.6 calloc的实现
有了malloc,实现calloc只要两步:
malloc一段内存
将数据区内容置为0
由于我们的数据区是按8字节对齐的,所以为了提高效率,我们可以每8字节一组置0,而不是一个一个字节设置。我们可以通过新建一个size_t指针,将内存区域强制看做size_t类型来实现。
void*calloc(size_t number,size_t size){
size_t*new;
size_t s8, i;
new= malloc(number * size);
if(new){
s8 = align8(number * size)>>3;
for(i =0; i < s8; i++)
new[i]=0;
}
returnnew;
}
3.2.7 free的实现
free的实现并不像看上去那么简单,这里我们要解决两个关键问题:
如何验证所传入的地址是有效地址,即确实是通过malloc方式分配的数据区首地址
如何解决碎片问题
首先我们要保证传入free的地址是有效的,这个有效包括两方面:
地址应该在之前malloc所分配的区域内,即在first_block和当前break指针范围内
这个地址确实是之前通过我们自己的malloc分配的
第一个问题比较好解决,只要进行地址比较就可以了,关键是第二个问题。这里有两种解决方案:一是在结构体内埋一个magic number字段,free之前通过相对偏移检查特定位置的值是否为我们设置的magic number,另一种方法是在结构体内增加一个magic pointer,这个指针指向数据区的第一个字节(也就是在合法时free时传入的地址),我们在free前检查magic pointer是否指向参数所指地址。这里我们采用第二种方案:
首先我们在结构体中增加magic pointer(同时要修改BLOCK_SIZE):
typedefstruct s_block *t_block;
struct s_block {
size_t size;/* 数据区大小 */
t_block next;/* 指向下个块的指针 */
int free; /* 是否是空闲块 */
int padding;/* 填充4字节,保证meta块长度为8的倍数 */
void*ptr; /* Magic pointer,指向data */
char data[1]/* 这是一个虚拟字段,表示数据块的第一个字节,长度不应计入meta */
};
然后我们定义检查地址合法性的函数:
t_block get_block(void*p){
char*tmp;
tmp = p;
return(p = tmp -= BLOCK_SIZE);
}
int valid_addr(void*p){
if(first_block){
if(p > first_block && p < sbrk(0)){
return p ==(get_block(p))->ptr;
}
}
return0;
}
当多次malloc和free后,整个内存池可能会产生很多碎片block,这些block很小,经常无法使用,甚至出现许多碎片连在一起,虽然总体能满足某此malloc要求,但是由于分割成了多个小block而无法fit,这就是碎片问题。
一个简单的解决方式时当free某个block时,如果发现它相邻的block也是free的,则将block和相邻block合并。为了满足这个实现,需要将s_block改为双向链表。修改后的block结构如下:
typedefstruct s_block *t_block;
struct s_block {
size_t size;/* 数据区大小 */
t_block prev;/* 指向上个块的指针 */
t_block next;/* 指向下个块的指针 */
int free; /* 是否是空闲块 */
int padding;/* 填充4字节,保证meta块长度为8的倍数 */
void*ptr; /* Magic pointer,指向data */
char data[1]/* 这是一个虚拟字段,表示数据块的第一个字节,长度不应计入meta */
};
合并方法如下:
t_block fusion(t_block b){
if(b->next && b->next->free){
b->size += BLOCK_SIZE + b->next->size;
b->next = b->next->next;
if(b->next)
b->next->prev = b;
}
return b;
}
有了上述方法,free的实现思路就比较清晰了:首先检查参数地址的合法性,如果不合法则不做任何事;否则,将此block的free标为1,并且在可以的情况下与后面的block进行合并。如果当前是最后一个block,则回退break指针释放进程内存,如果当前block是最后一个block,则回退break指针并设置first_block为NULL。实现如下:
void free(void*p){
t_block b;
if(valid_addr(p)){
b = get_block(p);
b->free =1;
if(b->prev && b->prev->free)
b = fusion(b->prev);
if(b->next)
fusion(b);
else{
if(b->prev)
b->prev->prev = NULL;
else
first_block = NULL;
brk(b);
}
}
}
3.2.8 realloc的实现
为了实现realloc,我们首先要实现一个内存复制方法。如同calloc一样,为了效率,我们以8字节为单位进行复制:
void _block(t_block src, t_block dst){
size_t*sdata,*ddata;
size_t i;
sdata = src->ptr;
ddata = dst->ptr;
for(i =0;(i *8)< src->size &&(i *8)< dst->size; i++)
ddata[i]= sdata[i];
}
然后我们开始实现realloc。一个简单(但是低效)的方法是malloc一段内存,然后将数据复制过去。但是我们可以做的更高效,具体可以考虑以下几个方面:
如果当前block的数据区大于等于realloc所要求的size,则不做任何操作
如果新的size变小了,考虑split
如果当前block的数据区不能满足size,但是其后继block是free的,并且合并后可以满足,则考虑做合并
下面是realloc的实现:
void*realloc(void*p,size_t size){
size_t s;
t_block b,new;
void*newp;
if(!p)
/* 根据标准库文档,当p传入NULL时,相当于调用malloc */
return malloc(size);
if(valid_addr(p)){
s = align8(size);
b = get_block(p);
if(b->size >= s){
if(b->size - s >=(BLOCK_SIZE +8))
split_block(b,s);
}else{
/* 看是否可进行合并 */
if(b->next && b->next->free
&&(b->size + BLOCK_SIZE + b->next->size)>= s){
fusion(b);
if(b->size - s >=(BLOCK_SIZE +8))
split_block(b, s);
}else{
/* 新malloc */
newp = malloc (s);
if(!newp)
return NULL;
new= get_block(newp);
_block(b,new);
free(p);
return(newp);
}
}
return(p);
}
return NULL;
}
3.3 遗留问题和优化
以上是一个较为简陋,但是初步可用的malloc实现。还有很多遗留的可能优化点,例如:
同时兼容32位和64位系统
在分配较大快内存时,考虑使用mmap而非sbrk,这通常更高效
可以考虑维护多个链表而非单个,每个链表中的block大小均为一个范围内,例如8字节链表、16字节链表、24-32字节链表等等。此时可以根据size到对应链表中做分配,可以有效减少碎片,并提高查询block的速度
可以考虑链表中只存放free的block,而不存放已分配的block,可以减少查找block的次数,提高效率
还有很多可能的优化,这里不一一赘述。下面附上一些参考文献,有兴趣的同学可以更深入研究。
⑺ linux进程怎么区分系统进程
1:没有内核进程和用户进程之分;
2:每个进程可以在用户态运行和内核态运行;
3:每个进程可以认为是一个指令运行路线+背景信息(如:打开文件),这些指令沿着路线的执行会影响到进程的信息(进程的路线是由CPU的eip决定的);
4:进程的切换:进程的切换需要由当前正在运行的进程准备好“切换到的进程”的相关信息的设置(如:current_proc),然后在执行一个CPU指令(如:ljmp可以通过TSS完成相关寄存器的设置),在这之后,地址空间变了,且CPU按照新的的eip执行了,也就是说新的进程得到了运行。
⑻ reboot 自动重启如何编写
使用notifier_call_chain向其它部分发出重启的消息,然后调用machine_restart函数完成重启。
machine_restart函数的开始部分有一段SMP相关的代码,主要完成多CPU时由一个CPU完成重启操作,其它CPU处于等待状态。之后系统根据一个变量reboot_thru_bios的内容判断重启方式,通过阅读reboot_setup我们可以得知,这个参数的内容是在系统启动时指定的,决定了是否利用bios,事实上是系统复位后的入口(FFFF:0000)地址的程序进行重启。在不通过bios进行重启的情况下,系统首先设定了重启标志,然后向端口0xfe写入数字0x64,这种重启的具体原理我还不大清楚,似乎是模拟了一次reset键的按下,希望大家和我讨论。在通过bios重启的情况下,系统同样先设定了重启模式,然后切换到了实模式,通过一条ljmp $0xffff,$0x0完成了重启。
⑼ 芯片(单片机)是怎么认识程序转化来的二进制,0是低电平,1是高电平使硬件运行呢
手机不再是单纯的手机,它是有情感的智能机器人,而它的芯片被别人控制,我们不仅仅要学会和人相处,更要学会和手机处理好关系,它一不高兴,就有办法给我们设置阻碍,天下之大,能人倍出,望老天爷快速研究出新的东西来智胜它吧,不想被手机控制,望转发!保护自己自己的隐私是每个公民的权利!
⑽ 什么是软件陷阱
所谓软件陷阱,就是一条引导指令,强行将乱飞的程序引向一个指定的地址,在那里有一段专门对程序出错进行处理的程序。如果我们把这段程序的入口标号称为ERR的话,软件陷阱即为一条LJMP ERR指令。为加强其捕捉效果,一般还在它前面加2条NOP指令。