Chip123 科技應用創新平台

 找回密碼
 申請會員

QQ登錄

只需一步,快速開始

Login

用FB帳號登入

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

trace linux kernel source - ARM - 03

[複製鏈接]
跳轉到指定樓層
1#
發表於 2008-10-7 14:00:18 | 只看該作者 回帖獎勵 |倒序瀏覽 |閱讀模式
到目前為止,我們已經進展到kernel幫自己relocate完,並解將自己解壓縮到一個地方要準備開始執行,那疑問來了?到底是跳到哪裡去了,因為./compressed/head.S最後一行居然是
% \; m4 V) q; x- `『mov pc, r4』, K/ x6 o, g0 o# {9 M
r4只代表了解壓縮完後kernel的位址,那究竟整包kernel編譯的時候,哪個function哪個東西被放在最前面咧?!# m  h7 r8 _  g4 N2 R
& T; }, ~6 r2 r2 f* t$ \3 L9 ]
所以我們又必須開始找於kernel link的時候是怎麼被安排的,有了前面的基礎,我們可以從Makefile知道程式碼如何被編譯。至於link上的細節,例如有那些section和section先後順序等等,可以從 lds 檔來規範。
( a5 I( i0 I7 a+ @+ W. P/ e! h- @: S/ a9 Y: G0 ?
有興趣的人可以看一下 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)
, X$ z8 t6 C! F- k7 P6 ?我們可以發現第一個section是『.text.head』,裡頭的_stext從目前的位置開始放。+ p  Q' ]! x7 i
於是我們曉得只要找到屬於.text.head這個section,並且是_stext這個symbol的程式碼,就是解壓縮完後的第一行程式碼。
  1.      26     .text.head : {4 {" g' `3 ~" y) t" a
  2.      27         _stext = .;! l* x  S" [6 b7 d' {% v
  3.      28         _sinittext = .;* g) ^8 F. g. J& Q
  4.      29         *(.text.head)2 h  x4 x/ h, I; r$ T( o+ {2 s+ J
  5.      30     }
複製代碼
用指令搜尋一下,發現 ./arch/arm/kernel/head.S 裡頭有關鍵字(如下),結果我們從./arch/arm/boot/compressed/head.S跳到了./arch/arm/kernel/head.S
  1.      77     .section ".text.head", "ax"0 X4 S7 y- d6 o
  2.      78     .type   stext, %function
    ; q& n% c5 b! l  R# Q
  3.      79 ENTRY(stext)3 C% a: V3 o# H4 J. x7 E
  4.      80     msr cpsr_c, #PSR_F_BIT | PSR_I_BIT | SVC_MODE @ ensure svc mode
    # K. u: F% K- @2 J. y( v
  5.      81                         @ and irqs disabled9 K+ y$ J+ W) f- V/ a5 p
  6.      82     mrc p15, 0, r9, c0, c0      @ get processor id
    + M9 I8 h& M' k5 g) ?
  7.      83     bl  __lookup_processor_type     @ r5=procinfo r9=cpuid1 o& m4 D3 |& k* W
  8.      84     movs    r10, r5             @ invalid processor (r5=0)?
    1 ^5 O- W0 @( p, p& K8 t
  9.      85     beq __error_p           @ yes, error 'p'$ E" s+ Z! B4 {# G$ D0 o7 O
  10.      86     bl  __lookup_machine_type       @ r5=machinfo
    2 i* N9 ~; y4 Y
  11.      87     movs    r8, r5              @ invalid machine (r5=0)?
    5 b9 L! l' n8 N- i8 p
  12.      88     beq __error_a           @ yes, error 'a', [# c% H4 X0 s5 S- m
  13.      89     bl  __vet_atags
    / ^+ m8 q, P1 [3 L& J6 B) P; N
  14.      90     bl  __create_page_tables
複製代碼
既然找到了檔案,我們又可以開始繼續。
7 T" b. t5 x/ ~$ Y8 @7 z0 S" T
% B5 j( W. p# o$ p/ f看了一下,程式碼多了很多bl的跳躍動作,看來會在各個function跳來跳去。
分享到:  QQ好友和群QQ好友和群 QQ空間QQ空間 騰訊微博騰訊微博 騰訊朋友騰訊朋友
收藏收藏 分享分享 頂 踩 分享分享
2#
 樓主| 發表於 2008-10-9 15:32:16 | 只看該作者
既然跳到真正的kernel開始跑,表示進入重頭戲,在進入kernel之前arm的平台有一些預設的狀況,也就是說arm kernel image會預設目前的cpu和系統的狀況是在某個狀態,這樣對一個剛要跑起來的OS比較決定目前要怎麼boot起來。
1 P7 }. X7 O5 d; E$ s1 F6 E$ k3 }0 i5 e8 n
可以看一下comment,有清楚的描述。這也幫助我們了解為什麼decompresser的程式跑完之後,還要把cache關掉。
  1.      59 /*4 I  F( L( g4 ?+ d/ r
  2.      60  * Kernel startup entry point.
    7 d: V( \1 F4 C+ e# N4 x& T4 Q
  3.      61  * ---------------------------
    . [* z: Q4 {9 M( f1 J
  4.      62  *
    ( d3 k5 u- ?& F4 Q0 d
  5.      63  * This is normally called from the decompressor code.  The requirements
    / `; A$ V3 q; E/ z1 |1 s6 t+ A
  6.      64  * are: MMU = off, D-cache = off, I-cache = dont care, r0 = 0,0 N- |2 P: o. H: I3 u
  7.      65  * r1 = machine nr, r2 = atags pointer.
複製代碼
基於以上的假設,當program counter指到kernel的程式的時候,就會從line 80開始跑。
+ i7 X) P; G& U) d6 ~/ q; mline 80, msr指令會把, operand的值搬到cpsr_c裡面。這是用來確保arm cpu目前是跑在svc mode, irq&fiq都disable。(設成0x1就會disable,definition在./include/asm-arm/ptrace.h)
) ^6 i1 ~" Q4 l" Mline 82, 讀取CPU ID到r9
+ Y  k: R7 |9 c# b( f  R! [7 tline 83, 跳到 __lookup_processor_type 執行(bl會記住返回位址)。
  1.      77     .section ".text.head", "ax"( y+ G; \* V# t" L; x- I- k+ u
  2.      78     .type   stext, %function9 J3 S* j$ A7 R) U
  3.      79 ENTRY(stext)
    5 W% V1 r8 J4 w9 i/ n3 U
  4.      80     msr cpsr_c, #PSR_F_BIT | PSR_I_BIT | SVC_MODE @ ensure svc mode& }3 M) a3 f- X) S$ x& O
  5.      81                         @ and irqs disabled2 p. b' _% S; E: T
  6.      82     mrc p15, 0, r9, c0, c0      @ get processor id
    " g- H& N$ S& L1 `2 u
  7.      83     bl  __lookup_processor_type     @ r5=procinfo r9=cpuid
複製代碼
接著會跳到head-common.S這個檔,
5 Y5 N" p+ f2 @7 t, b% S' Xline 158, (3f表示forware往前找叫做『3』label)將label 3的address放到r3。
4 ]( k/ D+ L" ?: eline 159, 將r3指到的位址的資料,依序放到r7, r6, r5.(ldmda的da是要每次都位址減一次)) ^8 M% `+ j& J8 l/ |
line l60, 161, 利用真實得到位址r3減去取得資料的位址得到一個offset值,這樣可計算出r5, r6真正應該要指到的地方。9 z& r% f" r5 \* W
line 163~169,這邊的程式應該不陌生,就是在各個CPU info裡面找尋對應的structure。找到的話就跳到line 171,返回head.S2 R4 d1 P4 C3 W& g# z* Y
line 170, 找不到的話,r5的processor id就放0x0.表示unknown id。7 q! _1 v) X8 \1 _! E  s
& l" x6 L0 X. c- I8 Y# P1 A
__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
    $ j+ N4 y& r; n3 B0 K
  2.     157 __lookup_processor_type:6 ^# I- v5 S1 X
  3.     158     adr r3, 3f
    + X- M  c9 c# f* p0 z3 u+ w
  4.     159     ldmda   r3, {r5 - r7}
    $ S% k7 s: [" `+ C' ]
  5.     160     sub r3, r3, r7          @ get offset between virt&phys- I' ?7 Q0 q6 L
  6.     161     add r5, r5, r3          @ convert virt addresses to/ ^2 E2 \' ?* o8 j- H1 G# x. n* @
  7.     162     add r6, r6, r3          @ physical address space
    ; F0 X: _9 S4 W# ?
  8.     163 1:  ldmia   r5, {r3, r4}            @ value, mask
    1 x# w$ Z- u: b% Q% c5 N% ^2 Y
  9.     164     and r4, r4, r9          @ mask wanted bits( w" c$ R5 n( i2 F
  10.     165     teq r3, r4; N2 Y3 t4 ?+ ?; h1 j$ U7 u
  11.     166     beq 2f
    . N( \0 p& k& X5 ?0 `% c* p
  12.     167     add r5, r5, #PROC_INFO_SZ       @ sizeof(proc_info_list)
    / t+ n* U+ z4 R6 w
  13.     168     cmp r5, r64 V. y/ c- G4 e- y
  14.     169     blo 1b
    8 ^  c* v2 @8 b
  15.     170     mov r5, #0              @ unknown processor  b& n8 Y! q( U
  16.     171 2:  mov pc, lr
    + F. W* s  s' _' A

  17. % J) D$ N& V* P$ [
  18.     187     .long   __proc_info_begin# r! v7 U& u' e' ]8 J- t2 g
  19.     188     .long   __proc_info_end
    6 v$ e7 [- D- A/ m1 r8 c  f
  20.     189 3:  .long   .! d1 J" j) E' X9 q+ L5 w2 M3 g7 H
  21.     190     .long   __arch_info_begin$ `) g4 T$ E( l! ^; C. F
  22.     191     .long   __arch_info_end
複製代碼
跳了很多檔案,建議指令和object code如何link的概念要有,就會很清楚了。
3#
 樓主| 發表於 2008-10-13 17:20:35 | 只看該作者
我們從 head-common.S返回之後,接著繼續看。, S+ a7 Z: R1 F6 U' @

' @% p' t; @* i; A7 @line 84, movs的意思是說,做mov的動作,並且將指令的S bit設起來,最後的結果也會update CPSR。這個指令執行的過程當中,會根據你要搬動的值去把N and Z flag設好。這個是有助於下個指令做check的動作。2 w9 K% h$ S$ @( D1 t) N0 q, }
line 85, 就是r5 = 0的話,就跳到__error_p去執行。1 R) j2 J5 Q  m& d
line 86, 跳到__lookup_machine_type。原理跟剛剛找proc的資料一樣。
  1.      83         bl      __lookup_processor_type         @ r5=procinfo r9=cpuid
    8 W/ D4 L1 ?$ F5 `) g
  2.      84         movs    r10, r5                         @ invalid processor (r5=0)?
    0 J/ B5 i$ ]( n2 J, n1 n1 |  F
  3.      85         beq     __error_p                       @ yes, error 'p'
    " U, m7 U) e& r7 A
  4.      86         bl      __lookup_machine_type           @ r5=machinfo
複製代碼
看得出來跟proc很像,有個兩個小地方是5 j1 v3 |+ W6 C1 o* s! G) Q. \
+ B* H0 M2 F7 X8 H% Q
1. line 207,用ldmia不是用ldmda。原因就在於存放arch 和 proc info 的位址剛好相反。一個在lable3的上面。一個在的下方。設計上應該是可以做修改的。
1 X/ e5 ~, A( r7 w1 N' X0 P5 n+ F1 x: J
2. arch定義的方式是透過macro,可以先看 ./include/asm-arm/mach/arch.h。裡頭有個 MACHINE_START ,這邊會設好macro,到時候直接使用就可以把arch的info宣告好。 例如 ./arch/arm/mach-omap1/board-generic.c
  1. /* macro */, x( P/ |, c+ f/ A
  2.      50 #define MACHINE_START(_type,_name)                      \
    + P+ J2 F% I* B! M: {% E) G6 I& z
  3.      51 static const struct machine_desc __mach_desc_##_type    \
    1 Y& W/ m2 x! t4 V7 Y! P$ m
  4.      52  __used                                                 \
    % R$ j5 y4 Y. p# N( k
  5.      53  __attribute__((__section__(".arch.info.init"))) = {    \& ~4 P& S4 e6 V7 x! L3 G
  6.      54         .nr             = MACH_TYPE_##_type,            \8 n$ x9 X2 r( _1 _+ ?
  7.      55         .name           = _name,
    + Q% t: k" }; F7 P5 I& _
  8.      56
    ( F: a/ k% p, F! k/ C
  9.      57 #define MACHINE_END                             \/ [" h* p+ b) b" W
  10.      58 };
    " `) L9 u: u3 e! J: d; ]
  11.      /* 用法 */# Z7 z( I( f* M" Z2 l$ a# W% c
  12.      93 MACHINE_START(OMAP_GENERIC, "Generic OMAP1510/1610/1710")
    % Y8 j" N6 i+ ?" c7 e8 U
  13.      94         /* Maintainer: Tony Lindgren <tony@atomide.com> */* J0 Z; J4 p2 |  S0 J- a8 R# T
  14.      95         .phys_io        = 0xfff00000,# m# n( i2 G5 S+ Q! O3 s  t
  15.      96         .io_pg_offst    = ((0xfef00000) >> 18) & 0xfffc,
    - V! o5 p3 w" r) ]. d0 O
  16.      97         .boot_params    = 0x10000100,
    # U% u- k9 _- ~' y) X
  17.      98         .map_io         = omap_generic_map_io,5 |9 W, E2 {# a5 J, s
  18.      99         .init_irq       = omap_generic_init_irq," f, j% h. Z( f- O
  19.     100         .init_machine   = omap_generic_init,
    ) p, |; p: Z& N
  20.     101         .timer          = &omap_timer,1 M( c9 ]8 E& ^: O1 J: w/ _1 m" ?, p* d
  21.     102 MACHINE_END
    & m2 m4 P0 q: H1 p' d; r0 ~
  22. % i" h3 j* n( H) h* Y. p( w4 f
  23.     /* func */
    ) _" P; q( q$ Z6 E' E
  24.     204         .type   __lookup_machine_type, %function& i- g$ m& d" M1 W6 u) B, Q" z. f
  25.     205 __lookup_machine_type:6 t( Z1 Y/ }, v' u
  26.     206         adr     r3, 3b
    - ]- U+ `/ m- m* n3 }1 ^
  27.     207         ldmia   r3, {r4, r5, r6}
    5 u' W) f; i1 L2 B( y( z, S* P
  28.     208         sub     r3, r3, r4                      @ get offset between virt&phys. G! c' ]5 r2 |  {# X, q3 t. B# g
  29.     209         add     r5, r5, r3                      @ convert virt addresses to
    # }4 e  v6 V+ |% k8 [  ]
  30.     210         add     r6, r6, r3                      @ physical address space' D8 X7 F. b, t3 L
  31.     211 1:      ldr     r3, [r5, #MACHINFO_TYPE]        @ get machine type
      c. f* D! r$ e
  32.     212         teq     r3, r1                          @ matches loader number?
    , E/ @4 A1 M0 ^$ e6 C2 l/ t# H
  33.     213         beq     2f                              @ found3 W8 I5 O, ?+ f+ ^6 K; @
  34.     214         add     r5, r5, #SIZEOF_MACHINE_DESC    @ next machine_desc
    % ?+ o" U2 K& g
  35.     215         cmp     r5, r6
    ' m* e# t( v- ?2 P; U* s
  36.     216         blo     1b
    ' W* j# w6 w8 Q
  37.     217         mov     r5, #0                          @ unknown machine
    $ b) P2 }" a& g/ w) p& S4 e
  38.     218 2:      mov     pc, lr
複製代碼
4#
 樓主| 發表於 2008-10-13 17:56:46 | 只看該作者
接著我們又返回到head.S,( C( O9 Y" O* ?* x! [& _5 A
line 87~88也是做check動作。
6 b2 _- k/ ?9 h# A* Wline 89跳到vet_atags。在head-common.S
  1.      87         movs    r8, r5                          @ invalid machine (r5=0)?
    ( A- q% l$ q: o* k& ~1 q
  2.      88         beq     __error_a                       @ yes, error 'a'% D* y' f& T) H% y
  3.      89         bl      __vet_atags( f$ b! `; _& I# Z
  4.      90         bl      __create_page_tables
複製代碼
line 245, tst會去做and動作。並且update flags。這邊的用意是判斷位址是不是aligned。7 C+ m/ u+ X- ?' J9 ~/ U' L: p
line 246, 沒有aligned跳到label 1,就返回了。) k) d  C4 |9 q( u
line 248~250, 讀取atags所在的address裡頭的值到r5,看看是否不等於ATAG_CORE_SIZE,不等於的話也是返回。
  V. o( M8 [1 q6 `7 ]7 _7 _line 251~254, 判斷一下第一個達到的atag pointer是不是等於ATAG_CORE。如果正確的話,等一下要讀取atag的資料,才會正確。
+ N3 \3 y9 ?% u% _(atag是由bootloader帶給linux kernel的東西,用來告知booting所需要知道的參數。例如螢幕寬度,記憶體大小等等)
  1.      14 #define ATAG_CORE 0x544100011 Z. d* A, V9 S* Q" e2 t. Y: b
  2.      15 #define ATAG_CORE_SIZE ((2*4 + 3*4) >> 2)
    ; ~* y% v' Y8 m

  3. # C9 \7 u5 q9 B# U  h
  4.     243         .type   __vet_atags, %function' g6 H5 q4 h( T. I# V, Z" V
  5.     244 __vet_atags:3 d4 d' j5 V2 W2 v6 X% Q
  6.     245         tst     r2, #0x3                        @ aligned?
    0 M5 d  h. Z* h- B7 h; U" {" D
  7.     246         bne     1f
      s) r$ e; P2 I: a( u) x
  8.     247% I8 A" n2 ]# V
  9.     248         ldr     r5, [r2, #0]                    @ is first tag ATAG_CORE?
    & q$ W$ _* t+ [! Z
  10.     249         subs    r5, r5, #ATAG_CORE_SIZE
    - C; l6 y, q0 ^) a- }) u
  11.     250         bne     1f. \, O+ I" n3 g5 g# z
  12.     251         ldr     r5, [r2, #4]6 ~6 ?6 z3 a+ T* f
  13.     252         ldr     r6, =ATAG_CORE5 V$ \: d7 J: \
  14.     253         cmp     r5, r6
    4 @2 X4 {4 `+ T6 Z+ Z% V# C
  15.     254         bne     1f6 J7 l$ ^3 Q% X& `2 M4 Y& Q% }
  16.     2555 [; Z% i5 x' F1 Q" I& y
  17.     256         mov     pc, lr                          @ atag pointer is ok# w& V8 U, U/ O* [/ S2 i
  18.     2577 i2 x& ]  Q( m2 d: A9 N: `" P
  19.     258 1:      mov     r2, #08 Q5 Q) \+ ]9 I- j1 q
  20.     259         mov     pc, lr
複製代碼
接著我們又跳回去head.S。  9 s1 O  W. Z1 g7 y* R2 T1 Y( g+ f
line 90,又跳到 __create_page_tables。   (很累人....應該會死不少腦細胞)1 p$ [# }5 U1 Q
哇!page table?!!如雷貫耳的東西,不知道會怎麼做。剛剛偷看了一下,@@還蠻長了,先到這邊好了,下次在繼續寫。
5#
 樓主| 發表於 2008-10-14 12:13:51 | 只看該作者
由於code看起來似乎越來越難解釋,會需要在檔案之間跳來跳去。建議一些基礎的東西可以再多複習幾次(其實是在說我自己 ),閱讀source code上收穫會比較多。例如:( `) |) J8 V$ p' \5 ~+ V
2 c( r- z" N  `( `0 ~
1. arm instruction set - 這個最好每個指令的意思都大略看過一次,行有餘力多看幾個版本,armv4, v5 or v6。9 e( l1 {  c: b- ]5 P
2. compiler & assembler & linker - toolchain工具做的事情和功能,大致的流程和功能要有概念。
/ J# O) r" M* ^. ~* h3. Makefile & link script - 這兩個功能和撰寫的方式要有簡單的概念。/ L# P( K5 l2 V; X# c# S
6 U! }0 k  W1 D. [3 `. ]8 X
以上1是非常重要的重點,2&3只要有大致上的概念就可以,因為trace code的時候,有時需要跳到Makeflie&Link script去看最後object code編排的位址。
5 _4 g: e- g+ M5 u; P4 ], o) d2 P6 H; R
由於我們trace到了page table這個關鍵字,在開始之前,稍微簡短的解釋,為了幫助了解,儘量用易懂的概念講,有些用詞可能會和一些真實狀況不同,但是懂了之後,應該會有能力分辨,至於正式而學術上解說,很多書上應該都有,或是google一下就很多啦∼
6#
 樓主| 發表於 2008-10-14 12:14:47 | 只看該作者
page table本身是很抽象的東西,尤其是對所謂的 user-mode application programmer 來說,大部分的狀況它是不需要被考慮到的部份,但是他的產生卻對os和driver帶來了很多影響。我們從一個小小的疑問出發。
- O0 o" b. A$ p& W& V- [2 c. C) h, Z3 B3 o
『產生page table到底是要給誰用的?』
4 `  Y) \' h/ O4 ]/ y  W( Z( S4 g5 P9 }- f- D6 a
其實真正作用在它上面的H/W是MMU,一旦CPU啟用了MMU,當cpu嘗試去記憶體讀取一個operand的時候,cpu內部打出去的位址訊號都會先送到mmu,mmu會拿著這個位址去對照page table,看看這個位址是不是被轉換到另外一個位置(還會確認讀寫權力),最後才會到真正的位址去讀寫。- Z+ Z  e+ p/ a9 |5 G

+ {8 @. h* y- g! z3 _這樣來看CPU其實一開始打出去的位址訊號其實不是最後可以拿到資料的位址,我們稱為virtual address(va),mmu打出來的位址才能真正拿到資料,所以我們把mmu打出去的位址稱為physical address(pa)。那用來查詢這個位址對照關係的表格,就是page table。
# D$ o6 }  n  y1 ]4 r
, y. x% d" u6 N8 F到這邊我們有一個簡單的概念。來想像一個小問題,一個普通的周邊(例如你的顯示卡),因為他並不具有MMU功能,hw只看得懂pa,但是CPU卻是作用在va,假如我想去對hw的控制暫存器做讀寫,到底要用va還是pa?
7#
 樓主| 發表於 2008-10-14 12:15:51 | 只看該作者
這時,寫driver的人就必須要小心,設定給硬體看的位址必須要先轉成pa(像是設定DMA),給CPU執行的程式碼要使用va,這對一開始嘗試寫driver但是又不瞭解va pa之間的差別的人,常常產生很多疑問。
  e8 ^& f6 V% }% [% ~- g  G; |& v5 c; M* W3 I" Q
現在我們回頭想想OS,既然我們跑到create page table,可以預期的是他想要將MMU打開,因此希望預先建立起一個page table,讓MMU知道目前os想規劃的位址對應和讀取權力是如何被安排的。所以os必須考慮到現在的系統究竟是長怎樣?應該要如何被安排?是不是有那些要被保護?好吧∼因為我們完全對os不了解,也不知道該安排什麼,只能祈禱在trace code完後,可以找到這些問題的答案,或者發現一些沒想到的問題。/ O, G/ \$ j  I3 c
8 l9 ~. i+ D( l
知道了page table的大致上的功能,下篇就可以專心的研究這個table的長相,和它想規劃出的系統模樣。$ i* U( H: q% d
$ t: K; w# I! R9 Q2 p, c7 X) ?/ L4 W
p.s. 字數限制好像變短了。   (看來很難寫)
8#
 樓主| 發表於 2008-10-14 13:56:17 | 只看該作者
由於字數限制挑整,用詞儘量精簡,以便可以貼必要source。另外,以後寫到一段落,考慮收集成一篇完整的文章,這樣應就不會因為用回覆的方式,造成閱讀斷斷續續的問題。希望有人對文章呈現方式有想法的話,可以跟我講。
9#
 樓主| 發表於 2008-10-14 13:57:39 | 只看該作者
現在,讓我們跳入create_page_tables吧∼6 V% e' E  ^5 [4 M2 `( d7 ~
line 216,會跳到pgtbl的macro去執行,其實只是載入一個位址。
( y. M# f! w$ X% v' L+ Z( O  Z* G2 n
只是這個位址因為你硬體規劃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 */
    + v2 M. p7 U- _. O0 a6 l
  2.      95 textofs-y       := 0x00008000
    7 C5 m. f% B8 l7 {! i7 h- Q4 `
  3.     152 TEXT_OFFSET := $(textofs-y)
複製代碼
  1. /* include/asm-arm/arch-omap/memory.h */' J" e$ n+ |6 |2 b. u6 `
  2.      40 #define PHYS_OFFSET             UL(0x10000000)* d. x0 m/ J2 ?! k$ T* e
  3. 8 o' o4 U  R" n2 r& \
  4.      /* arch/arm/kernel/head.S */2 V0 s. R7 W* [7 T
  5.      29 #define KERNEL_RAM_VADDR        (PAGE_OFFSET + TEXT_OFFSET)
    7 i4 p: V( j* c! F& e8 Z
  6.      30 #define KERNEL_RAM_PADDR        (PHYS_OFFSET + TEXT_OFFSET)
    6 t9 n* Z) y9 L7 N3 T* M
  7. ' t+ X# H5 E7 M/ w0 q1 X
  8.      47         .macro  pgtbl, rd( q4 k4 K* a: K$ C
  9.      48         ldr     \rd, =(KERNEL_RAM_PADDR - 0x4000)  r! c: R* s0 M8 d2 D% X' \1 j
  10.      49         .endm4 d7 I1 ?6 [  z' U8 i
  11. ( j& \- S. j" |- S
  12.     216         pgtbl   r4                              @ page table address
複製代碼
10#
 樓主| 發表於 2008-10-14 14:16:53 | 只看該作者
得到pg table的開始位置之後,當然就是初始化囉。6 i4 s, X5 U& U6 y
line 221, 將pg table的base addr放到r0.
. X7 Z% {0 {! T) F) V  a& d/ kline 223, 將pg table的end addr放到r6.: j$ v& I' [! V& U) X% {9 K, X
line 224~228, 反覆地將0x0寫到pg table的區段裡頭,每個loop寫16bytes. (4x4),直到碰到end,結果就是把它全部clear成0x0.
  1.     221         mov     r0, r4
    4 ^* k8 O& k2 g" }
  2.     222         mov     r3, #05 v0 R) d# m: `8 G8 |- ?$ V! y+ ?3 x& y
  3.     223         add     r6, r0, #0x4000
    & ?! T* V+ L2 n) S. f; Q# {
  4.     224 1:      str     r3, [r0], #4
    8 c& @2 m7 C! V0 z+ A1 m
  5.     225         str     r3, [r0], #42 v( R0 p# G1 h, L& k8 [
  6.     226         str     r3, [r0], #4
    5 t9 S$ l: z0 _* _
  7.     227         str     r3, [r0], #4
    / x8 B& c2 ~. z& {; B, t" C
  8.     228         teq     r0, r6
    / B( B, o" b( {3 V
  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 | 只看該作者
問題怎麼填值??- O: n7 q1 o0 [
拿出ARM的手冊,翻到MMU章節。一看發現page有很多種,還得分first level和second level。 # {% W+ [2 b" f) l# l0 M
& I) M2 f7 T- k* e) s$ y
念書時的印象,是從1st level在去查2nd level,先看怎麼設定1st level (不知以前老師有沒有亂教)& G: c. h+ \! V+ T0 i) m
1. [31:20]存著section base addr& k/ |! B1 V# `! l. H
2. [19:2]存著mmu flags0 A, M$ ]- O/ Q& S8 `
3. [1:0]用來辨別這是存放哪種page, 有四種:6 d2 b$ _- i3 o7 I+ U) C$ |
   a. fault (00) b. coarse page (01) c. section (1st level) (10) d. fine page (11)6 o5 J7 u6 _8 X& \% C7 g8 E
4. pg tabel資料要存放到 [31:14] = translation base, [13:2] = table index , [1:0] = 0b00 的位址8 j7 X" ]5 Z8 D6 q6 }

9 E* _; k' ]' I; m  E/ Q6 Y" P來看code是怎麼設定。' [/ o2 N# A& E9 u7 y3 h, M$ b3 F

6 C# j( W8 h7 E+ }line 239, 將pc的值往右shift 20次放到r6.這是取得前20高位元的資料,有點像是 1.。1 e$ h) q+ d1 B: {
line 240, 將r6往左shift 20次之後,和r7做or的動作,剛好也是 2.提到的mmu flags和1.處理好的資料做整理。
5 j! l/ e8 s9 D所以前面兩個做完,就完成了bit[31:2]。
% b6 B/ G: Z0 S4 Aline 241, 將r3的資料寫到r4+(r6<<0x2)的地方去,剛好是4.提到的位址。(lucky)
  1.     239         mov     r6, pc, lsr #20
    0 V; a; a, ^- G6 B# k5 |
  2.     240         orr     r3, r7, r6, lsl #20
    1 y0 p0 P/ V  g5 U9 [* i0 U" D. ^. w
  3.     241         str     r3, [r4, r6, lsl #2]
複製代碼
p.s. 用pc值剛好可以算出當前的page。
12#
 樓主| 發表於 2008-10-22 19:47:03 | 只看該作者
最近又被釘上了,開始忙碌,大概沒太多時間貼文∼
" _" W8 ?2 d+ Z* o
- B+ B' \4 }8 w+ @5 A上篇已經將pc所屬的page table entry(pte)設定好,接著繼續看$ @/ t5 H( ~( Y
line 247, 248, 將KERNEL_START的位址往右shift 18算出pte的offset,(等於line239&241,239&241是先shift right 20然後shift left 2),並將剛剛r3的值設給pte。『!』的意思是會把address存到r0。
0 v/ M1 w2 m5 }5 I. P: ?& u* z3 b- \4 b. Y4 |7 Y: @- J3 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! F# y) q+ I' y7 c: m% h; G5 {$ V
  2.     248         str     r3, [r0, #(KERNEL_START & 0x00f00000) >> 18]! 9 k. K, u  g. N; q8 S2 _# r! G
  3.     249         ldr     r6, =(KERNEL_END - 1)
    0 k0 s& r& i. D+ A+ o4 |# H
  4.     250         add     r0, r0, #4
    : H& M/ `6 h' D/ M' Z6 W: x
  5.     251         add     r6, r4, r6, lsr #18
    9 q5 k8 D. L% }% P* M
  6.     252 1:      cmp     r0, r6( t+ S8 h' a9 g7 M: \1 `
  7.     253         add     r3, r3, #1 << 207 L9 E; }' j( A5 `+ e
  8.     254         strls   r3, [r0], #4. j4 z3 @1 k4 A& r3 B- }
  9.     255         bls     1b
複製代碼
13#
 樓主| 發表於 2008-10-22 20:24:58 | 只看該作者
line279,PAGE_OFFSET是規範RAM mapping完後的virtual address,我們將這個位址所屬的pte算出來放到r0。! f$ ?/ |2 c( Y' F
line 280~283,將要 map 的physical address的方式算出來放到r6。
8 i& @' a* n& k# W, Wline 284,最後將結果存到r0所指到的pte。( r" d; z3 r- \9 X7 n: k
# Q3 i& o# M: N. e$ G5 C6 z
以上三個動作,就是做好 ram 起頭的位址一開始map,還沒做完整塊map。由於我們目前的設定方式每塊是1MB,所以這邊是將RAM開始的1MB做map。
' l% g2 D% b4 J; l0 I
) P- S- S% D3 K+ z! O# r$ Oline 327,返回,結束一開始的create page table的動作,我們可以看出其實並沒有做完整的初始化page table,所以之後應該會有其他page table的細節。
  1.     279         add     r0, r4, #PAGE_OFFSET >> 18* R2 g, j( e, M5 U6 C3 q" D
  2.     280         orr     r6, r7, #(PHYS_OFFSET & 0xff000000)0 B" _3 l, z0 Z
  3.     281         .if     (PHYS_OFFSET & 0x00f00000)
    / ~) a! _' \2 z! K; F
  4.     282         orr     r6, r6, #(PHYS_OFFSET & 0x00f00000)- o5 k( v& ~: ^# q
  5.     283         .endif! ^; Q+ Q4 j4 V0 ]5 F
  6.     284         str     r6, [r0]( K  y1 E- ?: T: _, x. @0 N4 v' j& F
  7.     327         mov     pc, lr
複製代碼
附帶一提,我們這邊省略了一些用ifdef包起來的程式碼,像是一開始會印一些output message的程式碼(line286~326)。
14#
 樓主| 發表於 2008-10-22 20:37:08 | 只看該作者
自create page table返回後,我們偷偷看一下接下來的程式碼,8 S  r4 r) V! I: \  Q) b" ~! H
line 99, 將switch_data擺到r130 X/ M2 G0 H2 h) }& E
line 101, 將enable_mmu擺到lr
& m- c: ?9 X: rline 102, 將pc改跳到r10+PROCINFO_INITFUNC的地方去' \, K/ w7 c! ]7 l
4 V  m8 W" P% r5 t3 T
其實這邊有點玄機,switch_data和enable_mmu都是function,結果位址都只是被存起來,並沒有直接跳過去執行。其實,雖然程式碼是順序switch_data->enable_mmu->proc init function,但其實執行的順序會是 procinfo 的init function-> enable_mmu -> switch_data 。至於,為什麼要這樣寫的原因?就不清楚了,也還沒仔細推敲過。
, q( E, @1 E- {# r8 D1 z: U/ H0 z$ _% i; s2 y' y; C6 w( A% Z% V
switch_data最後就會跳到大家都很熟悉的start_kernel(). 詳細要賣個關子,最近會忙一下,得要過一陣子才能貼文∼  
  1.      99         ldr     r13, __switch_data              @ address to jump to after. B; @; o  M1 \" K9 I8 z
  2.     100                                                 @ mmu has been enabled
    + r. d2 [4 [0 g# F! Q
  3.     101         adr     lr, __enable_mmu                @ return (PIC) address
    5 I% C# C" U+ s  e
  4.     102         add     pc, r10, #PROCINFO_INITFUNC
複製代碼
15#
 樓主| 發表於 2009-7-4 01:09:36 | 只看該作者
老店重新開張~
; {% r* F% I$ |6 L. A; a
, u) o+ e3 c" H花了一些時間把舊的貼文整理到一個blog
0 j- x; ^" u2 |) A/ A& n有把一些敘述修改過
) n5 t1 w( d0 G  }3 v: Y希望會比較容易集中閱讀
% t5 ^5 [$ K# u. `) I) p: G目前因為某些敘述不容易
  r. p8 g( O. f: Q4 g還是比較偏向筆記式而且用字不夠精確7 C+ R3 R+ }/ o9 e# v& m
希望之後能夠慢慢有系統地整理" b! V( k, ]7 t; r7 X- [
大家有興趣的話% S1 [! |1 r. ]6 B- b
可以來看看和討論
% K+ D; z2 T% \, c% [" vhttp://gogojesseco.blogspot.com/
  w3 S7 \4 R- {$ h, U2 D! \0 K( B% S6 u* W2 R! Q, |9 r' ^
以後可能會採取  先在chip123貼新文章
9 \1 D. j) b. C慢慢整理到blog上的方式3 B! x( K  R- Y0 h4 e. O
因為chip123比較方便討論 =). a* L9 ]: q' I, c1 a8 y; A
blog編輯修改起來比較方便
1 |# I+ [& E* {3 N6 s閱讀也比較集中   大家可以在這邊看到討論
8 }) K9 p9 ~4 k% \* }: Z0 ]" r: k- f1 C然後在blog看到完整的文章 (類似BBS精華區的感覺)

評分

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

查看全部評分

16#
 樓主| 發表於 2009-7-15 17:07:03 | 只看該作者
隔了很長一段時間沒update
5 \* w' Y' o% k7 M6 J/ i6 o% d之前程式碼走到 ./arch/arm/kernel/head.S 的 line 99 附近
  1.      99         ldr     r13, __switch_data              @ address to jump to after& S. L7 m3 N$ A$ r
  2.     100                                                 @ mmu has been enabled- h; I9 i& ]( x; b; ^
  3.     101         adr     lr, __enable_mmu                @ return (PIC) address. `! Z% Z7 T) v% h. G; F: N
  4.     102         add     pc, r10, #PROCINFO_INITFUNC
複製代碼
line 99, 將__switch_data放到r13。(留作之後用)
% A( L0 `5 x. a9 b  ~line 101, 將__enable_mmu的addr放到lr。(留作之後用)2 ^  t2 c2 l2 f6 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" o5 X; P; c$ }% V
  2. 374 __arm926_setup:# q& V& h7 U- r# M/ T8 @! L
  3. 375         mov     r0, #0( Z0 i2 X  g+ x0 P, B
  4. 376         mcr     p15, 0, r0, c7, c7              @ invalidate I,D caches on v4( C0 @- }( E" ~- Y
  5. 377         mcr     p15, 0, r0, c7, c10, 4          @ drain write buffer on v4
    4 m( c8 _" q% k5 V2 v: g% _/ ~
  6. 378 #ifdef CONFIG_MMU
    . J! I3 B0 x9 P
  7. 379         mcr     p15, 0, r0, c8, c7              @ invalidate I,D TLBs on v4
    - s/ P: U1 I6 t5 V6 Z; r  o
  8. 380 #endif8 @  ~8 G+ [; j, n3 \7 X' P
  9. $ {* V5 [) k' S$ m* p
  10. 388         adr     r5, arm926_crval' u+ ]. P$ ~, T$ z6 u- C1 `
  11. 389         ldmia   r5, {r5, r6}; l8 ], |" x5 Y
  12. 390         mrc     p15, 0, r0, c1, c0              @ get control register v4, T- G4 ^4 Q8 ~# y: ?! Y2 C0 M3 O( G
  13. 391         bic     r0, r0, r5- o( v0 |. z2 ^
  14. 392         orr     r0, r0, r6
    . L9 F2 l" e$ ~+ \" O, q; f2 R6 |, D

  15. 1 @7 W( o( q  }% T1 w8 X) ?
  16. 396         mov     pc, lr6 V3 J% m6 t+ `5 X9 ?1 A0 S
  17. 397         .size   __arm926_setup, . - __arm926_setup
複製代碼
這邊的程式碼就跟CPU有很大的相依性,3 v9 o' T2 ~' K# l
line 375~380, 主要就是invalidate CPU的I&D cache和清空write buffer。+ ~: m; p# q, B3 Q  O
line 388~392, 把cp15的設定讀出來,並且將一些預設狀態做好運算放到r0。(預設值從arm926_crval這邊可以取得。)8 ~0 m' _$ ^" J7 \2 m
line 396, 直接把pc跳到lr,因為我們之前已經將lr = enable_mmu,所以直接跳過去。
17#
 樓主| 發表於 2009-7-15 17:29:45 | 只看該作者
  1. 155 __enable_mmu:
    ( V6 y2 M: \5 m0 a
  2. 170         mov     r5, #(domain_val(DOMAIN_USER, DOMAIN_MANAGER) | \
    . k) K( D$ E6 F7 f: t! U( B
  3. 171                       domain_val(DOMAIN_KERNEL, DOMAIN_MANAGER) | \
    + J' b( j" L3 C- H) P& b7 H9 o, v
  4. 172                       domain_val(DOMAIN_TABLE, DOMAIN_MANAGER) | \
    5 a4 W! t" e1 m3 _  ?
  5. 173                       domain_val(DOMAIN_IO, DOMAIN_CLIENT))
    , ?: e1 h" h6 s# h; R- p) M9 o
  6. 174         mcr     p15, 0, r5, c3, c0, 0           @ load domain access register/ D7 Q0 A' P$ @( K" C( ?8 L7 P' }
  7. 175         mcr     p15, 0, r4, c2, c0, 0           @ load page table pointer, F" S: ^" x7 L" C) P
  8. 176         b       __turn_mmu_on
    + E4 d. B$ v* W5 Q0 F
  9. 177 ENDPROC(__enable_mmu)
複製代碼
line 170~174,設置好domain access。(domain access可以先當成設置access的權限,或許以後可以寫詳細的文章說明)
9 E  Y3 `$ c" }/ Nline 175~176,將create好的page table開頭丟給mmu。跳到turn_mmu_on
  1. 191 __turn_mmu_on:2 _- K8 q5 s& W6 ?" S$ G; Y/ L7 N! |
  2. 192         mov     r0, r0
    % l4 f; ~+ o) Q# z; O- X. o
  3. 193         mcr     p15, 0, r0, c1, c0, 0           @ write control reg. M/ x6 x+ Q7 y$ _0 n$ r5 _4 Z* l2 O
  4. 194         mrc     p15, 0, r3, c0, c0, 0           @ read id reg
    . ]9 {& w0 c3 _3 w  A: E
  5. 195         mov     r3, r3, o& P% q% E/ Q! k
  6. 196         mov     r3, r3) r. P( d! \, @/ ^" w
  7. 197         mov     pc, r13
    : E9 T, t& X: D( h
  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:
    8 ?' @5 U- m, U$ T
  2. 19         .long   __mmap_switched
    6 G* |+ m$ d1 [3 ]
  3. 20         .long   __data_loc                      @ r4' E) J; }7 B5 b2 H' r( i3 g
  4. 21         .long   _data                           @ r5
    : d4 n# l% y8 p8 q5 m
  5. 22         .long   __bss_start                     @ r6
    & Z7 q& D9 @* \. Q' B/ O
  6. 23         .long   _end                            @ r7
    6 A4 }7 K3 x; y; V. R
  7. 24         .long   processor_id                    @ r4
    , ?) @7 f0 T. L+ I8 }
  8. 25         .long   __machine_arch_type             @ r5/ _* F2 l* v9 \1 |" M3 `$ z: t2 e& o
  9. 26         .long   __atags_pointer                 @ r60 Q- }3 Y+ p7 F8 e4 [
  10. 27         .long   cr_alignment                    @ r77 x4 e* O: T9 Y+ |& R# V
  11. 28         .long   init_thread_union + THREAD_START_SP @ sp
      Q8 R9 u7 a3 l5 j
  12. 29
    3 M1 m8 V) a5 \
複製代碼
switch_data的第一行就是 __mmap_switched,所以我們直接看line 39。
18#
 樓主| 發表於 2009-7-15 17:30:00 | 只看該作者
  1. 39 __mmap_switched:
    % \2 b; g& u/ s: ~; i- r6 L* z
  2. 40         adr     r3, __switch_data + 4
    , {' `6 I3 E: w5 l  P. C( ^) W$ R
  3. 41
    ( t6 @$ X; f% H: K
  4. 42         ldmia   r3!, {r4, r5, r6, r7}" X* v, x$ F( B
  5. 43         cmp     r4, r5                          @ Copy data segment if needed
    9 O/ K( l% x; h7 ?
  6. 44 1:      cmpne   r5, r6
    - F8 d; F3 V" R! R) p: t
  7. 45         ldrne   fp, [r4], #4+ A* n5 ^- J7 [* P+ n! a
  8. 46         strne   fp, [r5], #4' q& ]2 M7 K+ w% ~. o% I, O5 C' Y
  9. 47         bne     1b
    ' x4 X) A3 e! V5 L
  10. 48
    6 H. r0 x7 n7 J+ o  a
  11. 49         mov     fp, #0                          @ Clear BSS (and zero fp)+ r  ]  e9 j; f4 \
  12. 50 1:      cmp     r6, r75 N6 i4 V6 `6 ^" S) |2 u
  13. 51         strcc   fp, [r6],#4$ j9 a, E# `& b, J' }
  14. 52         bcc     1b
    4 z. n8 Z! {) y7 b  v0 z
  15. 53
    , b+ B/ Q& d5 K9 h
  16. 54         ldmia   r3, {r4, r5, r6, r7, sp}9 q, d# J& x* Z5 d. z( f' m
  17. 55         str     r9, [r4]                        @ Save processor ID
    4 \/ s! X+ B  Q7 _" _) o- l, E
  18. 56         str     r1, [r5]                        @ Save machine type# g4 m! N$ i; t- J! n/ h9 Y; N
  19. 57         str     r2, [r6]                        @ Save atags pointer
    ; M# _2 O9 G: Q0 ]( A- b8 r2 R
  20. 58         bic     r4, r0, #CR_A                   @ Clear 'A' bit, y+ J0 N: I% L" R
  21. 59         stmia   r7, {r0, r4}                    @ Save control register values
    ' X) ]* q% m7 Q1 D% M  j
  22. 60         b       start_kernel" h6 d: E2 @, {2 ^! i  U
  23. 61 ENDPROC(__mmap_switched)
複製代碼
switch_data的第一行就是 __mmap_switched,所以我們直接看line 39。
* a9 ?$ c! X" [8 p9 s" Pline 39,將__data_loc的addr放到r3
3 b0 R! [3 y- W/ V9 lline 42,從r3的位址,連續讀取四筆資料到r4, r5, r6, r7. m+ k3 Z  `0 C' j  M% J
line 43~47,看看data segment是不是需要搬動。% Z3 }# _: z# ]
line 49~52, clear BSS。+ z) f$ h( A- f8 l3 e+ R

5 _5 t" E; i; o; f  E5 s, G由於linux kernel在進入start_kernel前有一些前提必須要滿足:; o. [" U" a7 o
r0  = cp#15 control register. O5 O0 D* u$ o% H2 A& ^
r1  = machine ID; J* i" y% D# Q/ p% u3 ^
r2  = atags pointer
1 Z. V: [, a: m6 Er9  = processor ID/ \* m2 u7 J' Z

6 a( M0 h9 s7 b% W: F所以line 54~59就是在做這些準備。! l# P/ o2 t2 {" ^
最後呢? 我們就跳到start_kernel了。(而且還是用b start_kernel,表示我們不會再回來了)' L. `6 q! G7 J8 B) M% J
: ]+ o7 p9 B7 C8 ~6 h+ K% g! @! ]2 @
看一下start_kernel()在./init/main.c,終於跳出architecture specific的目錄,表示
& \5 j7 f& }+ [- g: B% ~# {. u我們真正的開始linux kernel的初始化。
4 K0 o6 n- }$ X1 ^1 ^+ b7 t& R像是 shedule init, console init, memory init, irq init等等都在start_kernel裡頭。; M1 S4 U. t" K8 @5 }8 ?
到這邊之後,應該就可以深入linux kernel中,各項比較跟hardware不那麼相關的軟體部分。

評分

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

查看全部評分

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

本版積分規則

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

GMT+8, 2024-6-14 02:55 AM , Processed in 0.142018 second(s), 18 queries .

Powered by Discuz! X3.2

© 2001-2013 Comsenz Inc.

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