Chip123 科技應用創新平台

 找回密碼
 申請會員

QQ登錄

只需一步,快速開始

Login

用FB帳號登入

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

trace linux kernel source - ARM - 03

[複製鏈接]
跳轉到指定樓層
1#
發表於 2008-10-7 14:00:18 | 只看該作者 回帖獎勵 |倒序瀏覽 |閱讀模式
到目前為止,我們已經進展到kernel幫自己relocate完,並解將自己解壓縮到一個地方要準備開始執行,那疑問來了?到底是跳到哪裡去了,因為./compressed/head.S最後一行居然是2 x" K+ P9 L& V
『mov pc, r4』; u! P" [* R$ ^  U* I/ O
r4只代表了解壓縮完後kernel的位址,那究竟整包kernel編譯的時候,哪個function哪個東西被放在最前面咧?!
; A& E, A3 M: H: J
4 [$ x5 c5 T0 i  ^所以我們又必須開始找於kernel link的時候是怎麼被安排的,有了前面的基礎,我們可以從Makefile知道程式碼如何被編譯。至於link上的細節,例如有那些section和section先後順序等等,可以從 lds 檔來規範。) D3 F  U) c% ^

, N7 q; x  D+ k有興趣的人可以看一下 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)
' P4 t) Q9 E4 i我們可以發現第一個section是『.text.head』,裡頭的_stext從目前的位置開始放。" `7 O4 C, y6 W( m! @: a
於是我們曉得只要找到屬於.text.head這個section,並且是_stext這個symbol的程式碼,就是解壓縮完後的第一行程式碼。
  1.      26     .text.head : {
    / v3 G; }4 ^4 B+ r' X* d* G
  2.      27         _stext = .;8 i7 F* w* d; F; H" L* l
  3.      28         _sinittext = .;
    2 M  S7 R" N! t8 q* y
  4.      29         *(.text.head)
    . ^; l% X* H, U  o2 K, y
  5.      30     }
