版權(quán)說明:本文檔由用戶提供并上傳,收益歸屬內(nèi)容提供方,若內(nèi)容存在侵權(quán),請進行舉報或認領(lǐng)
文檔簡介
Linux啟動(1)【轉(zhuǎn)】
LinuxB^}\kemeKarch\arm\boot\compressed\head.S分析
這段代碼是linuxboot后執(zhí)行的第一個程序,完成的主要工作是解壓內(nèi)核,然后
跳轉(zhuǎn)到相關(guān)執(zhí)行地址。這部分代碼在做驅(qū)動開發(fā)時不需要改動,但分析其執(zhí)行流
程對是理解android的第一步
開頭有一段宏定義這是gnuarm匯編的宏定義。關(guān)于GUN的匯編和其他編譯器,
在指令語法上有很大差別,具體可查詢相關(guān)GUN匯編語法了解
另外此段代碼必須不能包括重定位部分。因為這時一開始必須要立即運行的。所
謂重定位,比如當編譯時某個文件用到外部符號是用動態(tài)鏈接庫的方式,那么該
文件生成的目標文件將包含重定位信息,在加載時需要重定位該符號,否則執(zhí)行
時將因找不到地址而出錯
#ifdefDEBUG〃開始是調(diào)試用,主要是一些打印輸出函數(shù),不用關(guān)心
#ifdefined(CONFIG_DEBUGJCEDCC)
……具體代碼略
#endif
宏定義結(jié)束之后定義了一個段,
.section".start",#alloc,#execinstr
這個段的段名是.start,#alloc表示Sectioncontainsallocateddata,
#execinstr表示Sectioncontainsexecutableinstructions.
生成最終映像時,這段代碼會放在最開頭
.align
start:
.typestart,#function/*.type指定start這個符號是函數(shù)類型*/
.rept8
movrO,rO〃將此命令重復(fù)8次,相當于nop,這里是為中斷向量保存空間
.endr
bIf
.word0x016f2818@Magicnumberstohelptheloader
.wordstart@absoluteload/runzlmage
〃此處保存了內(nèi)核加載和運行的地址,實質(zhì)上也是本函數(shù)的運行地址
address
.word_edata@內(nèi)核結(jié)束地址
〃注意這些地址在頂層vmlixu.lds(具體在/kernel文件夾里)里進行了定義,是
鏈接的地址,加載內(nèi)核后可能會進行重定位
1:movr7,rl@保存architectureID,這里是從bootload傳遞進來的
movr8,r2@保存參數(shù)列表atags指針
rl和己中分別存放著由bootloader傳遞過來的architectureID和指向標記列表
的指針。這里將這兩個參數(shù)先保存。
#ifndef_ARM_ARCH_2_
/*
*BootingfromAngel-needtoenterSVCmodeanddisable
*FIQs/IRQs(numericdefinitionsfromangelarm.hsource).
*Weonlydothisifwewereinusermodeonentry.
7
讀取cpsr并判斷是否處理器處于supervisor模式從bootload進入kernel.
系統(tǒng)已經(jīng)處于SVC32模式;而利用angel進入則處于user模式,還需要額外兩
條指令。之后是再次確認中斷關(guān)閉,并完成cpsr寫入
Angel是ARM的調(diào)試協(xié)議,一般用的是MULTMCE°ANGLE需要在板子上有駐
留程序,然后通過串口就可以調(diào)試了。用過的AXD或trace調(diào)試環(huán)境的話,對此
應(yīng)該比較熟悉。
not_angel:〃若不是通過angel調(diào)試進入內(nèi)核
mrsr2,cpsr@turnoffinterruptsto
orrr2,r2,#0xc0@preventangelfromrunning
msrcpsr_c,r2〃這里將cpsr中I、F位分別置“1”,關(guān)閉IRQ和FIQ
#else
teqppc,#0x0c000003@turnoffinterrupts
常用TEQPPC,#(新模式編號)來改變模式
#endif
另外鏈接器會把一些處理器相關(guān)的代碼鏈接到這個位置,也就是
arch/arm/boot/compressed/head-xxx.S文件中的代碼。在高通平臺下,這個
文件是head-msm.S連接腳是compress/vmlinux.lds,其中部分內(nèi)容大致如下
在連接時,連接器根據(jù)每個文件中的段名將相同的段合在一起,比如將head.S
和head-msm.S的.start段合在一起
SECTIONS
=TEXT_START;
_text=
.text:{
_start=
*(.start)
*(.text)
*(.text.*)
*(.fixup)
*(.gnu.warning)
*(.rodata)
*(.rodata.*)
*(.glue_7)
*(.glue_7t)
*(.piggydata)
=ALIGN(4);
)
_etext=
)
下面即進入.text段
.text
adrrO,LCO〃當前運行時LCO符號所在地址位置,注意,這里用的是adr指令,
這個指令會根據(jù)目前PC的值,計算符號相對于PC的位置,是個相對地址。之所
以這樣做,是因為下面指令用到了絕對地址加載Idmia指令,必須要調(diào)整確定目
前LCO的真實位置,這個位置也就是用adr來計算
IdmiarO,{rl,r2,r3,r4,r5,r6,ip,sp}
subsrO,rO,rl@〃這里獲得當前LCDO實際地址與鏈接地址差值
〃rl即是LC0的連接地址,也即由vmlinux.lds定位的地址
〃差值存入rO中。
beqnotjelocated〃如果相等不需要重定位,因為已經(jīng)在正確的〃地址運行了,
重定位的原因是,MMU單元未使能,不能進行地址映射,必須要手工重定位。
下面舉個簡單例子說明:
如果連接地址是OxcOOOOOOO,那么LCO的連接地址假如連接為OxcOOOOOlO,那
么LCO相對于連接起始地址的差為0x10,當此段代碼是從OxcOOOOOOO運行的話,
那么執(zhí)行adrr0,LCO的值實際上按下面公式計算:
RO=PC+OxlO,由于PC=連接處的值,可知,此時是在ram中運行,同理如果
是在不是在連接處運行,則假設(shè)是在0x00000000處運行,則
R0=0x00000000+0xl0,可知,此時不是在ram的連接處運行。
上面這幾行代碼用于判斷代碼是否已經(jīng)重定位到內(nèi)存中,LCO這個符號在head.S
中定義如下,實質(zhì)上相當于c語言的全局數(shù)據(jù)結(jié)構(gòu),結(jié)構(gòu)的每個域存儲的是一個
指針。指針本身的值代表不同的代碼段,已經(jīng)在頂層連接腳本vmlinuxJds里進
行了賦值,比如一start是內(nèi)核開始的地址
.typeLCO,#object
LCO:.wordLCO@rl〃這個要加載到rl中的LCO是鏈接時LCO的地址
.word_bss_start@r2
.word_end@r3
.wordzreladdr@r4
.word_start@r5
.word_got_start@r6
.word_got_end@ip
.worduser_stack+4096@sp
通過當前運行時LCO的地址與鏈接器所鏈接的地址進行比較判斷。若相等則是運
行在鏈接的地址上。
如果不是運行在鏈接的地址上,則下面的代碼必須修改相關(guān)地址,進行重新運行
/*
*r5-zlmagebaseaddress
*r6-GOTstart
*ip-GOTend
7
〃修正實際運行的位置,否則跳轉(zhuǎn)指令就找不到相關(guān)代碼
add⑸r5,rO〃修改內(nèi)核映像基地址
addr6,r6,rO
addip,ip,rO〃修改got表的起始和結(jié)束位置
#ifndefCONFIG_ZBOOT_ROM
/*若沒有定義CONFIG_ZBOOT_ROM,此時運行的是完全位置無關(guān)代碼
位置無關(guān)代碼,也就是不能有絕對地址尋址。所以為了保持相對地址正確,
需要將bss段以及堆棧的地址都進行調(diào)整
*r2-BSSstart
*r3-BSSend
*sp-stackpointer
7
add22ro
addr3,r3,rO
addsp,sp,rO
〃全局符號表的地址也需要更改,否則,對全局變量引用將會出錯
1:Idrrl,[r6,#0]@relocateentriesintheGOT
addrl,rl,rO@table.Thisfixesupthe
strrl,[r6],#4@Creferences.
cmpr6,ip
biolb
#else〃若定義了CONFIG_ZBOOT_ROM,只對got表中在bss段以外的符號
進行重定位
1:Idrrl,[r6,#0]@relocateentriesintheGOT
cmprl,r2@entry<bss_start||
cmphsr3,rl@_end<entry
addlorl,rl,rO@table.Thisfixesupthe
strrl,[r6],#4@Creferences.
cmpr6,ip
biolb
#endif
如果運行當前運行地址和鏈接地址相等,則不需進行重定位。直接清除bss段
not_relocated:movrO,#0
1:strrO,[r2],#4@clearbss
strrO,[r2],#4
strrO,[r2],#4
strrO,[r2],#4
cmpr2,r3
biolb
之后跳轉(zhuǎn)到cache_on處
blcache_on
cache_on定義
.align5
cache_on:movr3,#8@cache_onfunction
bcall_cache_fn
把r3的值設(shè)為8。這是一個偏移量,也就是索引projtypes中的操作函數(shù)。
然后跳轉(zhuǎn)到call_cache_fn。這個函數(shù)的定義如下:
call_cache_fn:
adrrl2,proc_types〃把proc_types的相對地址加載到r12中
#ifdefCONFIG_CPU_CP15
mrcpl5,0,r6,cO,cO@getprocessorID
#else
Idrr6,=CONFIG_PROCESSOR_ID
#endif
1:Idrrl,[rl2,#0]@getvalue
Idrr2,[rl2,#4]@getmask
eorrl,rl,r6@(realAmatch)
tstrl,r2@是否和CPUID匹配?
addeqpc,rl2,r3@用剛才的偏移量,查找〃到cache操作函數(shù),找到后就執(zhí)行
相關(guān)操作,比如執(zhí)行b_armv7_mmu_cache_on
//
addrl2,rl2,#4*5〃如果不相等,則偏移到下個proc_types結(jié)構(gòu)處
bib
addeqpc,rl2,r3@callcachefunction
proc_type的定義如下,實質(zhì)上還是一張數(shù)據(jù)結(jié)構(gòu)表
.typeproc_types,#object
proc_types:
.word0x41560600@ARM6/610
.wordOxffffffeO
b_arm6_mmu_cache_off@works,butslow
b_arm6_mmu_cache_off
movpc,Ir
@b_arm6_mmu_cache_on@untested
@b_arm6_mmu_cache_off
@b_armv3_mmu_cache_flush
.word0x00000000@oldARMID
.wordOxOOOOfOOO
movpc,Ir
movpc,Ir
movpc,Ir
.word0x41007000@ARM7/710
.word0xfff8fe00
b_arm7_mmu_cache_off
b_arm7_mmu_cache_off
movpc,Ir
.word0x41807200@ARM720T(writethrough)
.wordOxffffffOO
b_armv4_mmu_cache_on
b_armv4_mmu_cache_off
movpc,Ir
.word0x41007400@ARM74x
.wordOxffOOffOO
b_armv3_mpu_cache_on
b_armv3_mpu_cache_off
b_armv3_mpu_cache_flush
.word0x41009400@ARM94x
.wordOxffOOffOO
b_armv4_mpu_cache_on
b_armv4_mpu_cache_off
b_armv4_mpu_cache_flush
.word0x00007000@ARM7IDs
.wordOxOOOOfOOO
movpc,Ir
movpc,Ir
movpc,Ir
@EverythingfromhereonwillbethenewIDsystem.
.word0x4401al00@sallO/sallOO
.wordOxffffffeO
b_armv4_mmu_cache_on
b_armv4_mmu_cache_off
b_armv4_mmu_cache_flush
.word0x6901bll0@salllO
.wordOxfffffffO
b_armv4_mmu_cache_on
b_armv4_mmu_cache_off
b_armv4_mmu_cache_flush
@ThesematchonthearchitectureID
.word0x00020000@
.word0x00Of0000//
b_armv4_mmu_cache_on
b_armv4_mmu_cache_on〃指令的地址
b_armv4_mmu_cache_off
b_armv4_mmu_cache_flush
.word0x00050000@ARMv5TE
.word0x00Of0000
b_armv4_mmu_cache_on
b_armv4_mmu_cache_off
b_armv4_mmu_cache_flush
.word0x00060000@ARMv5TEJ
.word0x00Of0000
b_armv4_mmu_cache_on
b_armv4_mmu_cache_off
b_armv4_mmu_cache_flush
.word0x0007bOO0@ARMv6
.word0x0007f000
b_armv4_mmu_cache_on
b_armv4_mmu_cache_off
b_armv6_mmu_cache_flush
.word0@unrecognisedtype
.word0
movpc,Ir
movpc,Ir
movpc,Ir
.sizeproc_types,.-proc_types
找到執(zhí)行的cache函數(shù)后,就用上面的addeqpc,rl2,r3直接跳轉(zhuǎn),例如執(zhí)行
下面這個處理器結(jié)構(gòu)的cache函數(shù)
Linux啟動(2)【轉(zhuǎn)】
\kerneKarch\arm\boot\compressed\head?S分析(2)
_armv7_mmu_cache_on:
movrl2"r〃注意,這里需要手工保存返回地址??!這樣做的原因是下面的bl
指令會覆蓋掉原來的Ir,為保證程序正確返回,需要保存原來Ir的值
bl_setup_mmu
movrO,#0
mcrpl5,0,rO,c7,clO,4@drainwritebuffer
mcrpl5,0,rO,c8,c7,0@flushI,DTLBs
mrcpl5,0,rO,cl,cO,0@readcontrolreg
orrrO,rO,#0x5000@I-cacheenable,RRcachereplacement
orrrO,rO,#0x0030
bl_common_mmu_cache_on
movrO,#0
mcrpl5,0,rO,c8,c7,0@flushI,DTLBs
movpc,rl2〃返回至Ucache_on
這個函數(shù)首先執(zhí)行_setup_mmu,然后清空writebuffer、I/Dcache、TLB.接著
打開i-cache,設(shè)置為Round-robinreplacement。調(diào)用
_common_mmu_cache_on,JTJFmmu和d-cache.把頁表基地址和域訪問控
制寫入?yún)f(xié)處理器寄存器c2、c3._common_mmu_cache_on函數(shù)數(shù)定義如下:
_common_mmu_cache_on:
#ifndefDEBUG
orrrO,rO,#0x000d@Writebuffer,mmu
#endif
movrl,#-1〃-1的補碼是ffffffff,
mcrpl5,0,r3,c2,cO,0@把頁表地址存于協(xié)處理器寄存器中
mcrpl5,0,rl,c3,cO,0@設(shè)置domainaccesscontrol寄存器
blf
.align5@cachelinealigned
1:mcrpl5,0,rO,cl,cO,0@loadcontrolregister
mrcpl5,0,rO,cl,cO,0@andreaditbackto
subpc,Ir,rO,Isr#32@properlyflushpipeline
重點來看一下_setup_mmu這個函數(shù),定義如下:
_setup_mmu:subr3,r4,#16384@Pagedirectorysize
bicr3,r3,#Oxff@Alignthepointer
bicr3,r3,#Ox3fOO
這里r4中存放著內(nèi)核執(zhí)行地址,將16K的一級頁表放在這個內(nèi)核執(zhí)行地址下面的
16K空間里,上面通過subr3,r4,#16384獲得16K空間后,又將頁表的起始地
址進行16K對齊放在r3中。即ttb的低14位清零。
〃初始化頁表,并在RAM空間里打開cacheable和bufferable位
movrO,r3
movr9,rO,Isr#18
movr9,r9,Isl#18@startofRAM
addrlO,r9,#0x10000000@areasonableRAMsize
上面這幾行把一級頁表的起始地址保存在r0中,并通過rO獲得一個ram起始地
址(每個頁面大小為1M)然后映射256Mram空間,并把對應(yīng)的描述符的C和B
位均置“1”
movrl,#0x12〃一級描述符的bit[l:O]為10,表示這是一個section描述符。
也即分頁方式為段式分頁
orrrl,rl,#3<<10〃一級描述符的accesspermissionbits為31.
即
add2r3,#16384〃?級描述符表的結(jié)束地址存放在己中。
1:cmprl,r9@ifvirt>startofRAM
orrhsrl,rl,#0x0c@setcacheable,bufferable
cmprl,rlO@ifvirt>endofRAM
bichsrl,rl,#0x0c@clearcacheable,bufferable
strrl,[rO],#4@1:1mapping
addrl,rl,#1048576〃下個IM物理空間,每個頁框IM。
teqrO,r2
bnelb
因為打開cache前必須打開mmu,所以這里先對頁表進行初始化,然后打開
和
mmucache0
上面這段就是對一級描述符表(頁表)的初始化,首先比較這個描述符所描述的
地址是否在那個256M的空間中,如果在則這個描述符對應(yīng)的內(nèi)存區(qū)域是
如果不在則然后將描
cacheable,bufferableononcacheable,nonbufferable.
述符寫入一個一級描述符表的入口,并將一級描述符表入口地址加4,而指向下一
個IMsection的基地址。如果頁表入口未初始化完,則繼續(xù)初始化。
頁表大小為16K,每個描述符4字節(jié),剛好可以容納4096個描述符,每個描述符
映射1M,那么4096*所以這里就映射了4O96*1M=4G的空間。因此16K的頁
完全可以把256M地址空間全部映射
movrl,#0xle
orrrl,rl,#3<<10〃這兩行將描述的bit[ll:10]bit[4:l]置位,
〃具體置位的原因,在ARM11的頁表項描述符里有說明,由于沒找到完整的文檔,
這里只給出圖示:
movr2,pc,Isr#20
orrrl,rl,r2,Isl#20〃將當前地址進IM對齊,并與rl中的內(nèi)容結(jié)合形成一個描
述當前指令所在section的描述符。
addr0,r3,r2,Isl#2〃r3為剛才建立的一級描述符表的起始地址。通過將當前地
〃址(pc)的高12位左移兩位(形成14位索弓I)與r3中的地址
〃(低14位為0)相加形成一個4字節(jié)對齊的地址,這個
〃地址也在16K的一級描述符表內(nèi)。當前地址對應(yīng)的
〃描述符在一級頁表中的位置
strrl,[r0],#4
addrl,rl,#1048576
strrl,r0]〃這里將上面形成的描述符及其連續(xù)的下一個section描述
〃寫入上面4字節(jié)對齊地址處(一級頁表中索引為r2左移
〃2位)
movpc,lr〃返回,調(diào)用此函數(shù)時,調(diào)用指令的下一語句movr0,#0的地址保存
在lr中
這里進行的是一致性的映射,物理地址和虛擬地址是一樣。
_common_mmu_cache_on最后執(zhí)行movpc,rl2返回cache_on,為何返回
到的是cacheqn呢?這就是上面解釋保存上的原因,因為原來的lr保存了執(zhí)行
blcache_on語句的下條指令,因此能正確返回!
下一條指令也即是下面開始
movrl,sp@??臻g大小是4096字節(jié),那〃么在??臻g地址上面再分配64K字節(jié)
空間
addr2,sp,#0x10000@分配64k字節(jié)。
棧的分配如下:
.align
.section".stack","w"
user_stack:.space4096〃lc0對SP進行了定義.worduser_stack+4096@sp
由此可見sp是往下增長的
分配了解壓縮用的緩沖區(qū),那么接下來就判斷這個數(shù)據(jù)區(qū)是否和我們目前運行的
代碼空間重疊,如果重疊則需調(diào)整
/*
*Checktoseeifwewilloverwriteourselves.
*r4=finalkerneladdress
*r5=startofthisimage
*r2=endofmallocspace(andthereforethisimage)
*Webasicallywant:
*r4>=r2->OK
*r4+imagelength<=r5->OK
7
cmpr4,r2
bhswont_overwrite
subr3,sp,r5@>compressedkernelsize
addrO,r4,r3,Isl#2@allowfor4xexpansion
cmprO,r5
blswont_overwrite
緩沖區(qū)空間的起始地址和結(jié)束地址分別存放在rl、r2中。然后判斷最終內(nèi)核地址,
也就是解壓后內(nèi)核的起始地址,是否大于malloc空間的結(jié)束地址,如果大于就
跳到wont_overwrite執(zhí)行,wont_overwrite函數(shù)后面會講到。否則,檢查最終
內(nèi)核地址加解壓后內(nèi)核大小,也就是解壓后內(nèi)核的結(jié)束地址,是否小于現(xiàn)在未解
壓內(nèi)核映像的起始地址。小于也會跳到wont_owerwrite執(zhí)行。如兩這兩個條件
都不滿足,則繼續(xù)往下執(zhí)行。
movr5,r2@decompressaftermallocspace
movrO,r5
movr3,r7
bldecompress_kernel
這里將解壓后內(nèi)核的起始地址設(shè)為malloc空間的結(jié)束地址。然后后把處理器id
(開始時保存在r7中)保存到r3中,調(diào)用decompress_kernel開始解壓內(nèi)核。
這個函數(shù)的四個參數(shù)分別存放在rO-r3中,它在
arch/arm/boot/compressed/misc.c中定義。解壓的過程為先把解壓代碼放到
緩沖區(qū),然后從緩沖區(qū)在拷貝到最終執(zhí)行空間。
addrO,rO,#127
bicrO,rO,#127@alignthekernellength
/*
*rO=decompressedkernellength
*rl-r3=unused
*r4=kernelexecutionaddress
*r5=decompressedkernelstart
*r6=processorID
*r7=architectureID
*r8=atagspointer
*r9-rl4=corrupted
7
addrl,r5,rO@endofdecompressedkernel
adrr2,reloc_start
Idrr3,LC1
addr3,r2,r3
1:Idmiar2!,{r9-rl4}@copyrelocationcode
stmiarl!,{r9-rl4}
Idmiar2!,{r9-rl4}
stmiarl!({r9-rl4}
cmpr2,r3
biolb
這里首先計算出重定位段,也即relojstart段,然后對它的進行重定位
blcache_clean_flush
addpc,r5,rO@callrelocationcode
重定位結(jié)束后跳到解壓后執(zhí)行bcalLkernel,不再返回。call_kernel定義如下:
call_kernel:
blcache_clean_flush
blcache_off
movrO,#0@mustbezero
movrl,r7@restorearchitecturenumber
movr2,r8@restoreatagspointer
movpc,r4@callkernel
在運行解壓后內(nèi)核之前,先調(diào)用了
cachecleanflush這個函數(shù)。這個函數(shù)的定義如下:
cachecleanflush:
movr3,#16
bcall_cache_fn
其實這里又調(diào)用了call_cache_fn這個函數(shù),注意,這里r3的值為16,上面對cache
操作已經(jīng)比較詳細,不再討論。
刷新cache后,則執(zhí)行movpc,r4跳入內(nèi)核,開始進行下個階段的處理。
整個代碼流程如下:
機制
_lookup_processortype,_lookupmachinetype,_vetatags函數(shù)都在
kemel\head-comm.S內(nèi),這個文件實際上是被包含在head.S內(nèi)
Linux之所以把搜索機器類型和CPU類型獨立出來,就是為了讓內(nèi)核盡可能的和
bootload獨立,增強移植性,把不同CPU的差異性處理減到最小。比如不同ARM
架構(gòu)的CPU處理中斷的,打開MMU,each操作是不同的,因此,在內(nèi)核開始
執(zhí)行前需要定位CPU架構(gòu),比如高通利用的ARM11,Ti用的cortex-8架構(gòu)
_lookupmachinetype尋找的機器類型結(jié)構(gòu)定義在arch\arm\include\asm\mach.h
中
查詢方法比較簡單,利用bootloa傳進來的參數(shù)依次查詢上述結(jié)構(gòu)表項
這個表項是在編譯階段將#defineMACHINE_START(_type,_name)宏定義的結(jié)構(gòu)
體structmachinedesc連接到
_arch_infb段,那么結(jié)構(gòu)體開始和結(jié)束地址用_arch_infb_begin和
_archinfbend符號引用
3:.long.
.long_archinfobegin
.long_archinfbend
//rl=機器架構(gòu)代碼number,由bootload最后階段傳進來
.type_lookupmachinetype,%function
_lookupmachinetype:
adrr3,3b
Idmiar3,{r4,r5,r6}
subr3,r3,r4@此時沒有開MMU,因此需要確定放置_arch_info_begin的實際物
理地址
addr5/5,r3@調(diào)整地址,找到_arch_infb的實際地址(連接地址和物理地址不?
定一樣,因此需要調(diào)整)
addr6,r6,r3@
l:ldrr3,[r5,#MACHINFO_TYPE]@MACHINFO_TYPE=機器類型域的偏移量
teqr3,rl@是否和bootload傳進來的參數(shù)相同?
beq2f@找到則跳出循環(huán)
addr5,r5,#SIZEOF_MACHINE_DESC@地址偏移至下個_arch_inf表項
cmpr5,r6
biolb
movr5,#0@未知的類型
2:movpc,lr//返回
_lookup_processor_type的查詢的結(jié)構(gòu)為structproc_info_list
機器類型確定后即開始解析(_vet_atags)內(nèi)核參數(shù)列表,判斷第一個參數(shù)類型
是不是ATAG_COREo
內(nèi)核參數(shù)列表一般放在內(nèi)核前面16K地址空間處。列表的表項由structtag構(gòu)成,
每個structtag有常見的以下類型:
:ATAG_CORE、ATAG_MEM、ATAG_CMDLINE、ATAG_RAMDISK>ATAGJNITRD等。
這些類型是宏定義,比如#defineATAG_CORE0x54410001
arch\ann\include\asm\setup.h
structtag_header{
_u32size;
_u32tag;
};
structtag{
structtag_headerhdr;
union{
structtag_corecore;//有效的內(nèi)核
structtag_mem32mem;
structtag_videotextvideotext;
structtagramdiskramdisk;〃文件系統(tǒng)
structtaginitrdinitrd;//臨時根文件系統(tǒng)
structtagserialnrserialnr;
structtagrevisionrevision;
structtag_videolfbvideolfb;
structtag_cmdlinecmdline;〃命令行
);
接下來就是創(chuàng)建頁表,因為要使能MMU進行虛擬內(nèi)存管理,因此必須創(chuàng)建映射
用的頁表。頁表就像一個函數(shù)發(fā)生器,保證訪問虛擬地址時能從物理地址里取到
正確代碼
pgtblr4@pagetableaddress
〃頁表放置的位置可由下面的宏確定,即在內(nèi)核所在空間的前16K處
.macropgtbl,rd
Idr\rd,=(KERNEL_RAM_PADDR-0x4000)
.endm
movrO,r4
movr3,#0
addr6,rO,#0x4000//16K的空間,r6即是頁表結(jié)束處
1:strr3,[rO],#4〃清空頁表項,頁表項共有16K/4項
strr3,[r0],#4
strr3,[rO],#4
strr3,[rO],#4
teqrO,r6
bnelb
Idrr7,[rlO,#PROCINFO_MM_MMUFLAGS]
//從從差得的procinfblist結(jié)構(gòu)PROCINFOMMMMUFLAGS處獲取MMU的
信息
/*
為內(nèi)核創(chuàng)建IM的映射空間,這里是按照1:1一致映射,即代碼的基地址(高12bit)
對應(yīng)相同的物理塊地址。這種映射關(guān)系只是在啟動階段,在跳進start_kemel后
會被paging_init().移除。這種映射可以直接利用當前地址的高12bit作為基地址,
這種方式很巧妙,因為當前的PC(加顏色處的地址)依然在1M空間內(nèi),因此,高
12bit(段基地址)在1M空間內(nèi)都是相同的。
*/
movr6,pc,Isr#20@內(nèi)核映像的基地址
orrr3,r7,r6,Isl#20@基地址偏移后再加上標示符,即可得一個頁表項的值
strr3,[r4,r6,Isl#2]@將此表項按照頁表項的索引存入對應(yīng)的表項中。比如,若〃
基地址是OxcOOO1000,那么存入頁表的第OxcOO項中
//目前的映射依然是1:1的映射
〃然后移到下個段基地址處,開始映射此KERNEL_START對應(yīng)的空間
//這個空間映射的物理地址與上面的相同,也就是兩個虛擬地址映射到了同一個
物理地址空間
〃r0+基地址組成〃在第一級頁表中索引到相關(guān)的項
addr0,r4,#(KERNEL_START&OxffDOOOOO)?18
strr3,[rO,#(KERNEL_START&OxOOfOOOOO)?18]!
Idrr6,=(KERNEL_END-1)
addrO,rO,#4〃移到下個表項
addr6,r4,r6,Isr#18〃結(jié)束的基地址
1:cmprO,r6
addr3,r3,#1?20〃下個IM物理地址空間
strlsr3,[rO],#4〃建立映射表項,開始創(chuàng)建所有的內(nèi)核空間頁表項
bls1b//
#ifdefCONFIGXIPKERNEL
/*
*Mapsomeramtocoverour.dataand.bssareas.
*/
orrr3,r7,#(KERNEL_RAM_PADDR&OxffOOOOOO)
.if(KERNELRAMPADDR&OxOOfOOOOO)
onr3,r3,#(KERNEL_RAM_PADDR&OxOOfOOOOO)
.endif
addr0,r4,#(KERNEL_RAM_VADDR&OxffOOOOOO)?18
strr3,[rO,#(KERNEL_RAM_VADDR&OxOOfOOOOO)?18]!
Idrr6,=(_end-1)
addrO,rO,#4
addr6,r4,r6,Isr#18
1:cmprO,r6
addr3,r3,#1?20
strIsr3,[rO],#4
blslb
#endif
/*
*Thenmapfirst1MBoframincaseitcontainsourbootparams.
*/
//虛擬ram地址的第一個IM空間包含了參數(shù)列表,也需要映射
addr0,r4,#PAGE_OFFSET?18
orrr6,r7,#(PHYS_OFFSET&OxffOOOOOO)
.if(PHYS_OFFSET&OxOOiDOOOO)
orrr6,r6,#(PHYS_OFFSET&OxOOfOOOOO)
.endif
strr6,[rO]
movpc,h7/頁表建立完成,返回
頁表創(chuàng)建后,具體的映射空間如下圖:
執(zhí)行完上述頁表創(chuàng)建,開始執(zhí)行內(nèi)核跳轉(zhuǎn):
Idrrl3,_switchdata@addresstojumptoafter
@mmuhasbeenenabled
adrIr,_enablemmu@return(PIC)address
addpc,rlO,#PROCINFO_INITFUNC
_switch_data是一個數(shù)據(jù)結(jié)構(gòu),如下
.type_switchdata,%object
_switch_data:
.longmmapswitched
.long_dataloc@r4
.long_data_start@r5
.long_bss_start@r6
.long_end@r7
.longprocessor_id@r4
.long_machine_arch_type@r5
.long_atags_pointer@r6
.longcr_alignment@r7
Jonginitthreadunion+THREADSTARTSP@sp
語句“addpc,rlO,#PROCINFOJNnTUNC"通過查表調(diào)用proc-v7.s中
_v7_setup函數(shù),該函數(shù)末尾通過將lr寄存器賦給pc,導(dǎo)致對_enablemmu的
詞用;完成使能mmu的操作,之后將rl3寄存器值賦給pc,調(diào)甬_switdi_data
數(shù)據(jù)結(jié)構(gòu)中的第一個函數(shù)_mmap_switched,
.typemmapswitched,%function
_mmap_switched:
adrr3,_switchdata+4
Idmiar3!,{r4,r5,r6,r7}
cmpr4,r5@拷貝數(shù)據(jù)段
1:cmpner5,r6
Idmefp,[r4],#4
strnefp,[r5],#4
bnelb
movfp,#0@清除BSS段
1:cmpr6,r7
strccfp,[r6],#4
bcclb
Idmiar3,{r4,r5,r6,r7,sp}//然后調(diào)整指針到processor_id域
strr9,[r4]@保存CPUID
strrl,[r5]@保存機器類型
strr2,[r6]@保存參數(shù)列表指針
bicr4,rO,#CR_A@Clear'A'bit
stmiar7,{rO,r4}@保存控制信息
bstartkemel
最終調(diào)用init\main.c文件中的startkemel函數(shù)。
這個startkemel正是kemel\init\main.c的內(nèi)核起始函數(shù)
Linux啟動(4)【轉(zhuǎn)】
Linux2.6啟動4--start_kernel篇
當內(nèi)核與體系架構(gòu)相關(guān)的匯編代碼執(zhí)行完畢,即跳入start_kernel。這個函數(shù)在
kernel/init/main.c中。由于這部分涉及l(fā)inux眾多數(shù)據(jù)結(jié)構(gòu)的初始化,包括內(nèi)核
命令行解析,內(nèi)存緩沖區(qū)建立初始化,頁面分配和初始化,虛擬文件系統(tǒng)建立,
根文件系統(tǒng)掛載,驅(qū)動文件掛載,二進制程序文件的執(zhí)行等,限于篇幅和理解水
平,只能流程上的大致梳理,以上提及方面后期再做詳細分析。為保證準確性,
參考了一部分書籍和網(wǎng)上技術(shù)文檔,如有疑問請及時提出,共同學(xué)習(xí)探討。
asmlinkagevoid_initstart_kernel(void)
(
char*commandjine;
externstructkernel_param_start_param[],_stop_pararnQ;
〃這里引用兩個符號,是內(nèi)核編譯腳本定位的內(nèi)核參數(shù)起始地址
smp_setup_processor_id();〃多CPU架構(gòu)的初始化,目前我們的高通linux側(cè)
是單核的,此多核不做分析
unwind_initO;〃本架構(gòu)中沒有用
lockdep_init();〃本架構(gòu)為空
debug_objects_early_init();
cgroup_init_earlyO;
local_irq_disable();
early_boot_irqs_offO;
early_init_irqJock_cIass();
lock_kemel();〃本架構(gòu)為空函數(shù)
tickjnitO;
〃時鐘中斷初始化函數(shù),調(diào)用clockevents_register_notifier函數(shù)向
時鐘事件鏈注冊時鐘控制函數(shù)這是個回調(diào)函
clockevents_chaintick_notifiero
數(shù),指明了當時鐘事件發(fā)生變化時應(yīng)該執(zhí)行的哪些操作,比如時鐘的掛起操作等
boot_cpu_init();〃用于多核CPU的初始化
page_address_initO;〃用于高地址內(nèi)存,我們者B用32位CPU,此函數(shù)為空
printk(KERN_NOTICE);
printk(linux_banner);
setup_arch(&command_line);
〃具體看一下這個架構(gòu)初始化函數(shù)完成哪些功能
void_initsetup_arch(char**cmdline_p)
(
structtag*tags=(structtag*)&init_tags;〃定義了一個默認的內(nèi)核參數(shù)列表
structmachine_desc*mdesc;
char*from=default_command_line;
setup_processor();〃匯編的CPU初始化部分已講過,不再討論
mdesc=setup_machine(machine_arch_type);
machine_name=mdesc->name;
if(mdesc->soft_reboot)
reboot_setup("s");
if(_atags_pointer)
tags=phys_to_virt(_atags_pointer);
elseif(mdesc->boot_params)
tags=phys_to_virt(mdesc->boot_params);
〃由于MMU單元已打開,此處需要而boot_params是物理地址,需要轉(zhuǎn)換成
虛擬地址才能訪問,因為此時CPU訪問的都是虛擬地址
/*
*Ifwehavetheoldstyleparameters,convertthemto
*ataglist.
7
〃內(nèi)核參數(shù)列表第一項必須是ATAGJ2ORE類型
if(tags->hdr.tag!=ATAGJZORE)〃如果不是,則需要轉(zhuǎn)換成新的內(nèi)核參數(shù)類
型,新的內(nèi)核參數(shù)類型用下面structtag結(jié)構(gòu)表示
convert_to_tag」ist(tags);〃此函數(shù)完成新舊參數(shù)結(jié)構(gòu)轉(zhuǎn)換
structtag{
structtag_headerhdr;
union{
structtag_corecore;
structtag_mem32mem;
structtag_videotextvideotext;
structtag_ramdiskramdisk;
structtagjnitrdinitrd;
structtag_serialnrserialnr;
structtag_revisionrevision;
structtag_videolfbvideolfb;
structtag_cmdlinecmdline;
}u;
};
〃舊的內(nèi)核參數(shù)列表用下面結(jié)構(gòu)表示
structparam_struct{
union{
struct{
unsignedlongpage_size;/*0*/
unsignedlongnr_pages;/*4*/
unsignedlongramdisk_size;/*8*/
unsignedlongflags;/*12*/
oooooooooooo〃車父長,省略
)
if(tags->hdr.tag!=ATAG_CORE)〃如果沒有內(nèi)核參數(shù)
tags=(structtag*)&init_tags;〃則選用默認的內(nèi)核參數(shù)
if(mdesc->fixup)
mdesc->fixup(mdesc,tags,&from,&meminfo);〃用內(nèi)核參數(shù)列表填充
meminfo
if(tags->hdr.tag==ATAG_CORE){
if(meminfo.nr_banks!=0)
squash_mem_tags(tags);
save_atags(tags);
parse_tags(tags);〃解析內(nèi)核參數(shù)列表,然后調(diào)用內(nèi)核參數(shù)列表的處理函數(shù)對這
些參數(shù)進行處理。比如,如果列表為命令行,則最終會用parse_tag_cmdlin函
數(shù)進行解析,這個函數(shù)用_tagtable編譯連接到了內(nèi)核里
_tagtable(ATAG_CMDLINE,parse_tag_cmdline);
)
〃下面是記錄內(nèi)核代碼的起始,結(jié)束虛擬地址
init_mm.start_code=(unsignedlong)&_text;
init_mm.end_code=(unsignedlong)&_etext;
init_mm.end_data=(unsignedlong)&_edata;
init_mm.brk=(unsignedlong)&_end;
〃下面是對命令行的處理,剛才在參數(shù)列表處理parse_tag_cmdline函數(shù)已把命
令行拷貝到了from空間
memcpy(boot_command_line,from,COMMAND_LINE_SIZE);
boot_command_line[COMMAND_LINE_SIZE-l]='\0';
parse_cmdline(cmdline_p,from);〃解析出命令行,命令行解析出以后,同樣會
調(diào)用相關(guān)處理函數(shù)進行處理。系統(tǒng)用_early_param宏在編譯階段把處理函數(shù)編
譯進內(nèi)核。
paging_init(&meminfo,mdesc);
〃這個函數(shù)完成頁表初始化,具體的方法為建立線性地址劃分后每個地址空間的
標志;清除在boot階段建立的內(nèi)核映射空間,也即把頁表項全部清零;調(diào)用
bootmemjnit,禁止無效的內(nèi)存節(jié)點,由于我們的物理內(nèi)存都是連續(xù)的空間,
因此,內(nèi)存節(jié)點為1個。接下來判斷INITRD映像是否存在,若存在則檢查其所在
的地址是否在一個有效的地址內(nèi),然后返回此內(nèi)存節(jié)點號。
先看兩個數(shù)據(jù)結(jié)構(gòu)。
structmeminfo表示內(nèi)存的劃分情況。Linux的內(nèi)存劃分為bank。每個bank
用
structmembank表示,start表示起始地址,這里是物理地址,size表示大小,
node表示此bank所在的節(jié)點號,對于只有一個節(jié)點的內(nèi)存,所有bank節(jié)點都
相等
structmembank{
unsignedlongstart;
unsignedlongsize;
intnode;
);
structmeminfo{
intnr_banks;
structmembankbank[NR_BANKS];
};
〃在pagejnit函數(shù)中比較重要的是bootmemjnit函數(shù),此函數(shù)在完成原來映
射頁表的清除后,最終調(diào)用bootmem_init_node如下:
bootmemjnit_node(intnode,intinitrd_node,structmeminfo*mi)
(
unsignedlongzone_size[MAX_NR_ZONES],zhole_size[MAX_NR_ZONES];
unsignedlongstart_pfn,end_pfn,boot_pfn;
unsignedintboot_pages;
pg_data_t*pgdat;//每個節(jié)點用pg_data_t描述,這個結(jié)構(gòu)用在非一致性內(nèi)存
中,我們的內(nèi)存只有一個,地址是連續(xù)的
inti;
start_pfn=-1UL;
end_pfn=0;
for_each_nodebank(i,mi,node){
structmembank*bank=&mi->bank[i];
unsignedlongstart,end;
start=bank->start>>PAGE_SHIFT;〃計算出頁表號,實際也表示第幾個物理
頁號
end=(bank->start+bank->size)>>PAGE_SHIFT;
if(start_pfn>start)
start_pfn=start;
if(end_pfn<end)
end_pfn=end;
map_memory_bank(bank);〃將每個節(jié)點的每個bank重新映射,比如重新映射
內(nèi)核空間
)
if(end_pfn==0)
returnend_pfn;
〃一個字節(jié)代表8個頁,因此找到一個
〃可放置這些所有自己的頁面即可。用一個bit位表示一個頁是否已占用,那么一
個字節(jié)為8個頁,比如4096個頁需要4096/8=512字節(jié),容納這個位圖需要一個
頁
boot_pages=bootmem_bootmap_pages(end_pfn-start_pfn);
boot_pfn=find_bootmap_pfn(node,mi,boot_pages);//在node節(jié)點內(nèi)存的
bank中找到一個可以放置位圖的頁面的頁面序列,然后返回這個頁面序列的首個
頁面號
node_set_online(node);〃設(shè)置本節(jié)點有效
pgdat=NODE_DATA(node);〃獲取節(jié)點描述符pgdat
init_bootmem_node(pgdat,boot_pfn,start_pfn,end_pfn);〃設(shè)置本節(jié)點內(nèi)所
有映射頁的位圖,即每個字節(jié)全部置為Oxff,表示已經(jīng)映射使用。然后填充pgda
結(jié)構(gòu)
for_each_nodebank(i,mi,node)
free_bootmem_node(pgdat,mi->bank[i].start,mi->bank。].size);〃設(shè)置每
個映射的頁面空閑,實際是對位圖的操作,對每個bit清零
reserve_bootmem_node(pgdat,boot_pfn<<PAGE_SHIFT,
boot_pages<<PAGE_SHIFT,BOOTMEM_DEFAULT);
〃標示位圖所占的頁面被占用
if(node==0)
reserve_node_zero(pgdat);
#ifdefCONFIG_BLK_DEV」NITRD
/*
*Iftheinitrdisinthisnode,reserveitsmemory.
*/
if(node==initrd_node){
intres=reserve_bootmem_node(pgdat,phys_initrd_start,
phys_initrd_size,BOOTMEM_EXCLUSIVE);
//INITRD映像占用的空間需要標示占用,INITRD是虛擬根文件系統(tǒng),此時還未
加載,因此掛載之前這個物理空間不能再被分配使用
if(res==0){
initrd_start=_phys_to_virt(phys_initrd_start);
initrd_end=initrd_start+phys_initrd_size;
}else{
printk(KERN_ERR
"INITRD:0x%08lx+0x%08lxoverlapsin-use"
"memoryregion-disablinginitrd\n",
phys_initrd_start,phys_initrd_size);
)
#endif
*initialisethezoneswithinthisnode.
7
memset(zone_size,0,sizeof(zone_size));
memset(zhole_size,0,sizeof(zhole_size));
/*
*Thesizeofthisnodehasalreadybeendetermined.Ifweneed
*todoanythingfancywiththeallocationofthismemorytothe
*zones,nowisthetimetodoit.
*/
zone_size[0]=end_pfn-start_pfn;
zhole_size[0]=zone_size[0];
for_each_nodebank(i,mi,node)
zhole_size[0]-=mi->bank[i].size>>PAGE_SHIFT;
〃計算共有多少頁空洞,注意,有些bank的起始結(jié)束地址并不是剛好4K對齊的,
因此,可能存在某些空白頁框。用節(jié)點總的物理頁框減去每個bank頁框,就得
到頁空洞
〃這個函數(shù)里面主要完成zone區(qū)的初始化,linux內(nèi)存管理將內(nèi)存節(jié)點又分為
ZONE區(qū)管理,比如ZONE_DMA和ZONE_NORMAL等,因此需要初始化。由
于平臺只針對一致性內(nèi)存管理,即物理內(nèi)存空間只包含DDR部分,此處很多函
數(shù)是空的,再次略過
arch_adjust_zones(node,zone_size,zhole_size);
free_area_init_node(node,zone_size,start_pfn,zhole_size);
returnend_pfn;
)
〃在page」nit的最后完成devicemaps_init初始化,比如中斷向量的映射。映
射的大致過程是,申請一個物理框,然后調(diào)用creat_map將此物理頁框映射到
OxffffOOOO.最后再調(diào)用structmachine_desc的map_io完成10設(shè)備的映射
〃在完成內(nèi)存頁映射后即進入request_standard_resources,這個函數(shù)比較簡
單,主要完成從iomem,esource空間申請所需的內(nèi)存資源,比如內(nèi)核代碼和視
頻所需的資源等
request_standard_resources(&meminfo,mdesc);
#ifdefCONFIGSMP
smp_init_cpus();
#endif
cpu_init();//止匕函數(shù)為空
init_arch_irq=mdesc->init_irq;〃初始化與硬件體系相關(guān)的指針
system_timer=mdesc->timer;
init_machine=mdesc->init_machine;
#ifdefCONFIG_VT
#ifdefined(CONFIG_VGA_CONSOLE)
conswitchp=&vga_con;
#elifdefined(CONFIG_DUMMY_CONSOLE)
conswitchp=&dummy_con;
#endif
#endif
early_trap_init();〃重定位中斷向量,將中斷向量代碼拷貝到中斷向量頁,并把信
號處理代碼指令拷貝到向量頁中
)
mm_init_owner(&init_mm,&init_task);//空函數(shù)
setup_command」ine(command_line);〃保存命令行,以備后用,此保存空間
需申請
〃這個函數(shù)調(diào)用完了,就開始執(zhí)行下面初始化函數(shù)
unwind_setupO;〃空函數(shù)
setup_per_cpu_areas();〃設(shè)置每個CPU信息,單核CPU為空函數(shù)
setup_nr_cpu_ids();〃空函數(shù)
smp_prepare_boot_cpu();〃設(shè)置啟動的CPU為在線狀態(tài).在多CPU架構(gòu)下
〃第一個啟動的cpu啟動到一定階段后,開始啟動其它的cpu,它會為每個后來
啟動的cpu創(chuàng)建一個0號進程,而這些0號進程的堆棧的thread_info結(jié)構(gòu)中的
cpu成員變量則依次被分配出來(利用alloc_cpu_id()函數(shù))并設(shè)置好,這樣當
這些cpu開始運行的時候就有了自己的邏輯cpu號。
sched_init();〃初始化調(diào)度器,對調(diào)度機制進行初始化,對每個CPU的運行隊列
preempt_disable();〃啟動階段系統(tǒng)比較脆弱,禁止進程調(diào)度
build_all_zonelists();//建立內(nèi)存區(qū)域鏈表
page_alloc_init();〃內(nèi)存頁初始化,此處無執(zhí)行
printk(KERN_NOTICE"Kernelcommandline:%s\n",boot_command_line);
parse_early_param();
parse_args("Bootingkernel",static_command_line,_start_param,
_stop_param-_start_param
溫馨提示
- 1. 本站所有資源如無特殊說明,都需要本地電腦安裝OFFICE2007和PDF閱讀器。圖紙軟件為CAD,CAXA,PROE,UG,SolidWorks等.壓縮文件請下載最新的WinRAR軟件解壓。
- 2. 本站的文檔不包含任何第三方提供的附件圖紙等,如果需要附件,請聯(lián)系上傳者。文件的所有權(quán)益歸上傳用戶所有。
- 3. 本站RAR壓縮包中若帶圖紙,網(wǎng)頁內(nèi)容里面會有圖紙預(yù)覽,若沒有圖紙預(yù)覽就沒有圖紙。
- 4. 未經(jīng)權(quán)益所有人同意不得將文件中的內(nèi)容挪作商業(yè)或盈利用途。
- 5. 人人文庫網(wǎng)僅提供信息存儲空間,僅對用戶上傳內(nèi)容的表現(xiàn)方式做保護處理,對用戶上傳分享的文檔內(nèi)容本身不做任何修改或編輯,并不能對任何下載內(nèi)容負責。
- 6. 下載文件中如有侵權(quán)或不適當內(nèi)容,請與我們聯(lián)系,我們立即糾正。
- 7. 本站不保證下載資源的準確性、安全性和完整性, 同時也不承擔用戶因使用這些下載資源對自己和他人造成任何形式的傷害或損失。
最新文檔
- 游戲活動教案模板
- 2024年深海探測技術(shù)項目信托資金借款合同3篇
- 一年級語文園地五教案
- 2025年直流電源項目提案報告模稿
- 公文報告的范文
- 財務(wù)經(jīng)理述職報告
- 繪畫工作總結(jié)
- 結(jié)構(gòu)工程師工作總結(jié)(12篇)
- 學(xué)生會辭職報告(集合15篇)
- 簡短的求職自我介紹-
- 2025年上半年河南省西峽縣部分事業(yè)單位招考易考易錯模擬試題(共500題)試卷后附參考答案-1
- 深交所創(chuàng)業(yè)板注冊制發(fā)行上市審核動態(tài)(2020-2022)
- 手術(shù)室護理組長競聘
- 電力系統(tǒng)繼電保護試題以及答案(二)
- 小學(xué)生防打架斗毆安全教育
- 網(wǎng)絡(luò)運營代銷合同范例
- 2024年全國統(tǒng)一高考英語試卷(新課標Ⅰ卷)含答案
- 學(xué)生請假外出審批表
- 疼痛診療與康復(fù)
- T∕ACSC 01-2022 輔助生殖醫(yī)學(xué)中心建設(shè)標準(高清最新版)
- 新版【處置卡圖集】施工類各崗位應(yīng)急處置卡(20頁)
評論
0/150
提交評論