Chip123 科技應用創新平台

 找回密碼
 申請會員

QQ登錄

只需一步,快速開始

Login

用FB帳號登入

搜索
1 2 3 4
查看: 7360|回復: 17
打印 上一主題 下一主題

trace linux kernel source - ARM - 03

[複製鏈接]
跳轉到指定樓層
1#
發表於 2008-10-7 14:00:18 | 只看該作者 回帖獎勵 |正序瀏覽 |閱讀模式
到目前為止,我們已經進展到kernel幫自己relocate完,並解將自己解壓縮到一個地方要準備開始執行,那疑問來了?到底是跳到哪裡去了,因為./compressed/head.S最後一行居然是
& g! i6 }" q6 ?『mov pc, r4』
5 d$ I( o0 [& g# H* d( A4 fr4只代表了解壓縮完後kernel的位址,那究竟整包kernel編譯的時候,哪個function哪個東西被放在最前面咧?!
2 U, ]+ D+ b& ]4 I2 Q
$ M1 |( C2 u$ {  I所以我們又必須開始找於kernel link的時候是怎麼被安排的,有了前面的基礎,我們可以從Makefile知道程式碼如何被編譯。至於link上的細節,例如有那些section和section先後順序等等,可以從 lds 檔來規範。
0 x& y( {/ e( t5 C: X- Q
1 \' D" \2 ~" O8 z+ U有興趣的人可以看一下 kernel source 根目錄裡頭的 Makefile,Makefile file裡面指定了使用vmlinux.lds來當做lds檔。
  1. 659 vmlinux-lds  := arch/$(SRCARCH)/kernel/vmlinux.lds
複製代碼
打開./arch/arm/kernel/vmlinux.lds.S (會用來產生vmlinux.lds)
/ L8 E9 `7 r3 L# S) P$ m8 C我們可以發現第一個section是『.text.head』,裡頭的_stext從目前的位置開始放。0 n7 L2 h: _& S
於是我們曉得只要找到屬於.text.head這個section,並且是_stext這個symbol的程式碼,就是解壓縮完後的第一行程式碼。
  1.      26     .text.head : {% I6 e2 P+ L7 M+ R
  2.      27         _stext = .;- ?; l. ~1 O" o& v" b/ E$ X4 B2 T/ d
  3.      28         _sinittext = .;2 s  x. D3 T, I* l) i
  4.      29         *(.text.head). N, @7 O5 [* ~2 \2 c- U
  5.      30     }
複製代碼
用指令搜尋一下,發現 ./arch/arm/kernel/head.S 裡頭有關鍵字(如下),結果我們從./arch/arm/boot/compressed/head.S跳到了./arch/arm/kernel/head.S
  1.      77     .section ".text.head", "ax"+ V% M. ?9 U. l% H; @. L& w
  2.      78     .type   stext, %function6 ^" \! ^7 h/ ]/ ?8 n2 |
  3.      79 ENTRY(stext)/ J. e1 H: c* z* j" g1 y
  4.      80     msr cpsr_c, #PSR_F_BIT | PSR_I_BIT | SVC_MODE @ ensure svc mode. F- F* V0 M0 r/ t- C) v
  5.      81                         @ and irqs disabled
    # {! W- {' k# y. o
  6.      82     mrc p15, 0, r9, c0, c0      @ get processor id; _9 K9 X0 H) D9 e8 _- U. F" A
  7.      83     bl  __lookup_processor_type     @ r5=procinfo r9=cpuid
    - e' _7 R) Q* y4 n+ e% B
  8.      84     movs    r10, r5             @ invalid processor (r5=0)?
    8 i8 C6 _8 k! q! \
  9.      85     beq __error_p           @ yes, error 'p'' f$ v2 x+ f$ d4 H
  10.      86     bl  __lookup_machine_type       @ r5=machinfo- z# X1 P  G0 m  v/ C* ~
  11.      87     movs    r8, r5              @ invalid machine (r5=0)?2 Z4 d$ a$ r; m8 s
  12.      88     beq __error_a           @ yes, error 'a'
    1 v, O' y2 q$ L3 L) `, J
  13.      89     bl  __vet_atags+ S/ f; V1 S3 r, X) a& N* {
  14.      90     bl  __create_page_tables
複製代碼
既然找到了檔案,我們又可以開始繼續。
) k6 ~" r3 }7 m7 f% u
3 W& D- b$ i! ]2 C% F看了一下,程式碼多了很多bl的跳躍動作,看來會在各個function跳來跳去。
分享到:  QQ好友和群QQ好友和群 QQ空間QQ空間 騰訊微博騰訊微博 騰訊朋友騰訊朋友
收藏收藏 分享分享 頂 踩 分享分享
18#
 樓主| 發表於 2009-7-15 17:30:00 | 只看該作者
  1. 39 __mmap_switched:
    . h& ]; H' i" I. P
  2. 40         adr     r3, __switch_data + 4% g  S% i$ i# E$ u5 j. @
  3. 41
    2 Z( V. y2 N, P7 ]
  4. 42         ldmia   r3!, {r4, r5, r6, r7}8 Z  B7 B+ f1 E, S9 b- H  p5 o/ d
  5. 43         cmp     r4, r5                          @ Copy data segment if needed
    ( f8 `; P4 {! S2 U. U6 F
  6. 44 1:      cmpne   r5, r63 R; P; t/ |1 _
  7. 45         ldrne   fp, [r4], #4
    ' r" U! {$ R7 C5 p% j2 I. T, e
  8. 46         strne   fp, [r5], #4
    $ ?: v+ u% C$ m; u8 h) v
  9. 47         bne     1b& h( j2 o8 ]$ a
  10. 484 U0 C" Q% F' c2 t7 A. \( k* b
  11. 49         mov     fp, #0                          @ Clear BSS (and zero fp)5 M0 o( J* [1 z! q2 @) K0 W
  12. 50 1:      cmp     r6, r7
    # a3 [; B6 a9 L4 x9 ?# I
  13. 51         strcc   fp, [r6],#43 Z% a+ r( G+ h, ?& M5 x
  14. 52         bcc     1b3 {1 O" B# P, K
  15. 53
    ; ?  }3 \# {* z4 b0 u
  16. 54         ldmia   r3, {r4, r5, r6, r7, sp}
    0 K2 P/ g- P! S& k
  17. 55         str     r9, [r4]                        @ Save processor ID
    5 P1 G7 C* i2 i! G1 |: f# ?  z
  18. 56         str     r1, [r5]                        @ Save machine type( b0 q$ B9 n3 x$ S* v/ p
  19. 57         str     r2, [r6]                        @ Save atags pointer
    4 p$ W; s6 j2 l3 L$ h
  20. 58         bic     r4, r0, #CR_A                   @ Clear 'A' bit% d1 ~) y. a; w( B5 @
  21. 59         stmia   r7, {r0, r4}                    @ Save control register values
    ; t4 }7 Y) c3 F4 O; a
  22. 60         b       start_kernel
    * [# I8 _% K! n, Q) U6 z! I
  23. 61 ENDPROC(__mmap_switched)
複製代碼
switch_data的第一行就是 __mmap_switched,所以我們直接看line 39。: F. L! u: j# p9 N" q
line 39,將__data_loc的addr放到r33 x3 M/ E6 ^5 C4 \9 y
line 42,從r3的位址,連續讀取四筆資料到r4, r5, r6, r7
. P9 u! |/ d' e" A" Z! O8 ?line 43~47,看看data segment是不是需要搬動。
& R) m+ m9 g; @5 w% Pline 49~52, clear BSS。+ _7 d! }! p/ w- q! S- Z  j8 {7 f

# L0 ?: N) I" E% H由於linux kernel在進入start_kernel前有一些前提必須要滿足:
5 m1 c8 O3 k; H# e9 D  [3 sr0  = cp#15 control register
+ }/ D7 S% W8 _! f8 l8 w# rr1  = machine ID) C$ j/ i5 M' y- \+ ]2 P
r2  = atags pointer
% r! U* [/ Z3 ]2 b) kr9  = processor ID
: s( u! w; R, h
0 R# ?; D( @' |& ]5 r: X; @* E* g所以line 54~59就是在做這些準備。6 I4 b; `. H; w# x$ i$ O" B2 |# l2 K
最後呢? 我們就跳到start_kernel了。(而且還是用b start_kernel,表示我們不會再回來了)" ^, W) J  S' f7 N0 G2 H/ T
( y, B1 _$ L* L8 e4 i& x4 q
看一下start_kernel()在./init/main.c,終於跳出architecture specific的目錄,表示
' M7 f" k5 C1 v* T3 z8 z我們真正的開始linux kernel的初始化。
. w! T% T  d' W* R: F6 I3 w; B像是 shedule init, console init, memory init, irq init等等都在start_kernel裡頭。
, L; a1 X4 j& X6 v: h. q+ c8 j到這邊之後,應該就可以深入linux kernel中,各項比較跟hardware不那麼相關的軟體部分。

評分

參與人數 1 +8 收起 理由
card_4_girt + 8 感謝經驗分享,希望你再接再厲!

查看全部評分

17#
 樓主| 發表於 2009-7-15 17:29:45 | 只看該作者
  1. 155 __enable_mmu:. V# m: {( a" f/ l% C9 u5 q
  2. 170         mov     r5, #(domain_val(DOMAIN_USER, DOMAIN_MANAGER) | \1 r; q) Q  g; Z- O: Z' t
  3. 171                       domain_val(DOMAIN_KERNEL, DOMAIN_MANAGER) | \& h; `0 g9 J1 R  k; Y
  4. 172                       domain_val(DOMAIN_TABLE, DOMAIN_MANAGER) | \
    ! g/ ^" `9 t! m" ~: C  P2 _
  5. 173                       domain_val(DOMAIN_IO, DOMAIN_CLIENT))
    " M2 f' U5 ?2 Y4 P& {$ D
  6. 174         mcr     p15, 0, r5, c3, c0, 0           @ load domain access register
    5 N/ m# q; f8 k) R
  7. 175         mcr     p15, 0, r4, c2, c0, 0           @ load page table pointer$ y: z* F  P+ y1 Q: _- J. O- d
  8. 176         b       __turn_mmu_on" I7 N$ O7 X5 R  K! D% ^! u9 L
  9. 177 ENDPROC(__enable_mmu)
複製代碼
line 170~174,設置好domain access。(domain access可以先當成設置access的權限,或許以後可以寫詳細的文章說明)
- f: r4 n' R2 P6 x  X, t- Eline 175~176,將create好的page table開頭丟給mmu。跳到turn_mmu_on
  1. 191 __turn_mmu_on:
    ; G& e: F4 ?. c% }: X
  2. 192         mov     r0, r0
      L  s5 ~* w4 x+ F
  3. 193         mcr     p15, 0, r0, c1, c0, 0           @ write control reg- E/ t" \. Q& a& e, W: \/ \0 N2 w
  4. 194         mrc     p15, 0, r3, c0, c0, 0           @ read id reg0 i' g) E9 |# `! ~
  5. 195         mov     r3, r3
    . S8 U7 x- ~0 z% \& g& @4 u9 P
  6. 196         mov     r3, r37 W- k0 a# t7 ]8 a
  7. 197         mov     pc, r13
    9 I& M/ e) q+ D3 L3 ~2 a$ B
  8. 198 ENDPROC(__turn_mmu_on)
複製代碼
顧名思義就是把mmu打開,將我們準備好的r0設定交給mmu,並讀取id到r3,接著pc跳到r13,r13剛剛在head.S已經先擺好__switch_data。所以會跳到head-common.S。
  1. 18 __switch_data:
    - q& v$ J$ b+ d% ~- V3 ~+ q
  2. 19         .long   __mmap_switched( D- m) c: N6 K" A7 J  Z
  3. 20         .long   __data_loc                      @ r4
    8 B! J% f6 n# p: c2 q
  4. 21         .long   _data                           @ r5
    ) S, I/ R# X- u
  5. 22         .long   __bss_start                     @ r6& A2 H% R2 q4 O; Z" d. j* Z
  6. 23         .long   _end                            @ r7( s6 {' d  C1 v( L
  7. 24         .long   processor_id                    @ r4& l' l$ h0 c5 P& M; I9 c9 a/ R
  8. 25         .long   __machine_arch_type             @ r5: Q7 O" A" t' S
  9. 26         .long   __atags_pointer                 @ r67 I& U5 c/ w5 v4 H
  10. 27         .long   cr_alignment                    @ r7
    4 t) h: c# y- Y& l; o( P& s. s
  11. 28         .long   init_thread_union + THREAD_START_SP @ sp* }' G/ U: |+ q9 I5 d9 I' q0 t
  12. 29
    * l! h4 A0 o1 b  ~  i