複製代碼
用指令搜尋一下,發現 ./arch/arm/kernel/head.S 裡頭有關鍵字(如下),結果我們從./arch/arm/boot/compressed/head.S跳到了./arch/arm/kernel/head.S
  1.      77     .section ".text.head", "ax"
    ' f+ m2 A  P1 ?3 m' e6 G: H/ S4 J
  2.      78     .type   stext, %function* H/ [! V- C7 g( U
  3.      79 ENTRY(stext)
      c& W# ^3 w% I8 \* K8 @+ _& M
  4.      80     msr cpsr_c, #PSR_F_BIT | PSR_I_BIT | SVC_MODE @ ensure svc mode
    ; d& Q3 B  L* q) C
  5.      81                         @ and irqs disabled
    6 K( U% b) m) v4 j4 ^/ j6 ?6 o
  6.      82     mrc p15, 0, r9, c0, c0      @ get processor id4 ]9 |# V& E. W
  7.      83     bl  __lookup_processor_type     @ r5=procinfo r9=cpuid
    7 M# [: A. ]2 {) z) G. W
  8.      84     movs    r10, r5             @ invalid processor (r5=0)?; W3 l9 \3 K0 V# H
  9.      85     beq __error_p           @ yes, error 'p'
    . ^. ]' |9 T! ?+ W
  10.      86     bl  __lookup_machine_type       @ r5=machinfo) X8 A9 l1 f, J# Q6 I- X  U
  11.      87     movs    r8, r5              @ invalid machine (r5=0)?
    ; X) q/ w8 n% h, E7 w8 F
  12.      88     beq __error_a           @ yes, error 'a'
    3 `. c9 o9 }% d8 r; r& Z0 x
  13.      89     bl  __vet_atags% ^, ?/ A; w# R  K7 @
  14.      90     bl  __create_page_tables
複製代碼
既然找到了檔案,我們又可以開始繼續。/ M) D: n8 i3 x3 u" n* a5 z

) V: l8 u! a0 V  I! H% O: v9 c看了一下,程式碼多了很多bl的跳躍動作,看來會在各個function跳來跳去。
分享到:  QQ好友和群QQ好友和群 QQ空間QQ空間 騰訊微博騰訊微博 騰訊朋友騰訊朋友
收藏收藏 分享分享 頂 踩 分享分享
2#
 樓主| 發表於 2008-10-9 15:32:16 | 只看該作者
既然跳到真正的kernel開始跑,表示進入重頭戲,在進入kernel之前arm的平台有一些預設的狀況,也就是說arm kernel image會預設目前的cpu和系統的狀況是在某個狀態,這樣對一個剛要跑起來的OS比較決定目前要怎麼boot起來。! I, C. W( B9 T8 [' ^

6 c( y8 ]2 R& C4 a可以看一下comment,有清楚的描述。這也幫助我們了解為什麼decompresser的程式跑完之後,還要把cache關掉。
  1.      59 /*+ T" d; T8 z9 H2 T8 i2 ]
  2.      60  * Kernel startup entry point.
    % Q" d+ T) T# Y, U& x- e9 C
  3.      61  * ---------------------------. ~. w: R6 ?: K
  4.      62  *
    # J4 y0 W  @+ G
  5.      63  * This is normally called from the decompressor code.  The requirements  T, U9 [6 g9 {. w& ]6 X
  6.      64  * are: MMU = off, D-cache = off, I-cache = dont care, r0 = 0,
    ' X& C0 K. R+ Y( ~
  7.      65  * r1 = machine nr, r2 = atags pointer.
複製代碼
基於以上的假設,當program counter指到kernel的程式的時候,就會從line 80開始跑。
4 q: y3 v8 j8 W3 w- \+ sline 80, msr指令會把, operand的值搬到cpsr_c裡面。這是用來確保arm cpu目前是跑在svc mode, irq&fiq都disable。(設成0x1就會disable,definition在./include/asm-arm/ptrace.h)! t. A$ S' Y1 r% @
line 82, 讀取CPU ID到r9' f! H; n2 T# }. j  z+ I; Z. J- S+ L
line 83, 跳到 __lookup_processor_type 執行(bl會記住返回位址)。
  1.      77     .section ".text.head", "ax") p0 p& L' B% ~! J$ A* k" o
  2.      78     .type   stext, %function
    ! k! ]+ ~0 d2 C
  3.      79 ENTRY(stext)
    2 v/ ]! ]$ D  G. F# w' B
  4.      80     msr cpsr_c, #PSR_F_BIT | PSR_I_BIT | SVC_MODE @ ensure svc mode
    6 R% B0 D, h1 [7 y! t1 O. h
  5.      81                         @ and irqs disabled8 C1 Z6 @3 V( O- C+ L) s8 t
  6.      82     mrc p15, 0, r9, c0, c0      @ get processor id
    " I) o, F: h) A5 Q6 L
  7.      83     bl  __lookup_processor_type     @ r5=procinfo r9=cpuid
複製代碼
接著會跳到head-common.S這個檔,
5 I5 }, n8 E5 P) M0 nline 158, (3f表示forware往前找叫做『3』label)將label 3的address放到r3。/ a) Y5 K% |" r" m
line 159, 將r3指到的位址的資料,依序放到r7, r6, r5.(ldmda的da是要每次都位址減一次)4 d- ?8 c0 Z& q  w8 w- `8 S
line l60, 161, 利用真實得到位址r3減去取得資料的位址得到一個offset值,這樣可計算出r5, r6真正應該要指到的地方。9 [( ?" \! A& [. y. x: Z( s& k
line 163~169,這邊的程式應該不陌生,就是在各個CPU info裡面找尋對應的structure。找到的話就跳到line 171,返回head.S
# V9 n- w; A5 ~. Vline 170, 找不到的話,r5的processor id就放0x0.表示unknown id。, M) Y/ ~/ h3 c4 T' `3 v9 j
" T) A4 h! l" |* a4 j# t
__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; W# [: C+ o* Z' j6 e8 W- T
  2.     157 __lookup_processor_type:. |' m, p7 n& {
  3.     158     adr r3, 3f7 \$ V! W7 E7 |# I' d$ L
  4.     159     ldmda   r3, {r5 - r7}
    1 r5 r  z3 v: L% j; _
  5.     160     sub r3, r3, r7          @ get offset between virt&phys- X% c( x# D7 G! r4 Z
  6.     161     add r5, r5, r3          @ convert virt addresses to
    # b6 y2 M2 f9 \7 B& O, q
  7.     162     add r6, r6, r3          @ physical address space
    ; ?6 O0 Q/ k5 c; Z3 ~
  8.     163 1:  ldmia   r5, {r3, r4}            @ value, mask( d% F( P% }/ G- n- N2 f8 v, k
  9.     164     and r4, r4, r9          @ mask wanted bits
    . n5 ?6 r- |. c) ^0 O0 L- G
  10.     165     teq r3, r4
    : @0 h0 e" L4 N
  11.     166     beq 2f; D: |5 s1 h9 a4 E0 ^
  12.     167     add r5, r5, #PROC_INFO_SZ       @ sizeof(proc_info_list); L  K. u  C: E4 M3 X7 j
  13.     168     cmp r5, r69 m% `2 J) f6 A& j
  14.     169     blo 1b* v8 S! k  d3 w7 M( t, O' ~7 t- G
  15.     170     mov r5, #0              @ unknown processor- k3 V& T/ F5 X" K7 v
  16.     171 2:  mov pc, lr
    . e0 \5 B7 N) ?* e3 g- _4 |' J$ Q
  17. 3 j2 d- Q  y; T8 X
  18.     187     .long   __proc_info_begin
    1 f* ]6 D# M& P/ s( J; Z3 T4 ~6 M' X% G
  19.     188     .long   __proc_info_end9 `) p8 W+ h: B3 W9 |8 v7 n
  20.     189 3:  .long   .
    2 N% }' F, W, p. M' z. K
  21.     190     .long   __arch_info_begin% K( ?! B, J; C  A- a9 Y
  22.     191     .long   __arch_info_end
複製代碼
跳了很多檔案,建議指令和object code如何link的概念要有,就會很清楚了。
3#
 樓主| 發表於 2008-10-13 17:20:35 | 只看該作者
我們從 head-common.S返回之後,接著繼續看。
, B8 B* g  \4 l) ~' Q1 q, C. _, q+ u- }5 d
line 84, movs的意思是說,做mov的動作,並且將指令的S bit設起來,最後的結果也會update CPSR。這個指令執行的過程當中,會根據你要搬動的值去把N and Z flag設好。這個是有助於下個指令做check的動作。
4 A. U9 o7 z$ ]: E7 H; P7 Sline 85, 就是r5 = 0的話,就跳到__error_p去執行。: `9 l2 |7 h8 Y! [4 }8 C
line 86, 跳到__lookup_machine_type。原理跟剛剛找proc的資料一樣。
  1.      83         bl      __lookup_processor_type         @ r5=procinfo r9=cpuid
    1 B2 M1 O1 a& i8 {) x4 R8 Z) G
  2.      84         movs    r10, r5                         @ invalid processor (r5=0)?
    5 v7 l/ e- T  S- W
  3.      85         beq     __error_p                       @ yes, error 'p'
    + Q' |1 |8 b4 s+ c% \7 J1 U+ j1 R. U
  4.      86         bl      __lookup_machine_type           @ r5=machinfo
複製代碼
看得出來跟proc很像,有個兩個小地方是6 O8 {( G! q- U9 z6 s3 V# o
2 }% V* G3 A) \/ m" b: v
1. line 207,用ldmia不是用ldmda。原因就在於存放arch 和 proc info 的位址剛好相反。一個在lable3的上面。一個在的下方。設計上應該是可以做修改的。# Y+ f' w; ?; C6 _

- U/ e( I. L) p) L2. arch定義的方式是透過macro,可以先看 ./include/asm-arm/mach/arch.h。裡頭有個 MACHINE_START ,這邊會設好macro,到時候直接使用就可以把arch的info宣告好。 例如 ./arch/arm/mach-omap1/board-generic.c
  1. /* macro */0 |6 J7 o0 C" M: U- _) {& P
  2.      50 #define MACHINE_START(_type,_name)                      \
    . i; q% h. ~! o; G1 x. [
  3.      51 static const struct machine_desc __mach_desc_##_type    \
    . q* y7 g, \% ?$ x( D
  4.      52  __used                                                 \$ o+ |; l. h# Q( [+ h
  5.      53  __attribute__((__section__(".arch.info.init"))) = {    \
    & e3 b6 w# m! Z  _3 G9 M( |' l
  6.      54         .nr             = MACH_TYPE_##_type,            \
    - o" ^. |; A0 N; D
  7.      55         .name           = _name,: k- c1 T7 v; @7 m# W+ t. ?
  8.      56- o9 M5 h! B* l( H6 [' i8 d
  9.      57 #define MACHINE_END                             \3 [2 P, j1 ]. k/ l& P! v) P
  10.      58 };, }5 b) ]0 J7 h' ?3 ^0 {
  11.      /* 用法 */
    1 f, K" d/ I5 j9 y7 Y6 y! _
  12.      93 MACHINE_START(OMAP_GENERIC, "Generic OMAP1510/1610/1710")/ w: y1 C$ I/ I; ]9 p* \" \) }
  13.      94         /* Maintainer: Tony Lindgren <tony@atomide.com> */" A" V, x% @/ O( F6 j3 E7 a+ d( S2 |. y
  14.      95         .phys_io        = 0xfff00000,
    , N2 l* j. P6 z, r
  15.      96         .io_pg_offst    = ((0xfef00000) >> 18) & 0xfffc,8 o- {( Z' k: x8 {  r) a7 g- @) P
  16.      97         .boot_params    = 0x10000100,
    & F: X5 K3 {1 p- `
  17.      98         .map_io         = omap_generic_map_io,
    . y5 d4 f  V: z  J8 W3 f
  18.      99         .init_irq       = omap_generic_init_irq,
    " |8 D# E7 d* F; v! a
  19.     100         .init_machine   = omap_generic_init,6 ?" |- s* Z& F$ ^8 o
  20.     101         .timer          = &omap_timer,2 K' ]9 E: G" g9 p8 Z5 x$ y+ s9 e
  21.     102 MACHINE_END+ C4 e7 N" e$ f2 }% M/ M0 ]" c$ R
  22. 4 i( S1 V* L+ R+ R" L' `7 _
  23.     /* func */' ?; U/ [2 d( ~! I
  24.     204         .type   __lookup_machine_type, %function( @/ X/ G8 @' \1 B  s
  25.     205 __lookup_machine_type:( Y( P- {: `4 w8 a/ A
  26.     206         adr     r3, 3b
    . a9 s' q3 ?+ ~# f
  27.     207         ldmia   r3, {r4, r5, r6}# {* X0 a7 E. P. t9 [
  28.     208         sub     r3, r3, r4                      @ get offset between virt&phys
    % }7 ~3 ~! W0 A# o# r7 O9 J
  29.     209         add     r5, r5, r3                      @ convert virt addresses to1 L+ E+ t1 n  x# z2 T
  30.     210         add     r6, r6, r3                      @ physical address space
    6 B4 l+ U, A* V) H, w# m
  31.     211 1:      ldr     r3, [r5, #MACHINFO_TYPE]        @ get machine type
    ' [/ C2 n7 D" j) m* \5 c" X9 S& u4 r
  32.     212         teq     r3, r1                          @ matches loader number?* s6 }  s2 M2 ^* U
  33.     213         beq     2f                              @ found3 A) x& t4 k) \9 X, r
  34.     214         add     r5, r5, #SIZEOF_MACHINE_DESC    @ next machine_desc
    ( e) o; k* V! J
  35.     215         cmp     r5, r6
    ; t! j% l2 O1 d# b/ f
  36.     216         blo     1b
    " m9 n# ~0 ^/ }) v  j; g3 S0 Z
  37.     217         mov     r5, #0                          @ unknown machine
    8 {0 R/ y( X- z, h, c8 ~$ M1 O; b, {
  38.     218 2:      mov     pc, lr
複製代碼
4#
 樓主| 發表於 2008-10-13 17:56:46 | 只看該作者
接著我們又返回到head.S,
! v6 t+ m' \' S2 S$ O. vline 87~88也是做check動作。
; o0 p: O7 {8 A+ g5 a4 i9 aline 89跳到vet_atags。在head-common.S
  1.      87         movs    r8, r5                          @ invalid machine (r5=0)?* H$ y' F% f6 |* f8 c: V
  2.      88         beq     __error_a                       @ yes, error 'a'* w0 c! R% N) h9 i+ P% m
  3.      89         bl      __vet_atags/ _+ D$ h# P8 p* N5 f& v* l
  4.      90         bl      __create_page_tables
複製代碼
line 245, tst會去做and動作。並且update flags。這邊的用意是判斷位址是不是aligned。+ E' J, \: W7 b& ^. c; `) i# T6 s* k
line 246, 沒有aligned跳到label 1,就返回了。( b. e4 q8 B# X3 _
line 248~250, 讀取atags所在的address裡頭的值到r5,看看是否不等於ATAG_CORE_SIZE,不等於的話也是返回。
6 D8 a, u& E- Xline 251~254, 判斷一下第一個達到的atag pointer是不是等於ATAG_CORE。如果正確的話,等一下要讀取atag的資料,才會正確。
% w. ^; y( ~* o' f/ K; B, A/ s: Q(atag是由bootloader帶給linux kernel的東西,用來告知booting所需要知道的參數。例如螢幕寬度,記憶體大小等等)
  1.      14 #define ATAG_CORE 0x54410001
    " s- ]( x5 I; R; e, C% @) I' K
  2.      15 #define ATAG_CORE_SIZE ((2*4 + 3*4) >> 2)  }2 j" i5 g/ L" w' H3 h

  3.   s  y: {% ?) p! r- Q
  4.     243         .type   __vet_atags, %function# B3 a+ ]; [$ w6 H0 J. o! U
  5.     244 __vet_atags:
    - {0 z4 M! ?( d3 Z) d0 L) J
  6.     245         tst     r2, #0x3                        @ aligned?. r& K$ r4 C0 B  N/ `; S
  7.     246         bne     1f9 U  P  v7 k; `. L( ~# i! E
  8.     247
    , Y+ |" j  o+ Z4 j: ]6 i5 q
  9.     248         ldr     r5, [r2, #0]                    @ is first tag ATAG_CORE?0 V+ l" v; m5 `6 P3 A
  10.     249         subs    r5, r5, #ATAG_CORE_SIZE# {$ f9 ?$ K: @1 ~! X/ b  P
  11.     250         bne     1f
    ; `/ f* N9 ]; Y, f& r
  12.     251         ldr     r5, [r2, #4]
    0 N( z7 u! G1 v: o. Y3 q& Z8 g
  13.     252         ldr     r6, =ATAG_CORE. H& y' Y0 W1 G$ V
  14.     253         cmp     r5, r6" ?; a* O4 @; M$ c7 o, h7 H+ O
  15.     254         bne     1f7 C5 U) B4 E8 k. w
  16.     255
    . D3 Z9 V8 J0 E  h, i+ o! a
  17.     256         mov     pc, lr                          @ atag pointer is ok
    ; z- B  x' u3 ~8 w! Y  q- Y5 {% m! s
  18.     257
    / t' a0 ~* S* q  j& ~
  19.     258 1:      mov     r2, #0
    * x- D& o) M8 n
  20.     259         mov     pc, lr
複製代碼
接著我們又跳回去head.S。  - l# c7 r0 \; p
line 90,又跳到 __create_page_tables。   (很累人....應該會死不少腦細胞)
4 ]( z2 y3 S! n9 |哇!page table?!!如雷貫耳的東西,不知道會怎麼做。剛剛偷看了一下,@@還蠻長了,先到這邊好了,下次在繼續寫。
5#
 樓主| 發表於 2008-10-14 12:13:51 | 只看該作者
由於code看起來似乎越來越難解釋,會需要在檔案之間跳來跳去。建議一些基礎的東西可以再多複習幾次(其實是在說我自己 ),閱讀source code上收穫會比較多。例如:
, G* {. B2 D. K4 r7 n
0 I  [" |% W4 C) \$ }: V1 P4 F1. arm instruction set - 這個最好每個指令的意思都大略看過一次,行有餘力多看幾個版本,armv4, v5 or v6。
9 z" r4 h/ S0 k5 Z2. compiler & assembler & linker - toolchain工具做的事情和功能,大致的流程和功能要有概念。2 m! @9 L5 j) ]) n- ~5 J
3. Makefile & link script - 這兩個功能和撰寫的方式要有簡單的概念。  C! q' Q/ H' F5 a' Q
2 Z) K* n( {2 l5 ]: e
以上1是非常重要的重點,2&3只要有大致上的概念就可以,因為trace code的時候,有時需要跳到Makeflie&Link script去看最後object code編排的位址。
7 I, A$ B4 {; n9 h0 t3 s9 y! i; u( P
由於我們trace到了page table這個關鍵字,在開始之前,稍微簡短的解釋,為了幫助了解,儘量用易懂的概念講,有些用詞可能會和一些真實狀況不同,但是懂了之後,應該會有能力分辨,至於正式而學術上解說,很多書上應該都有,或是google一下就很多啦∼
6#
 樓主| 發表於 2008-10-14 12:14:47 | 只看該作者
page table本身是很抽象的東西,尤其是對所謂的 user-mode application programmer 來說,大部分的狀況它是不需要被考慮到的部份,但是他的產生卻對os和driver帶來了很多影響。我們從一個小小的疑問出發。) K+ B0 y4 P# _
9 w! S: v% w1 a
『產生page table到底是要給誰用的?』
! M8 ]% }# T* O- ?& b! L3 c: A/ h. _7 [' U+ S1 _
其實真正作用在它上面的H/W是MMU,一旦CPU啟用了MMU,當cpu嘗試去記憶體讀取一個operand的時候,cpu內部打出去的位址訊號都會先送到mmu,mmu會拿著這個位址去對照page table,看看這個位址是不是被轉換到另外一個位置(還會確認讀寫權力),最後才會到真正的位址去讀寫。
) @$ J  r& v! a) G, b
% T6 U. O. ^3 h6 }' o這樣來看CPU其實一開始打出去的位址訊號其實不是最後可以拿到資料的位址,我們稱為virtual address(va),mmu打出來的位址才能真正拿到資料,所以我們把mmu打出去的位址稱為physical address(pa)。那用來查詢這個位址對照關係的表格,就是page table。
9 \& M# v% b2 b: {1 f: o3 {2 u1 L9 A. j; f/ D
到這邊我們有一個簡單的概念。來想像一個小問題,一個普通的周邊(例如你的顯示卡),因為他並不具有MMU功能,hw只看得懂pa,但是CPU卻是作用在va,假如我想去對hw的控制暫存器做讀寫,到底要用va還是pa?
7#
 樓主| 發表於 2008-10-14 12:15:51 | 只看該作者
這時,寫driver的人就必須要小心,設定給硬體看的位址必須要先轉成pa(像是設定DMA),給CPU執行的程式碼要使用va,這對一開始嘗試寫driver但是又不瞭解va pa之間的差別的人,常常產生很多疑問。: i; [  i# N0 z0 S7 B4 o6 J- J

( {( Y: |/ }% {8 u) C3 L9 ]現在我們回頭想想OS,既然我們跑到create page table,可以預期的是他想要將MMU打開,因此希望預先建立起一個page table,讓MMU知道目前os想規劃的位址對應和讀取權力是如何被安排的。所以os必須考慮到現在的系統究竟是長怎樣?應該要如何被安排?是不是有那些要被保護?好吧∼因為我們完全對os不了解,也不知道該安排什麼,只能祈禱在trace code完後,可以找到這些問題的答案,或者發現一些沒想到的問題。
/ L% ]2 m) f  w/ Y; @9 K! u2 R7 |4 d" D7 U$ r3 J4 \
知道了page table的大致上的功能,下篇就可以專心的研究這個table的長相,和它想規劃出的系統模樣。
4 F3 L6 Z$ K' |& J0 j' a8 `9 p2 P- c. ?% I# z! X: k" S
p.s. 字數限制好像變短了。   (看來很難寫)
8#
 樓主| 發表於 2008-10-14 13:56:17 | 只看該作者
由於字數限制挑整,用詞儘量精簡,以便可以貼必要source。另外,以後寫到一段落,考慮收集成一篇完整的文章,這樣應就不會因為用回覆的方式,造成閱讀斷斷續續的問題。希望有人對文章呈現方式有想法的話,可以跟我講。
9#
 樓主| 發表於 2008-10-14 13:57:39 | 只看該作者
現在,讓我們跳入create_page_tables吧∼
, M4 s/ F, q& R. uline 216,會跳到pgtbl的macro去執行,其實只是載入一個位址。
! j# k4 I1 t+ ]4 N  a& F0 |# N( h( ~9 r, W) T/ m
只是這個位址因為你硬體規劃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 */; P. H' b& r; A
  2.      95 textofs-y       := 0x00008000# ~- [  b8 O3 v
  3.     152 TEXT_OFFSET := $(textofs-y)
複製代碼
  1. /* include/asm-arm/arch-omap/memory.h */8 ^$ J" `% t, m. h
  2.      40 #define PHYS_OFFSET             UL(0x10000000)
    & E  \1 S1 b% |

  3. & y+ m) l2 @2 B
  4.      /* arch/arm/kernel/head.S */
    9 i5 D3 F: E* w+ V. G* y' s3 q
  5.      29 #define KERNEL_RAM_VADDR        (PAGE_OFFSET + TEXT_OFFSET)8 v7 n" \7 b/ T7 _9 ~. d1 o8 m# C
  6.      30 #define KERNEL_RAM_PADDR        (PHYS_OFFSET + TEXT_OFFSET)
    * ?7 @. ^9 y9 ?/ ~" F( a, Z3 q
  7. ! R  S# W2 r7 |7 n2 B
  8.      47         .macro  pgtbl, rd
    / G" \, C/ }  l: M: e* w
  9.      48         ldr     \rd, =(KERNEL_RAM_PADDR - 0x4000)1 y2 e) x8 p$ y; m- C# ^3 E
  10.      49         .endm1 L5 U/ Z' A5 z) V/ Z; ?0 H

  11. 1 D3 @! ~7 w  W7 j+ n* A
  12.     216         pgtbl   r4                              @ page table address
複製代碼
10#
 樓主| 發表於 2008-10-14 14:16:53 | 只看該作者
得到pg table的開始位置之後,當然就是初始化囉。
5 P; ?4 V0 ^4 A6 E/ n# ^line 221, 將pg table的base addr放到r0.
% C# ~5 c- h* iline 223, 將pg table的end addr放到r6.% [8 ~, w9 x2 F& q, X
line 224~228, 反覆地將0x0寫到pg table的區段裡頭,每個loop寫16bytes. (4x4),直到碰到end,結果就是把它全部clear成0x0.
  1.     221         mov     r0, r4
    - ]5 O: I9 U5 E) x  q- l4 K
  2.     222         mov     r3, #0
    ) T" t2 m* q% r# \
  3.     223         add     r6, r0, #0x4000
    ; T6 F5 ~6 t1 a
  4.     224 1:      str     r3, [r0], #4
    5 E( S5 t1 f; O8 {4 [
  5.     225         str     r3, [r0], #48 Y, E" \# B) \0 I8 h3 a. ~$ n/ e
  6.     226         str     r3, [r0], #4+ {' ^6 b, m8 w5 T5 x- K# Y: m
  7.     227         str     r3, [r0], #40 V7 w7 s, ^' s: l) l; m) `
  8.     228         teq     r0, r6
    . I1 T  P/ P6 ^% r
  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
複製代碼
11#
 樓主| 發表於 2008-10-14 15:11:48 | 只看該作者
問題怎麼填值??
) f; s& h( F6 X$ v0 d8 e( J1 i+ y, p拿出ARM的手冊,翻到MMU章節。一看發現page有很多種,還得分first level和second level。 / X1 k, x9 m! k# x

" j9 ?8 M% u7 k) O1 y$ F# u念書時的印象,是從1st level在去查2nd level,先看怎麼設定1st level (不知以前老師有沒有亂教)
, b1 e! v/ z/ s' l( t6 I- A1. [31:20]存著section base addr
; ]0 r, o; T+ C' c% j3 S+ e! K2. [19:2]存著mmu flags# o% }9 V# c! v' l; j
3. [1:0]用來辨別這是存放哪種page, 有四種:, c) s/ J# e* a; j6 U- Z- |
   a. fault (00) b. coarse page (01) c. section (1st level) (10) d. fine page (11)
( H: l, H6 @% a4. pg tabel資料要存放到 [31:14] = translation base, [13:2] = table index , [1:0] = 0b00 的位址
+ g1 }  x) Q1 I9 g# w. @4 I- o
, c5 \. u$ j' L% S$ ^來看code是怎麼設定。
) X9 @- }* I2 A9 Q5 Z  g! f% g/ v4 v9 u3 Q
line 239, 將pc的值往右shift 20次放到r6.這是取得前20高位元的資料,有點像是 1.。# t" y3 D: l1 I! p5 R3 R! H
line 240, 將r6往左shift 20次之後,和r7做or的動作,剛好也是 2.提到的mmu flags和1.處理好的資料做整理。! f( I  W0 @! h: p
所以前面兩個做完,就完成了bit[31:2]。
$ E6 b$ h+ o5 P( e0 t) a& C8 nline 241, 將r3的資料寫到r4+(r6<<0x2)的地方去,剛好是4.提到的位址。(lucky)
  1.     239         mov     r6, pc, lsr #20
    * j8 f! Q# s: s0 y* _2 X8 s; E( |
  2.     240         orr     r3, r7, r6, lsl #203 `  X6 f. k: ]
  3.     241         str     r3, [r4, r6, lsl #2]
複製代碼
p.s. 用pc值剛好可以算出當前的page。
12#
 樓主| 發表於 2008-10-22 19:47:03 | 只看該作者
最近又被釘上了,開始忙碌,大概沒太多時間貼文∼
# Q: S: K, h6 k( j
. }* \5 h7 |: \上篇已經將pc所屬的page table entry(pte)設定好,接著繼續看9 U3 R7 O1 ?. O1 F
line 247, 248, 將KERNEL_START的位址往右shift 18算出pte的offset,(等於line239&241,239&241是先shift right 20然後shift left 2),並將剛剛r3的值設給pte。『!』的意思是會把address存到r0。0 x+ O- R! N* h. i8 e* U" _- B
' v! l) \$ {# q1 d) b
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
    4 x0 V( h3 _7 g6 ~
  2.     248         str     r3, [r0, #(KERNEL_START & 0x00f00000) >> 18]! 3 X( C/ t  J& D4 N
  3.     249         ldr     r6, =(KERNEL_END - 1)
    ) D5 j. Q* B, D- k+ F% S
  4.     250         add     r0, r0, #4
    % Y: d0 `2 S% `' a) K
  5.     251         add     r6, r4, r6, lsr #18
    6 |- |3 z$ E$ m
  6.     252 1:      cmp     r0, r6
    9 B8 B' L/ \1 P
  7.     253         add     r3, r3, #1 << 20
    6 L! P: s8 T5 B& H" Q
  8.     254         strls   r3, [r0], #4
    - Y  \% M% A! d2 t, N( T
  9.     255         bls     1b
複製代碼
13#
 樓主| 發表於 2008-10-22 20:24:58 | 只看該作者
line279,PAGE_OFFSET是規範RAM mapping完後的virtual address,我們將這個位址所屬的pte算出來放到r0。9 @& W  ]# v3 H; ?
line 280~283,將要 map 的physical address的方式算出來放到r6。
1 G7 c4 _" a' W5 V% T# c5 oline 284,最後將結果存到r0所指到的pte。( `9 V* I9 M) ~1 j1 f, v1 N3 x
& h, L8 ^: Y+ q. W9 \% a
以上三個動作,就是做好 ram 起頭的位址一開始map,還沒做完整塊map。由於我們目前的設定方式每塊是1MB,所以這邊是將RAM開始的1MB做map。' f9 \6 v! k# B

* q' e9 F" @; E- H2 i6 `line 327,返回,結束一開始的create page table的動作,我們可以看出其實並沒有做完整的初始化page table,所以之後應該會有其他page table的細節。
  1.     279         add     r0, r4, #PAGE_OFFSET >> 18
    9 `/ j% x. B# R$ v2 l
  2.     280         orr     r6, r7, #(PHYS_OFFSET & 0xff000000)
    5 f9 p& q% O4 j+ n- p
  3.     281         .if     (PHYS_OFFSET & 0x00f00000)- J1 w) q. O3 u- r1 T" |; g
  4.     282         orr     r6, r6, #(PHYS_OFFSET & 0x00f00000)' w( m5 e0 a6 X1 y+ e! J
  5.     283         .endif
    % w, l* e( S5 g9 M: x$ A) ^. h5 N* F
  6.     284         str     r6, [r0]$ Q0 T  t8 ?% i5 g( b" p
  7.     327         mov     pc, lr
複製代碼
附帶一提,我們這邊省略了一些用ifdef包起來的程式碼,像是一開始會印一些output message的程式碼(line286~326)。
14#
 樓主| 發表於 2008-10-22 20:37:08 | 只看該作者
自create page table返回後,我們偷偷看一下接下來的程式碼," v* `. r2 T. e9 e. y: |
line 99, 將switch_data擺到r13
2 S+ h$ g0 ^8 i0 z- K3 i& U2 E9 wline 101, 將enable_mmu擺到lr
4 e) c. Q: y6 a' A: {( w6 n; ~6 w7 kline 102, 將pc改跳到r10+PROCINFO_INITFUNC的地方去
. u8 a* [& R( \1 \/ \1 |8 H) P! B, d6 o. i" t
其實這邊有點玄機,switch_data和enable_mmu都是function,結果位址都只是被存起來,並沒有直接跳過去執行。其實,雖然程式碼是順序switch_data->enable_mmu->proc init function,但其實執行的順序會是 procinfo 的init function-> enable_mmu -> switch_data 。至於,為什麼要這樣寫的原因?就不清楚了,也還沒仔細推敲過。 , O" n' X( l9 B5 i

7 d9 X1 M5 b) Q& ]1 v- m3 vswitch_data最後就會跳到大家都很熟悉的start_kernel(). 詳細要賣個關子,最近會忙一下,得要過一陣子才能貼文∼  
  1.      99         ldr     r13, __switch_data              @ address to jump to after
    3 H& w# T, B2 o. Y3 B# u" s, V
  2.     100                                                 @ mmu has been enabled
    1 l1 \4 f+ F5 L
  3.     101         adr     lr, __enable_mmu                @ return (PIC) address6 H( i1 t& G6 e: G
  4.     102         add     pc, r10, #PROCINFO_INITFUNC
複製代碼
15#
 樓主| 發表於 2009-7-4 01:09:36 | 只看該作者
老店重新開張~
4 L! ^0 G# ^% E" r
' `. n" C" m" a( b& _& T花了一些時間把舊的貼文整理到一個blog1 P' c( \9 C* K" f! a- ~9 P
有把一些敘述修改過) M7 E/ Z4 Q9 I3 }! [: v  b
希望會比較容易集中閱讀% x6 t5 J. |! B9 E% \( P# X6 e
目前因為某些敘述不容易. Y& _( b! M; T
還是比較偏向筆記式而且用字不夠精確+ F) v* x) W$ _+ e
希望之後能夠慢慢有系統地整理; j0 M+ m2 N6 p) k2 l* I
大家有興趣的話- \; b0 ~0 ]9 s, H, m% W  X
可以來看看和討論 6 D  I3 k% b; _
http://gogojesseco.blogspot.com/
7 D: W0 d' o+ [2 H" _3 t0 [3 J; M" l5 `4 ?
以後可能會採取  先在chip123貼新文章! x, I8 A& o1 ^- b5 c
慢慢整理到blog上的方式
( o% M' i4 I+ _% y7 |7 \3 l/ Y7 k因為chip123比較方便討論 =)
' ]  {- ]) X; \4 cblog編輯修改起來比較方便) b. u3 a. U! B4 q% h+ `
閱讀也比較集中   大家可以在這邊看到討論
2 M: C0 v" A/ F+ |然後在blog看到完整的文章 (類似BBS精華區的感覺)

評分

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

查看全部評分

16#
 樓主| 發表於 2009-7-15 17:07:03 | 只看該作者
隔了很長一段時間沒update* j9 ?* [8 N! q/ g% H; L
之前程式碼走到 ./arch/arm/kernel/head.S 的 line 99 附近
  1.      99         ldr     r13, __switch_data              @ address to jump to after
    9 ?+ R' x. a% c* t- ?2 C1 Q0 S
  2.     100                                                 @ mmu has been enabled
    6 Y3 A$ O' I" p
  3.     101         adr     lr, __enable_mmu                @ return (PIC) address
    " E) k  v: y4 |
  4.     102         add     pc, r10, #PROCINFO_INITFUNC
複製代碼
line 99, 將__switch_data放到r13。(留作之後用): N9 @4 y' X) b( Q/ r- U8 l# f9 R
line 101, 將__enable_mmu的addr放到lr。(留作之後用)0 m1 E: w* M, }: ~* ~8 Q; S* U
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! F% t+ {0 ~. e( U9 O3 L0 p7 b, W
  2. 374 __arm926_setup:1 ^- @5 X. a. `. X
  3. 375         mov     r0, #0
    " c1 H. Z4 |: |; h9 j/ w" ]
  4. 376         mcr     p15, 0, r0, c7, c7              @ invalidate I,D caches on v46 n1 S- S, A4 \$ b
  5. 377         mcr     p15, 0, r0, c7, c10, 4          @ drain write buffer on v4
    9 ^* d+ F' q* D. D# t
  6. 378 #ifdef CONFIG_MMU
    : ~. B; e  h& A
  7. 379         mcr     p15, 0, r0, c8, c7              @ invalidate I,D TLBs on v4
    8 I7 W, F* o& J* Q4 E- s  Q, W
  8. 380 #endif
    4 _1 ^: N0 o4 m9 p6 ]8 G

  9. / b# o+ I; A: F$ @+ F
  10. 388         adr     r5, arm926_crval8 K: ?8 l6 t: `
  11. 389         ldmia   r5, {r5, r6}
    # n5 {! P+ Q# r  J" b3 I- N
  12. 390         mrc     p15, 0, r0, c1, c0              @ get control register v4
    ' j# C- Q2 Z  P& e. G: r% P
  13. 391         bic     r0, r0, r5
    & D5 F6 ~! W7 i! [, S0 p6 l
  14. 392         orr     r0, r0, r6
    $ C, a+ i& |0 Z6 `3 g5 e: U. N

  15. , {( v& d2 P0 S" Q3 _
  16. 396         mov     pc, lr
    6 ^' [( A) p/ S
  17. 397         .size   __arm926_setup, . - __arm926_setup
複製代碼
這邊的程式碼就跟CPU有很大的相依性,
( D0 g7 J2 i& j7 c1 W7 z7 x# _line 375~380, 主要就是invalidate CPU的I&D cache和清空write buffer。& j+ ^2 V: `) d* ]9 i1 N. {
line 388~392, 把cp15的設定讀出來,並且將一些預設狀態做好運算放到r0。(預設值從arm926_crval這邊可以取得。)* Q+ ]! \/ Y. w  l6 R5 m; D/ n
line 396, 直接把pc跳到lr,因為我們之前已經將lr = enable_mmu,所以直接跳過去。
17#
 樓主| 發表於 2009-7-15 17:29:45 | 只看該作者
  1. 155 __enable_mmu:
      I5 z* i& ^* @7 X$ S# }9 q
  2. 170         mov     r5, #(domain_val(DOMAIN_USER, DOMAIN_MANAGER) | \
    $ e% T2 Z2 P( D
  3. 171                       domain_val(DOMAIN_KERNEL, DOMAIN_MANAGER) | \+ q8 N7 o' w* m/ Z5 k
  4. 172                       domain_val(DOMAIN_TABLE, DOMAIN_MANAGER) | \
    " e$ _& ?- E. i2 J
  5. 173                       domain_val(DOMAIN_IO, DOMAIN_CLIENT))# a& @( f% a1 h% B% [8 B# g* u
  6. 174         mcr     p15, 0, r5, c3, c0, 0           @ load domain access register+ f; E; [1 }! W* l2 s4 e- B
  7. 175         mcr     p15, 0, r4, c2, c0, 0           @ load page table pointer
    ! F  ?+ `* x9 B/ \; B3 g" ~
  8. 176         b       __turn_mmu_on. V) h4 a" R. p
  9. 177 ENDPROC(__enable_mmu)
複製代碼
line 170~174,設置好domain access。(domain access可以先當成設置access的權限,或許以後可以寫詳細的文章說明)
" g1 x; D' a+ |: u3 }: M4 i, b# V/ M- qline 175~176,將create好的page table開頭丟給mmu。跳到turn_mmu_on
  1. 191 __turn_mmu_on:
    2 I! y) f, A) I( ?
  2. 192         mov     r0, r0
    7 E* }" L) s6 z/ j
  3. 193         mcr     p15, 0, r0, c1, c0, 0           @ write control reg
    0 F& w4 d. u' O( `+ F
  4. 194         mrc     p15, 0, r3, c0, c0, 0           @ read id reg
    6 U1 t3 T5 p( U
  5. 195         mov     r3, r39 X6 \( }( s5 ]5 E
  6. 196         mov     r3, r3, ^# A0 K! `" I: g# `
  7. 197         mov     pc, r13
    # @" b% z6 g5 T+ V' T; o" A
  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:2 y  G; l! J& Z9 }
  2. 19         .long   __mmap_switched
    7 h7 }2 b- n& w+ y2 {2 j
  3. 20         .long   __data_loc                      @ r4
    8 L2 S2 [9 n+ V
  4. 21         .long   _data                           @ r5
    3 T3 p7 e" s% p9 K$ ?' q# b: I
  5. 22         .long   __bss_start                     @ r6
    ) J4 E7 E' [5 D: k* s
  6. 23         .long   _end                            @ r7
      k7 L2 h3 _5 F: L
  7. 24         .long   processor_id                    @ r45 J0 y" W" G  x: H3 ?( u- L
  8. 25         .long   __machine_arch_type             @ r5
    3 G4 [2 a( l. Q
  9. 26         .long   __atags_pointer                 @ r62 G9 X/ K. v; j; N6 c
  10. 27         .long   cr_alignment                    @ r7, x( v; i7 f6 Q0 d& q8 g+ P5 o: u; [
  11. 28         .long   init_thread_union + THREAD_START_SP @ sp
    & V0 F# j; M$ k5 F
  12. 29  s5 V4 i3 ]5 K/ |6 O9 f* |: @
複製代碼
switch_data的第一行就是 __mmap_switched,所以我們直接看line 39。
18#
 樓主| 發表於 2009-7-15 17:30:00 | 只看該作者
  1. 39 __mmap_switched:
    # r4 S) s! Y$ R! P3 v* A! ^# u
  2. 40         adr     r3, __switch_data + 4
    % M8 k: Y  G1 f$ |( e) k6 p' c
  3. 41
    . e' P8 b& B9 H, G
  4. 42         ldmia   r3!, {r4, r5, r6, r7}
    7 G% M7 w/ ]* u( O, w; n
  5. 43         cmp     r4, r5                          @ Copy data segment if needed* j; `9 O2 n; l2 s: q
  6. 44 1:      cmpne   r5, r6
    0 }/ ?7 J, I& s4 v; ]: k
  7. 45         ldrne   fp, [r4], #4
    , N- z4 b3 T3 y
  8. 46         strne   fp, [r5], #48 l6 S- o  Y; c7 D5 w% e3 i
  9. 47         bne     1b1 o, L* {$ P& g5 \( }+ h
  10. 482 w. K" {/ {* \" Y$ G) j4 ~. U
  11. 49         mov     fp, #0                          @ Clear BSS (and zero fp)
    3 |  L1 i; D* L# W
  12. 50 1:      cmp     r6, r75 E1 X: B- ?% n& T2 n
  13. 51         strcc   fp, [r6],#4
    ' U; G5 Q  u* o; P+ _& ~
  14. 52         bcc     1b
    # }& u) |9 U/ p: j
  15. 537 O4 v( D6 H+ E" U/ B4 v
  16. 54         ldmia   r3, {r4, r5, r6, r7, sp}
    8 F+ c- ], d* j
  17. 55         str     r9, [r4]                        @ Save processor ID
    % A3 M: b, E3 Y8 |4 t7 d
  18. 56         str     r1, [r5]                        @ Save machine type8 p3 D& R) F! M. u; @3 ]) ^
  19. 57         str     r2, [r6]                        @ Save atags pointer
    ' @' ?( b% ~: p% E+ ?) D
  20. 58         bic     r4, r0, #CR_A                   @ Clear 'A' bit6 L6 ?6 |! P6 u& E
  21. 59         stmia   r7, {r0, r4}                    @ Save control register values
    . G6 G; J. c% l; V
  22. 60         b       start_kernel
    + X" l5 y0 P% C6 [0 N
  23. 61 ENDPROC(__mmap_switched)
複製代碼
switch_data的第一行就是 __mmap_switched,所以我們直接看line 39。
1 X" g+ b: k. m( Uline 39,將__data_loc的addr放到r3
0 ^& o3 `. N1 z) R2 p9 N+ Oline 42,從r3的位址,連續讀取四筆資料到r4, r5, r6, r7
: Z, W; ^6 u; P- e# D& Gline 43~47,看看data segment是不是需要搬動。
( J. k' r! r2 Y6 X. s; Gline 49~52, clear BSS。* \" \7 ?4 M( A/ W* t

9 j2 i4 S/ ~( E3 l9 g* t9 p由於linux kernel在進入start_kernel前有一些前提必須要滿足:
7 W: T# v: P8 C' [* b2 e! nr0  = cp#15 control register/ v* f" c3 a, h) L# V+ K
r1  = machine ID" c5 E2 z, S8 m$ c) q$ ^
r2  = atags pointer
* _8 j$ h) [5 ?" s; `, Kr9  = processor ID
$ R% Q, q2 ?5 _& o- y6 q) |. h! C  A1 B$ C! [) j! P' H2 m1 q8 e! `
所以line 54~59就是在做這些準備。. |4 t8 [5 x: a+ N: _$ K8 G; r. e
最後呢? 我們就跳到start_kernel了。(而且還是用b start_kernel,表示我們不會再回來了)# s% k$ Z# {6 R: M9 I8 L! T

. Z. A+ b3 z. n7 t看一下start_kernel()在./init/main.c,終於跳出architecture specific的目錄,表示
5 W! E% w' `3 p" ~, X* j我們真正的開始linux kernel的初始化。
- M% I0 p2 p8 o: Z7 j/ P  q, l+ r像是 shedule init, console init, memory init, irq init等等都在start_kernel裡頭。" U4 H. |. u8 R# T! G
到這邊之後,應該就可以深入linux kernel中,各項比較跟hardware不那麼相關的軟體部分。

評分

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

查看全部評分

您需要登錄後才可以回帖 登錄 | 申請會員

本版積分規則

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

GMT+8, 2024-12-28 05:41 PM , Processed in 0.207012 second(s), 19 queries .

Powered by Discuz! X3.2

© 2001-2013 Comsenz Inc.

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