複製代碼
switch_data的第一行就是 __mmap_switched,所以我們直接看line 39。
16#
 樓主| 發表於 2009-7-15 17:07:03 | 只看該作者
隔了很長一段時間沒update6 C0 I) @2 q! h: P* N. w+ y
之前程式碼走到 ./arch/arm/kernel/head.S 的 line 99 附近
  1.      99         ldr     r13, __switch_data              @ address to jump to after
    $ ]: P: R0 V$ c% D4 S, F
  2.     100                                                 @ mmu has been enabled  q  U8 W5 Q8 _. u7 i+ k
  3.     101         adr     lr, __enable_mmu                @ return (PIC) address5 Q  T- V- i5 |4 C+ a
  4.     102         add     pc, r10, #PROCINFO_INITFUNC
複製代碼
line 99, 將__switch_data放到r13。(留作之後用)
' G; K: g& g9 Xline 101, 將__enable_mmu的addr放到lr。(留作之後用)' `6 O- W6 |8 ^9 }+ s& U/ }+ d
line 102, 將 r10+#PROCINFO_INITFUNC 放到pc,也就是jump過去的意思。r10是proc_info的位址。PROCINFO_INITFUNC則是用之前提過的技巧,指向定義在./arch/arm/mm/proc-xxx.S的資料結構,以arm926為例,最後會指到
  1. 463         b       __arm926_setup
複製代碼
所以程式碼就跳到了 __arm926_setup。
  1. 373         .type   __arm926_setup, #function$ _5 _# j" n: k- m
  2. 374 __arm926_setup:: _+ L/ U6 J# E) r! U8 b2 W% t
  3. 375         mov     r0, #0
      ^$ p2 V, u! J; v8 C
  4. 376         mcr     p15, 0, r0, c7, c7              @ invalidate I,D caches on v4
    8 ~$ U  ]. I4 D5 F; `' W
  5. 377         mcr     p15, 0, r0, c7, c10, 4          @ drain write buffer on v4
    / ?7 ]* R3 I4 F1 r- E/ @
  6. 378 #ifdef CONFIG_MMU
    + A! `/ l. `  X& T
  7. 379         mcr     p15, 0, r0, c8, c7              @ invalidate I,D TLBs on v4
    & o& p2 M# N3 Q* ?) K! Q
  8. 380 #endif
    - u7 r5 j( w: B2 h( O1 z6 G
  9. : P9 X1 |% n. ]9 S$ N
  10. 388         adr     r5, arm926_crval5 n9 p) _# t6 H0 K/ i+ u' `( w' N2 z
  11. 389         ldmia   r5, {r5, r6}0 f: w; s# e5 c( t6 F% Q
  12. 390         mrc     p15, 0, r0, c1, c0              @ get control register v4
    ! I- G0 h& ^2 |; @
  13. 391         bic     r0, r0, r50 ]$ P* C: J* z8 |% T# ]& m
  14. 392         orr     r0, r0, r6
    ' a' M6 p, u, t! i8 L

  15. 0 E$ _# e* R) t
  16. 396         mov     pc, lr; `% N; b3 \( S
  17. 397         .size   __arm926_setup, . - __arm926_setup
複製代碼
這邊的程式碼就跟CPU有很大的相依性,
8 m* h0 o6 b. H/ E% Tline 375~380, 主要就是invalidate CPU的I&D cache和清空write buffer。: _- k/ a0 h+ H4 P( Y$ W# T4 D* g+ w
line 388~392, 把cp15的設定讀出來,並且將一些預設狀態做好運算放到r0。(預設值從arm926_crval這邊可以取得。)
4 ]% f( c4 n9 s; m- q0 R4 wline 396, 直接把pc跳到lr,因為我們之前已經將lr = enable_mmu,所以直接跳過去。
15#
 樓主| 發表於 2009-7-4 01:09:36 | 只看該作者
老店重新開張~
: c% K% x) x/ ^, Y) L0 i( b2 k2 z9 `
花了一些時間把舊的貼文整理到一個blog
8 k" ^" R( ^# h有把一些敘述修改過
. r- n; @% y* k7 N* y2 U5 s希望會比較容易集中閱讀
# `' j: s6 N- h+ z5 j$ H6 y目前因為某些敘述不容易
) ?2 V) A& f' L' n# ?$ a9 i# ~, \* g- Q還是比較偏向筆記式而且用字不夠精確% {; q6 ^5 }8 O6 u
希望之後能夠慢慢有系統地整理
$ t6 ?9 R8 D# t7 W: }大家有興趣的話
9 W4 k* m  S( }8 ~, L9 U可以來看看和討論 + Q  B* ^- E! r5 j$ f' v' X' k
http://gogojesseco.blogspot.com/
" x1 D7 j+ `1 m7 L) ^; N
6 G) z+ B+ ~6 b) q: I以後可能會採取  先在chip123貼新文章
, E2 B* u/ F9 _4 |; X& y9 i慢慢整理到blog上的方式
, N. q1 g( X: U- L. E+ j因為chip123比較方便討論 =)" L* J* [, @" a7 j) L/ H' Z
blog編輯修改起來比較方便
$ K) e5 g8 P* G) ~閱讀也比較集中   大家可以在這邊看到討論
5 V2 {* O9 z& t, z( X然後在blog看到完整的文章 (類似BBS精華區的感覺)

評分

參與人數 1Chipcoin +5 +3 收起 理由
jacky002 + 5 + 3 感謝經驗分享!

查看全部評分

14#
 樓主| 發表於 2008-10-22 20:37:08 | 只看該作者
自create page table返回後,我們偷偷看一下接下來的程式碼,
8 I5 _4 @( @, k: Z4 m$ `$ ^9 U: D5 M6 Dline 99, 將switch_data擺到r13$ b: E% ~, r. k, M. n- m# h. Q5 K
line 101, 將enable_mmu擺到lr
6 u2 a/ A3 ^/ C* ^6 w! _) vline 102, 將pc改跳到r10+PROCINFO_INITFUNC的地方去
9 W$ c0 i6 |: L1 k
* d+ l" n4 q  w- c  V/ @8 @% ~其實這邊有點玄機,switch_data和enable_mmu都是function,結果位址都只是被存起來,並沒有直接跳過去執行。其實,雖然程式碼是順序switch_data->enable_mmu->proc init function,但其實執行的順序會是 procinfo 的init function-> enable_mmu -> switch_data 。至於,為什麼要這樣寫的原因?就不清楚了,也還沒仔細推敲過。
/ r# x, j9 M0 F- S5 e  w$ Y1 {2 o) T) ]) q4 f4 M
switch_data最後就會跳到大家都很熟悉的start_kernel(). 詳細要賣個關子,最近會忙一下,得要過一陣子才能貼文∼  
  1.      99         ldr     r13, __switch_data              @ address to jump to after. q, K8 z' y$ l6 F) n' [
  2.     100                                                 @ mmu has been enabled( o  U0 k) R5 i# k, H* z: v
  3.     101         adr     lr, __enable_mmu                @ return (PIC) address+ @5 s: [; d! m1 {  U
  4.     102         add     pc, r10, #PROCINFO_INITFUNC
複製代碼
13#
 樓主| 發表於 2008-10-22 20:24:58 | 只看該作者
line279,PAGE_OFFSET是規範RAM mapping完後的virtual address,我們將這個位址所屬的pte算出來放到r0。* u+ @6 T& {- a4 G+ A
line 280~283,將要 map 的physical address的方式算出來放到r6。) Q$ C: z4 q! X; Z1 F, m
line 284,最後將結果存到r0所指到的pte。2 Y. \( k; v& F$ o
/ w) O- Y- m" B- W9 p7 `$ K' `
以上三個動作,就是做好 ram 起頭的位址一開始map,還沒做完整塊map。由於我們目前的設定方式每塊是1MB,所以這邊是將RAM開始的1MB做map。
3 B; T/ v+ v7 L: X3 w
+ u- \" G( C% _line 327,返回,結束一開始的create page table的動作,我們可以看出其實並沒有做完整的初始化page table,所以之後應該會有其他page table的細節。
  1.     279         add     r0, r4, #PAGE_OFFSET >> 18
    & a$ y# U( S/ r$ {
  2.     280         orr     r6, r7, #(PHYS_OFFSET & 0xff000000)9 P+ A; P7 L3 l# n
  3.     281         .if     (PHYS_OFFSET & 0x00f00000)
    * E5 x- z* P  J" q; \
  4.     282         orr     r6, r6, #(PHYS_OFFSET & 0x00f00000)
    1 C0 I4 n5 v, o4 m
  5.     283         .endif/ v  p( I# P' E" c) c
  6.     284         str     r6, [r0]  q0 d# u. _! [/ n
  7.     327         mov     pc, lr
複製代碼
附帶一提,我們這邊省略了一些用ifdef包起來的程式碼,像是一開始會印一些output message的程式碼(line286~326)。
12#
 樓主| 發表於 2008-10-22 19:47:03 | 只看該作者
最近又被釘上了,開始忙碌,大概沒太多時間貼文∼
1 D. K) K! E) p( i( b5 Z  m: S% I) E; `2 C3 k7 u
上篇已經將pc所屬的page table entry(pte)設定好,接著繼續看7 b. n5 U* h9 Y
line 247, 248, 將KERNEL_START的位址往右shift 18算出pte的offset,(等於line239&241,239&241是先shift right 20然後shift left 2),並將剛剛r3的值設給pte。『!』的意思是會把address存到r0。# `6 u" j% R+ Z
* q0 i2 {  _  c; C4 u
line 249~252, 算出KERNEL_END-1的pte位址放到r6, KERNEL_START的下一個pte的位址放到r0。r0 <= r6的話就持續對pte寫入初值的動作。但是這邊的r3有加上(0x1<<20),所以原本的section base會變成加1,目前不是很明瞭為什麼要加1,或許往後面會找到答案。
  1.     247         add     r0, r4,  #(KERNEL_START & 0xff000000) >> 18
    % j8 a2 v- u9 e3 m" R
  2.     248         str     r3, [r0, #(KERNEL_START & 0x00f00000) >> 18]!   B4 D3 Y7 k4 A3 L
  3.     249         ldr     r6, =(KERNEL_END - 1)
    4 }3 ^4 w- A5 O% i" Q/ j' l8 |
  4.     250         add     r0, r0, #45 i* @$ O2 Y; B4 C$ [/ R) E. Q
  5.     251         add     r6, r4, r6, lsr #18
    * R9 N  R" w& i+ H
  6.     252 1:      cmp     r0, r6& U5 S. j/ _* e- ]3 {* O
  7.     253         add     r3, r3, #1 << 20: K! U5 L" _% y- N8 [. \  t
  8.     254         strls   r3, [r0], #4
    " t  l6 m' O) b) |- I- U7 d+ _
  9.     255         bls     1b
複製代碼
11#
 樓主| 發表於 2008-10-14 15:11:48 | 只看該作者
問題怎麼填值??
" J: K1 j. J6 ~# ^; u拿出ARM的手冊,翻到MMU章節。一看發現page有很多種,還得分first level和second level。 + m: |+ q# K' `& l0 R& {% B! z
2 t/ w) ~$ }" o0 z
念書時的印象,是從1st level在去查2nd level,先看怎麼設定1st level (不知以前老師有沒有亂教)
( s9 ?) x4 S+ U; J. g# [) U6 A* B1. [31:20]存著section base addr
+ R5 E% a6 f* L' e, V# Q+ q6 E2. [19:2]存著mmu flags
) @2 `/ }* |8 u- N3. [1:0]用來辨別這是存放哪種page, 有四種:
8 E* Z; ?; X( c% N' `$ W   a. fault (00) b. coarse page (01) c. section (1st level) (10) d. fine page (11)
1 a' E; X/ J  h# q0 G4. pg tabel資料要存放到 [31:14] = translation base, [13:2] = table index , [1:0] = 0b00 的位址0 G) m: I' p3 O* x

& J7 t6 N" `4 O2 l來看code是怎麼設定。. a1 v. Q# b1 `3 g/ }
" n! X/ l8 m6 O0 h- F) ?# ?
line 239, 將pc的值往右shift 20次放到r6.這是取得前20高位元的資料,有點像是 1.。3 S2 T$ l: p: j" z4 ~+ f. K
line 240, 將r6往左shift 20次之後,和r7做or的動作,剛好也是 2.提到的mmu flags和1.處理好的資料做整理。
5 d: u3 h, R: i& n7 I* g0 \所以前面兩個做完,就完成了bit[31:2]。2 p& A! M! M* E* C3 b+ L
line 241, 將r3的資料寫到r4+(r6<<0x2)的地方去,剛好是4.提到的位址。(lucky)
  1.     239         mov     r6, pc, lsr #20
    0 }4 X6 c4 S2 C' o3 ^* B
  2.     240         orr     r3, r7, r6, lsl #20/ ^) x4 D: j7 V+ Q
  3.     241         str     r3, [r4, r6, lsl #2]
複製代碼
p.s. 用pc值剛好可以算出當前的page。
10#
 樓主| 發表於 2008-10-14 14:16:53 | 只看該作者
得到pg table的開始位置之後,當然就是初始化囉。! ]% e1 K$ Z  b, ?
line 221, 將pg table的base addr放到r0.
$ o, C# @% y6 D, j& _$ [line 223, 將pg table的end addr放到r6., T# `7 C, K4 `
line 224~228, 反覆地將0x0寫到pg table的區段裡頭,每個loop寫16bytes. (4x4),直到碰到end,結果就是把它全部clear成0x0.
  1.     221         mov     r0, r4# E+ x4 I% ^6 Q( ~& i
  2.     222         mov     r3, #0
    & R6 ^: |' m; n, s$ T
  3.     223         add     r6, r0, #0x4000
    " N0 p+ W3 y  N
  4.     224 1:      str     r3, [r0], #4
    1 x$ n( l/ C( ~6 E1 T  Z
  5.     225         str     r3, [r0], #43 V! q+ ]0 n3 @& S1 O
  6.     226         str     r3, [r0], #40 |3 \4 @0 f2 T7 k2 X2 h( Y
  7.     227         str     r3, [r0], #4
    # o' f4 S3 f/ G+ O' x
  8.     228         teq     r0, r6
    , V& m* V( |: Q! |7 ]
  9.     229         bne     1b
複製代碼
line 231, 將位址等於 r10+PROCINFO_MM_MMUFLAGS 裡頭的值放到r7。r10是proc_info的位址。proc的info data structure被定義在『./include/asm-arm/procinfo.h』,offset取得的方式用compiler的功能,以便以後新增structure的欄位的時候不需要更動程式碼。這邊的動作合起來就是讀預設要設給mmu flags的值。
  1.    231         ldr     r7, [r10, #PROCINFO_MM_MMUFLAGS] @ mm_mmuflags
複製代碼
9#
 樓主| 發表於 2008-10-14 13:57:39 | 只看該作者
現在,讓我們跳入create_page_tables吧∼
* r! n# W6 B  `3 G4 Uline 216,會跳到pgtbl的macro去執行,其實只是載入一個位址。
) a& u: p2 \+ f1 l. w) P; }2 l; M- W& w
只是這個位址因為你硬體規劃dram位置不同,所以必須可以變動。一般會定義在./include/asm-arm/arch-你的平台/memory.h,我們看得出來dram開始的地方是從0x8000 offset(text_offset)開始算,猜測可能一開始有保留空間給kernel使用。實際算page table的時候有減去0x4000,表示是從DRAM+0x8000-0x4000開始放pg table.
  1. /* arch/arm/Makefile */
    ; u) d+ t* Z, ], v" H  E
  2.      95 textofs-y       := 0x000080002 k& f3 p1 D% `5 G9 i) B
  3.     152 TEXT_OFFSET := $(textofs-y)
複製代碼
  1. /* include/asm-arm/arch-omap/memory.h */6 W% e, c. x1 h7 z! o
  2.      40 #define PHYS_OFFSET             UL(0x10000000)
    ! d7 {4 v6 N' w
  3. & ^4 n7 [4 G" w+ W. {
  4.      /* arch/arm/kernel/head.S */# C$ W/ j; f5 S2 d. V0 ~( |
  5.      29 #define KERNEL_RAM_VADDR        (PAGE_OFFSET + TEXT_OFFSET)
    & c/ ]" N' @. C. [
  6.      30 #define KERNEL_RAM_PADDR        (PHYS_OFFSET + TEXT_OFFSET)
    ' `5 a  K4 A% b

  7. & T1 W; m8 D7 j; X
  8.      47         .macro  pgtbl, rd
    ; k% b) v+ y6 G; Y
  9.      48         ldr     \rd, =(KERNEL_RAM_PADDR - 0x4000)
    % `7 X7 d9 m" ^% W3 e0 ^
  10.      49         .endm- w/ g; p- C6 W: X7 L/ e
  11. : A1 Q4 S( E" q0 x
  12.     216         pgtbl   r4                              @ page table address
複製代碼
8#
 樓主| 發表於 2008-10-14 13:56:17 | 只看該作者
由於字數限制挑整,用詞儘量精簡,以便可以貼必要source。另外,以後寫到一段落,考慮收集成一篇完整的文章,這樣應就不會因為用回覆的方式,造成閱讀斷斷續續的問題。希望有人對文章呈現方式有想法的話,可以跟我講。
7#
 樓主| 發表於 2008-10-14 12:15:51 | 只看該作者
這時,寫driver的人就必須要小心,設定給硬體看的位址必須要先轉成pa(像是設定DMA),給CPU執行的程式碼要使用va,這對一開始嘗試寫driver但是又不瞭解va pa之間的差別的人,常常產生很多疑問。1 u% D: E* x& T! p

: o% d2 H1 k9 r" g現在我們回頭想想OS,既然我們跑到create page table,可以預期的是他想要將MMU打開,因此希望預先建立起一個page table,讓MMU知道目前os想規劃的位址對應和讀取權力是如何被安排的。所以os必須考慮到現在的系統究竟是長怎樣?應該要如何被安排?是不是有那些要被保護?好吧∼因為我們完全對os不了解,也不知道該安排什麼,只能祈禱在trace code完後,可以找到這些問題的答案,或者發現一些沒想到的問題。4 L* M& F/ I7 q) {8 R+ f6 ]

2 a% S8 ^9 c3 R+ W8 U& h+ O! Y# a知道了page table的大致上的功能,下篇就可以專心的研究這個table的長相,和它想規劃出的系統模樣。0 ~0 x2 _1 g7 }- Q& ^5 [1 s
. a2 {) m' I: Q# |2 Q
p.s. 字數限制好像變短了。   (看來很難寫)
6#
 樓主| 發表於 2008-10-14 12:14:47 | 只看該作者
page table本身是很抽象的東西,尤其是對所謂的 user-mode application programmer 來說,大部分的狀況它是不需要被考慮到的部份,但是他的產生卻對os和driver帶來了很多影響。我們從一個小小的疑問出發。# m% ]6 w* }) b6 `$ H
  A3 `) d; E0 [0 H7 d
『產生page table到底是要給誰用的?』
& X5 G$ D/ _# u9 |8 @# ^& C) J# ?/ b! Q" L! s2 e
其實真正作用在它上面的H/W是MMU,一旦CPU啟用了MMU,當cpu嘗試去記憶體讀取一個operand的時候,cpu內部打出去的位址訊號都會先送到mmu,mmu會拿著這個位址去對照page table,看看這個位址是不是被轉換到另外一個位置(還會確認讀寫權力),最後才會到真正的位址去讀寫。1 A2 N: [3 f! j$ e$ I8 o9 g3 o( f

  l$ m) b9 c- t這樣來看CPU其實一開始打出去的位址訊號其實不是最後可以拿到資料的位址,我們稱為virtual address(va),mmu打出來的位址才能真正拿到資料,所以我們把mmu打出去的位址稱為physical address(pa)。那用來查詢這個位址對照關係的表格,就是page table。
3 \7 q# M; i$ j" l
% u5 ~& Z$ A! g, Q到這邊我們有一個簡單的概念。來想像一個小問題,一個普通的周邊(例如你的顯示卡),因為他並不具有MMU功能,hw只看得懂pa,但是CPU卻是作用在va,假如我想去對hw的控制暫存器做讀寫,到底要用va還是pa?
5#
 樓主| 發表於 2008-10-14 12:13:51 | 只看該作者
由於code看起來似乎越來越難解釋,會需要在檔案之間跳來跳去。建議一些基礎的東西可以再多複習幾次(其實是在說我自己 ),閱讀source code上收穫會比較多。例如:
. i( e, n1 j1 x0 t7 v& a1 e( F$ v8 I/ S( O- ~7 J. X6 |
1. arm instruction set - 這個最好每個指令的意思都大略看過一次,行有餘力多看幾個版本,armv4, v5 or v6。' D4 s" i6 _& V
2. compiler & assembler & linker - toolchain工具做的事情和功能,大致的流程和功能要有概念。
# v; Z- c! A" v6 L; M3. Makefile & link script - 這兩個功能和撰寫的方式要有簡單的概念。
1 D. W- q6 s2 ~+ R; \+ N5 A* `0 b8 [: C: c. d* w8 y
以上1是非常重要的重點,2&3只要有大致上的概念就可以,因為trace code的時候,有時需要跳到Makeflie&Link script去看最後object code編排的位址。
2 ]- w# k: P; S
1 y# y- l! I* U, i1 L由於我們trace到了page table這個關鍵字,在開始之前,稍微簡短的解釋,為了幫助了解,儘量用易懂的概念講,有些用詞可能會和一些真實狀況不同,但是懂了之後,應該會有能力分辨,至於正式而學術上解說,很多書上應該都有,或是google一下就很多啦∼
4#
 樓主| 發表於 2008-10-13 17:56:46 | 只看該作者
接著我們又返回到head.S,
: k# D2 H; W, aline 87~88也是做check動作。
+ k6 v( C' o% b7 ~8 i$ w  Eline 89跳到vet_atags。在head-common.S
  1.      87         movs    r8, r5                          @ invalid machine (r5=0)?
    ) [% m/ h. V6 }, s4 k& {
  2.      88         beq     __error_a                       @ yes, error 'a'8 u/ W  _: g6 b
  3.      89         bl      __vet_atags
    ( _! `+ ?  T% e$ b
  4.      90         bl      __create_page_tables
複製代碼
line 245, tst會去做and動作。並且update flags。這邊的用意是判斷位址是不是aligned。& u8 R2 e5 E* K- X7 v* ^' u! y! l, ^
line 246, 沒有aligned跳到label 1,就返回了。0 b  E. r' X) `8 ^+ Y
line 248~250, 讀取atags所在的address裡頭的值到r5,看看是否不等於ATAG_CORE_SIZE,不等於的話也是返回。! i' G& M; W. ~3 A; O: _
line 251~254, 判斷一下第一個達到的atag pointer是不是等於ATAG_CORE。如果正確的話,等一下要讀取atag的資料,才會正確。
# |8 r, x& }7 u(atag是由bootloader帶給linux kernel的東西,用來告知booting所需要知道的參數。例如螢幕寬度,記憶體大小等等)
  1.      14 #define ATAG_CORE 0x54410001% s( M& p+ q- V9 R$ h# ~
  2.      15 #define ATAG_CORE_SIZE ((2*4 + 3*4) >> 2)( ?) ~9 @" R2 e5 p& A% R4 i& q3 Q  {
  3. & I( Z, C9 p: X
  4.     243         .type   __vet_atags, %function
    # f, J2 i! `9 m( o3 M4 N/ y4 {
  5.     244 __vet_atags:2 d4 W9 v4 K( w
  6.     245         tst     r2, #0x3                        @ aligned?' f$ ~7 T. c3 x8 W* o' @: C
  7.     246         bne     1f3 E, q3 h3 y' i7 t
  8.     247
    1 C0 j& E: D/ {  ^! m" I
  9.     248         ldr     r5, [r2, #0]                    @ is first tag ATAG_CORE?
    ( ]8 P0 |( ?+ @
  10.     249         subs    r5, r5, #ATAG_CORE_SIZE
    7 {- J6 U5 F  s) v
  11.     250         bne     1f2 f; i) m5 g3 [! ]+ K
  12.     251         ldr     r5, [r2, #4]
    . @1 C1 ~( M2 E5 A5 Z* U4 ~& z2 n3 O7 I
  13.     252         ldr     r6, =ATAG_CORE; t- I5 \& U0 T7 u& @0 I
  14.     253         cmp     r5, r6
    * B, n8 C' k& t1 \0 c5 [0 h, G
  15.     254         bne     1f
    9 C- d8 u* n! ^' i
  16.     255
    ) |7 a. e9 r0 R& u9 @& u
  17.     256         mov     pc, lr                          @ atag pointer is ok, o- n0 i8 ]* k" p, q
  18.     257- O6 k/ p/ Y3 S2 [
  19.     258 1:      mov     r2, #0, ]  s+ i% i: K% l
  20.     259         mov     pc, lr
複製代碼
接著我們又跳回去head.S。  
* o; {) H: S  i# B! r; ]/ kline 90,又跳到 __create_page_tables。   (很累人....應該會死不少腦細胞)6 x9 }3 O, v. c. }
哇!page table?!!如雷貫耳的東西,不知道會怎麼做。剛剛偷看了一下,@@還蠻長了,先到這邊好了,下次在繼續寫。
3#
 樓主| 發表於 2008-10-13 17:20:35 | 只看該作者
我們從 head-common.S返回之後,接著繼續看。
6 w$ U7 Q2 a. \
2 m7 x( W7 b/ U& e! n% z3 R2 k3 Aline 84, movs的意思是說,做mov的動作,並且將指令的S bit設起來,最後的結果也會update CPSR。這個指令執行的過程當中,會根據你要搬動的值去把N and Z flag設好。這個是有助於下個指令做check的動作。
7 k& Z  i8 [, A# p# qline 85, 就是r5 = 0的話,就跳到__error_p去執行。
% h( P; N$ ?5 a" \line 86, 跳到__lookup_machine_type。原理跟剛剛找proc的資料一樣。
  1.      83         bl      __lookup_processor_type         @ r5=procinfo r9=cpuid
    / W; M$ N0 n7 b' }$ r
  2.      84         movs    r10, r5                         @ invalid processor (r5=0)?4 r( k+ p8 H8 ^
  3.      85         beq     __error_p                       @ yes, error 'p'8 Q) t. U( l( s4 n7 a5 o. J
  4.      86         bl      __lookup_machine_type           @ r5=machinfo
複製代碼
看得出來跟proc很像,有個兩個小地方是1 T, E( W3 B0 c7 b8 ?  r: S) W

$ N# i6 p9 x$ b: [: a1 M1. line 207,用ldmia不是用ldmda。原因就在於存放arch 和 proc info 的位址剛好相反。一個在lable3的上面。一個在的下方。設計上應該是可以做修改的。
/ j" \  D6 H1 d1 a" N% D! w+ n8 {
2. arch定義的方式是透過macro,可以先看 ./include/asm-arm/mach/arch.h。裡頭有個 MACHINE_START ,這邊會設好macro,到時候直接使用就可以把arch的info宣告好。 例如 ./arch/arm/mach-omap1/board-generic.c
  1. /* macro */
    ! p4 S- j3 W' Q
  2.      50 #define MACHINE_START(_type,_name)                      \
    6 ^. P& D( _% C2 s. d
  3.      51 static const struct machine_desc __mach_desc_##_type    \) T, j% q2 ^1 N2 o5 ~
  4.      52  __used                                                 \
    3 V8 ^4 G( u. o$ t1 q% X: N6 {
  5.      53  __attribute__((__section__(".arch.info.init"))) = {    \
    4 g) A) a0 o6 L7 a
  6.      54         .nr             = MACH_TYPE_##_type,            \' F  T6 R1 S. P6 d- x" X& G
  7.      55         .name           = _name,
    # `3 q& M6 U& F% U
  8.      56
    " x( l8 ?9 n5 l' g9 A: `$ e) ?: V
  9.      57 #define MACHINE_END                             \7 O: h* N; E9 K/ T7 v
  10.      58 };
    + Q3 n% {' Y' A* K# B2 o
  11.      /* 用法 */4 ?; c. v' i: Z3 R- n/ F
  12.      93 MACHINE_START(OMAP_GENERIC, "Generic OMAP1510/1610/1710"). U! ~) j1 E8 s: n
  13.      94         /* Maintainer: Tony Lindgren <tony@atomide.com> */
    ; S  @. \6 q, @0 _5 p/ e" w, E
  14.      95         .phys_io        = 0xfff00000,
    2 c& Z) f9 n9 H& R, a% q
  15.      96         .io_pg_offst    = ((0xfef00000) >> 18) & 0xfffc,; I) o8 I; @1 a
  16.      97         .boot_params    = 0x10000100,
    ! S. C5 V7 S  {/ m0 `
  17.      98         .map_io         = omap_generic_map_io,
    6 l! h/ o) Z& [7 ~2 G
  18.      99         .init_irq       = omap_generic_init_irq,- j6 Z2 E% A/ z) \1 t: ~6 H% A
  19.     100         .init_machine   = omap_generic_init,7 }7 G( R2 O1 {; |) W% S1 P* K; Y  F& U
  20.     101         .timer          = &omap_timer,
    " x) F  \' b$ \4 s1 f; ~9 L
  21.     102 MACHINE_END
      ?+ T- A. d) U$ n

  22. 9 R, v. J# a& _8 C2 f
  23.     /* func */. s+ n2 ~' s+ j3 M4 ]. N' J0 x
  24.     204         .type   __lookup_machine_type, %function
    7 I: f3 J' q1 S
  25.     205 __lookup_machine_type:
    ) G' h4 ~; B' }( K$ t) ]
  26.     206         adr     r3, 3b
    0 T/ E+ Z' l! O3 M& B, d
  27.     207         ldmia   r3, {r4, r5, r6}
    8 q. d9 h* h5 f" Q  ^9 M. K
  28.     208         sub     r3, r3, r4                      @ get offset between virt&phys6 i0 O2 O: o* m# v+ j
  29.     209         add     r5, r5, r3                      @ convert virt addresses to
    - B, @0 B: H/ |
  30.     210         add     r6, r6, r3                      @ physical address space% Y2 C" R2 n: r( D4 _/ s
  31.     211 1:      ldr     r3, [r5, #MACHINFO_TYPE]        @ get machine type: t' V( \2 X  O: W
  32.     212         teq     r3, r1                          @ matches loader number?
      R' q" ~; t1 W7 v3 J- n$ L
  33.     213         beq     2f                              @ found
    - z. S* h3 Y5 z( G, ^) a
  34.     214         add     r5, r5, #SIZEOF_MACHINE_DESC    @ next machine_desc
    , k) [9 g  _+ |. R
  35.     215         cmp     r5, r63 G# e# B  |  T7 p: x& X
  36.     216         blo     1b
    6 p4 r+ Q& O. W3 G0 ~9 g
  37.     217         mov     r5, #0                          @ unknown machine
    " P) L8 ?3 u# p3 j9 W
  38.     218 2:      mov     pc, lr
複製代碼
2#
 樓主| 發表於 2008-10-9 15:32:16 | 只看該作者
既然跳到真正的kernel開始跑,表示進入重頭戲,在進入kernel之前arm的平台有一些預設的狀況,也就是說arm kernel image會預設目前的cpu和系統的狀況是在某個狀態,這樣對一個剛要跑起來的OS比較決定目前要怎麼boot起來。
) K" P( m7 m% k) h* T  P: L0 \3 D" f! |4 }: w8 |- S6 m
可以看一下comment,有清楚的描述。這也幫助我們了解為什麼decompresser的程式跑完之後,還要把cache關掉。
  1.      59 /*
    7 e; U1 Q* n/ i. u' l
  2.      60  * Kernel startup entry point.
    - f8 H3 f+ D& a) `7 }' w
  3.      61  * ---------------------------" b0 k/ X0 s: p! [5 V1 Y1 @
  4.      62  *& ]( ^- d) h; w3 W( S" B  b
  5.      63  * This is normally called from the decompressor code.  The requirements9 x; u4 p5 y- |2 k" C' Q% T
  6.      64  * are: MMU = off, D-cache = off, I-cache = dont care, r0 = 0,
    7 _6 a! L1 H- x4 i) a3 ]$ f1 p) Z7 C
  7.      65  * r1 = machine nr, r2 = atags pointer.
複製代碼
基於以上的假設,當program counter指到kernel的程式的時候,就會從line 80開始跑。
/ [7 V* W3 J3 S( C! g6 r9 T# \' tline 80, msr指令會把, operand的值搬到cpsr_c裡面。這是用來確保arm cpu目前是跑在svc mode, irq&fiq都disable。(設成0x1就會disable,definition在./include/asm-arm/ptrace.h)/ o* q% u1 E, l% k* y, A4 X: s
line 82, 讀取CPU ID到r9
4 U+ {2 [7 l/ Z. D' b2 e& dline 83, 跳到 __lookup_processor_type 執行(bl會記住返回位址)。
  1.      77     .section ".text.head", "ax"
    ; w( q! ~* k2 r- P0 V/ G
  2.      78     .type   stext, %function8 f& \+ C- K& g
  3.      79 ENTRY(stext)
    8 y+ m, Q, v1 }$ \  ~  h
  4.      80     msr cpsr_c, #PSR_F_BIT | PSR_I_BIT | SVC_MODE @ ensure svc mode
    * {2 y2 ^& l7 i  t- \6 r
  5.      81                         @ and irqs disabled
    . B6 c0 Q7 M! V* {6 v
  6.      82     mrc p15, 0, r9, c0, c0      @ get processor id, t" h' b$ ]6 n. q/ I! [
  7.      83     bl  __lookup_processor_type     @ r5=procinfo r9=cpuid
複製代碼
接著會跳到head-common.S這個檔,6 J0 Z8 O, q/ _! _
line 158, (3f表示forware往前找叫做『3』label)將label 3的address放到r3。
, O* a+ ]7 |* Rline 159, 將r3指到的位址的資料,依序放到r7, r6, r5.(ldmda的da是要每次都位址減一次), H4 S, q4 v) S9 b0 ?  R
line l60, 161, 利用真實得到位址r3減去取得資料的位址得到一個offset值,這樣可計算出r5, r6真正應該要指到的地方。: \) @) h  k  g6 h  X
line 163~169,這邊的程式應該不陌生,就是在各個CPU info裡面找尋對應的structure。找到的話就跳到line 171,返回head.S
0 f+ e6 j5 d8 S- s6 U' ^" Hline 170, 找不到的話,r5的processor id就放0x0.表示unknown id。! }8 y# s9 R3 |" ?' i! T

0 r" L1 C# N: z__proc_info_xxx可以在 vmlinux.lds.S 找到,是用來包住CPU info的所有data.資料則是被定義在./arch/arm/mm/proc-xxx.S,例如arm926就有 proc-arm926.S,裡面有相對應的data宣告,compiling time的時候,這些資料會被編譯到這個區段當中。
  1.     156     .type   __lookup_processor_type, %function
    : z  c2 `5 `/ T- n
  2.     157 __lookup_processor_type:
    3 A$ d: T% c8 I5 z# z" e
  3.     158     adr r3, 3f
    . f: _) O9 u2 ?
  4.     159     ldmda   r3, {r5 - r7}
    / s( G! [7 c" q
  5.     160     sub r3, r3, r7          @ get offset between virt&phys3 J$ k: V# j# e, g; Q# c( N9 R
  6.     161     add r5, r5, r3          @ convert virt addresses to
    3 K0 I% a3 O1 D9 _
  7.     162     add r6, r6, r3          @ physical address space
    $ e* m8 ?3 ^( d6 u) H
  8.     163 1:  ldmia   r5, {r3, r4}            @ value, mask
    ; L2 O4 r1 b" w
  9.     164     and r4, r4, r9          @ mask wanted bits* r# \+ C" m% z8 f
  10.     165     teq r3, r4; V5 e* a( V4 q
  11.     166     beq 2f
    , q2 w- U9 M1 z, e" }
  12.     167     add r5, r5, #PROC_INFO_SZ       @ sizeof(proc_info_list)
    * r2 A1 O' s" ^: ^! l2 C0 [
  13.     168     cmp r5, r63 M6 N3 s( N* ~! S, B9 Y
  14.     169     blo 1b
    - v: z4 G1 y, M" B
  15.     170     mov r5, #0              @ unknown processor
    6 [1 s  M# g2 {" X" M) [
  16.     171 2:  mov pc, lr6 ?$ y. k  L# G; g- @7 O% A

  17. 0 V2 a2 E. x" N. K0 T7 m, r
  18.     187     .long   __proc_info_begin
    1 j/ j$ v, f8 u( p+ s5 u! |
  19.     188     .long   __proc_info_end' Y& u7 K9 [) d8 {* [
  20.     189 3:  .long   .2 T4 T) e" a+ ]/ M' l
  21.     190     .long   __arch_info_begin" n) c7 b/ ^/ C1 _  Z
  22.     191     .long   __arch_info_end
複製代碼
跳了很多檔案,建議指令和object code如何link的概念要有,就會很清楚了。
您需要登錄後才可以回帖 登錄 | 申請會員

本版積分規則

首頁|手機版|Chip123 科技應用創新平台 |新契機國際商機整合股份有限公司

GMT+8, 2024-5-21 02:43 AM , Processed in 0.135517 second(s), 19 queries .

Powered by Discuz! X3.2

© 2001-2013 Comsenz Inc.

快速回復 返回頂部 返回列表