Chip123 科技應用創新平台

 找回密碼
 申請會員

QQ登錄

只需一步,快速開始

Login

用FB帳號登入

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

trace linux kernel source - ARM - 03

[複製鏈接]
跳轉到指定樓層
1#
發表於 2008-10-7 14:00:18 | 只看該作者 回帖獎勵 |倒序瀏覽 |閱讀模式
到目前為止,我們已經進展到kernel幫自己relocate完,並解將自己解壓縮到一個地方要準備開始執行,那疑問來了?到底是跳到哪裡去了,因為./compressed/head.S最後一行居然是
" Q- u4 p& y3 ]『mov pc, r4』" I# B8 i1 f0 ]3 E7 F3 o; x) ?
r4只代表了解壓縮完後kernel的位址,那究竟整包kernel編譯的時候,哪個function哪個東西被放在最前面咧?!; f; ?% c0 \( |- F! v; E' v

) n; v6 b. N3 m& T  @  R$ |所以我們又必須開始找於kernel link的時候是怎麼被安排的,有了前面的基礎,我們可以從Makefile知道程式碼如何被編譯。至於link上的細節,例如有那些section和section先後順序等等,可以從 lds 檔來規範。- L, O) X' R* S! V/ s( f

! {) ~, w0 B. k* s" e2 l- S- z有興趣的人可以看一下 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)  ]- w" ^% P' [/ I& T9 [
我們可以發現第一個section是『.text.head』,裡頭的_stext從目前的位置開始放。. f3 E1 a5 {6 m4 Y. i% V" c
於是我們曉得只要找到屬於.text.head這個section,並且是_stext這個symbol的程式碼,就是解壓縮完後的第一行程式碼。
  1.      26     .text.head : {
    ! e* P4 u- W6 ~: S
  2.      27         _stext = .;) p$ ?9 ?/ f# g( b) A; i
  3.      28         _sinittext = .;
    " k& {& |! ?" Z! N! n7 ^
  4.      29         *(.text.head)
    + n: S9 M  o) ?5 Q9 n7 S
  5.      30     }
複製代碼
用指令搜尋一下,發現 ./arch/arm/kernel/head.S 裡頭有關鍵字(如下),結果我們從./arch/arm/boot/compressed/head.S跳到了./arch/arm/kernel/head.S
  1.      77     .section ".text.head", "ax"
    3 Y3 T6 m/ M. Z( k! p8 l
  2.      78     .type   stext, %function! C$ ^6 d6 D8 Q3 q
  3.      79 ENTRY(stext)
    9 u, I" b( ?4 U3 C1 O: A6 A
  4.      80     msr cpsr_c, #PSR_F_BIT | PSR_I_BIT | SVC_MODE @ ensure svc mode
    : _0 b2 V- \: n; l5 E! m
  5.      81                         @ and irqs disabled7 G) M) h- ]/ M8 V5 m, M& W
  6.      82     mrc p15, 0, r9, c0, c0      @ get processor id
    ' I0 B5 u7 N. d
  7.      83     bl  __lookup_processor_type     @ r5=procinfo r9=cpuid
    9 \3 n) F2 G1 [
  8.      84     movs    r10, r5             @ invalid processor (r5=0)?
    : g/ x  q: j; p0 V; w/ p7 g9 l* {
  9.      85     beq __error_p           @ yes, error 'p'4 c8 `! d) _' d
  10.      86     bl  __lookup_machine_type       @ r5=machinfo/ I- B) o! F% I
  11.      87     movs    r8, r5              @ invalid machine (r5=0)?
    , h8 P( n2 I6 T4 L
  12.      88     beq __error_a           @ yes, error 'a'
    9 ~5 r3 L/ P) `! v  ?) ?5 u
  13.      89     bl  __vet_atags2 x$ m. V8 Y- w7 Z$ s) ?. \
  14.      90     bl  __create_page_tables
複製代碼
既然找到了檔案,我們又可以開始繼續。
6 i7 Y6 }# q" h1 b5 y
* e: ]) C" S, K  g看了一下,程式碼多了很多bl的跳躍動作,看來會在各個function跳來跳去。
分享到:  QQ好友和群QQ好友和群 QQ空間QQ空間 騰訊微博騰訊微博 騰訊朋友騰訊朋友
收藏收藏 分享分享 頂 踩 分享分享
2#
 樓主| 發表於 2008-10-9 15:32:16 | 只看該作者
既然跳到真正的kernel開始跑,表示進入重頭戲,在進入kernel之前arm的平台有一些預設的狀況,也就是說arm kernel image會預設目前的cpu和系統的狀況是在某個狀態,這樣對一個剛要跑起來的OS比較決定目前要怎麼boot起來。
. W' W0 ^- g1 t' G: t8 }! f! c, l& x( B. R
可以看一下comment,有清楚的描述。這也幫助我們了解為什麼decompresser的程式跑完之後,還要把cache關掉。
  1.      59 /** ~' a# j- W$ O) m' L
  2.      60  * Kernel startup entry point.
    ) C& a& K/ P% Z! o" H
  3.      61  * ---------------------------
    5 Y1 \, [4 `  [. |
  4.      62  *
    & y1 E+ D& Q( a- ?
  5.      63  * This is normally called from the decompressor code.  The requirements7 i' i% D; i. X' k' B) p
  6.      64  * are: MMU = off, D-cache = off, I-cache = dont care, r0 = 0,  l8 U3 v' P, D  h7 S8 Y7 f+ [8 s
  7.      65  * r1 = machine nr, r2 = atags pointer.
複製代碼
基於以上的假設,當program counter指到kernel的程式的時候,就會從line 80開始跑。
# j2 q# j: E1 b4 |1 E9 ^1 p% nline 80, msr指令會把, operand的值搬到cpsr_c裡面。這是用來確保arm cpu目前是跑在svc mode, irq&fiq都disable。(設成0x1就會disable,definition在./include/asm-arm/ptrace.h)4 `9 a: E/ b/ \. c9 j& \) a
line 82, 讀取CPU ID到r9/ b, _# z6 Y: A1 _( o
line 83, 跳到 __lookup_processor_type 執行(bl會記住返回位址)。
  1.      77     .section ".text.head", "ax"
    4 l  O9 x3 r+ p3 L4 I7 w+ j
  2.      78     .type   stext, %function
    ! E! i4 ~  M/ }5 F
  3.      79 ENTRY(stext)
    9 N* J3 H$ c' C0 G9 i' ~& g5 _
  4.      80     msr cpsr_c, #PSR_F_BIT | PSR_I_BIT | SVC_MODE @ ensure svc mode
    ' `1 \" C1 j% e7 s: e
  5.      81                         @ and irqs disabled7 M* n+ Z, F# A
  6.      82     mrc p15, 0, r9, c0, c0      @ get processor id
    ( a1 W) |8 ?- `, k7 h* ]5 F; {
  7.      83     bl  __lookup_processor_type     @ r5=procinfo r9=cpuid
複製代碼
接著會跳到head-common.S這個檔,3 B6 K* D8 N% L- A, ]
line 158, (3f表示forware往前找叫做『3』label)將label 3的address放到r3。' i* |# m+ v- C. ?2 D9 w) m4 R9 @" s; M
line 159, 將r3指到的位址的資料,依序放到r7, r6, r5.(ldmda的da是要每次都位址減一次)
' b3 d7 N+ N" q- l% m- U5 {# _line l60, 161, 利用真實得到位址r3減去取得資料的位址得到一個offset值,這樣可計算出r5, r6真正應該要指到的地方。5 S$ {  U. C: a1 I1 W  W/ @
line 163~169,這邊的程式應該不陌生,就是在各個CPU info裡面找尋對應的structure。找到的話就跳到line 171,返回head.S
- U5 K' J' v. Y' W! aline 170, 找不到的話,r5的processor id就放0x0.表示unknown id。
& K! k1 ?' W9 z, ^& s- f( `, P- ?$ V# ^: @
__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/ u+ t& d2 j9 L
  2.     157 __lookup_processor_type:
    . _: r2 j3 N9 r2 N. b7 V* Q& |2 x4 A5 u
  3.     158     adr r3, 3f4 Z. H# [0 @2 J. |/ A
  4.     159     ldmda   r3, {r5 - r7}" `' v8 \: f6 R/ {" p" n* h
  5.     160     sub r3, r3, r7          @ get offset between virt&phys
    ! }- e' _. ~# G1 _' F: ]3 @
  6.     161     add r5, r5, r3          @ convert virt addresses to
    # o2 A4 m: _0 x; h7 m/ F; M5 O
  7.     162     add r6, r6, r3          @ physical address space& a: ]: e8 b7 G
  8.     163 1:  ldmia   r5, {r3, r4}            @ value, mask
    3 q7 h$ w$ ^. B4 w7 `  F
  9.     164     and r4, r4, r9          @ mask wanted bits) x. G+ y7 ~2 `  ]6 l$ p
  10.     165     teq r3, r4
    7 }' @. N% b4 p6 B
  11.     166     beq 2f
    + R" v4 q* @0 {5 a  t9 R+ Q1 v7 g
  12.     167     add r5, r5, #PROC_INFO_SZ       @ sizeof(proc_info_list)
    8 ~! y6 X  X1 f8 D/ P3 k) u. r
  13.     168     cmp r5, r64 E! A& B" o) h8 s& V
  14.     169     blo 1b
    - _& j! m9 i: }. _1 n7 m' h* H( ^
  15.     170     mov r5, #0              @ unknown processor" M* i- T# _. |. ~0 m
  16.     171 2:  mov pc, lr9 l) @% Z; j7 L( q: U) o
  17. * e! b5 E* e/ |/ N+ c  Z
  18.     187     .long   __proc_info_begin1 A8 F8 V3 B- I; w0 O+ s7 `
  19.     188     .long   __proc_info_end
    % t. C- ]0 ?) s3 \
  20.     189 3:  .long   .# }- f5 K3 {  F9 ~# J8 d
  21.     190     .long   __arch_info_begin  j6 e6 |0 q6 A) w  k% ?
  22.     191     .long   __arch_info_end
複製代碼
跳了很多檔案,建議指令和object code如何link的概念要有,就會很清楚了。
3#
 樓主| 發表於 2008-10-13 17:20:35 | 只看該作者
我們從 head-common.S返回之後,接著繼續看。; j7 X% f7 r1 ~5 `, w
* d( i# X/ Z* H& m' U; [
line 84, movs的意思是說,做mov的動作,並且將指令的S bit設起來,最後的結果也會update CPSR。這個指令執行的過程當中,會根據你要搬動的值去把N and Z flag設好。這個是有助於下個指令做check的動作。( ]) ?6 A" v, G# P6 l  N* ~
line 85, 就是r5 = 0的話,就跳到__error_p去執行。$ \2 E$ D# f8 j5 J' N
line 86, 跳到__lookup_machine_type。原理跟剛剛找proc的資料一樣。
  1.      83         bl      __lookup_processor_type         @ r5=procinfo r9=cpuid
    ; B) L" M8 H4 @- n9 f
  2.      84         movs    r10, r5                         @ invalid processor (r5=0)?
    ! @+ ^  l1 `! d; S4 X
  3.      85         beq     __error_p                       @ yes, error 'p'9 g2 O) T8 G' k! y1 {9 U9 p2 _0 E
  4.      86         bl      __lookup_machine_type           @ r5=machinfo
複製代碼
看得出來跟proc很像,有個兩個小地方是9 ?' t* P% g2 L/ ?/ B

0 F' q) v: |% j6 q8 [1. line 207,用ldmia不是用ldmda。原因就在於存放arch 和 proc info 的位址剛好相反。一個在lable3的上面。一個在的下方。設計上應該是可以做修改的。' g# l- O& I' C' N

" B1 L3 N2 h1 b  i2. arch定義的方式是透過macro,可以先看 ./include/asm-arm/mach/arch.h。裡頭有個 MACHINE_START ,這邊會設好macro,到時候直接使用就可以把arch的info宣告好。 例如 ./arch/arm/mach-omap1/board-generic.c
  1. /* macro */" _% L. ~4 C3 }' W# E
  2.      50 #define MACHINE_START(_type,_name)                      \
      g1 P( t3 F7 d+ u" @6 X
  3.      51 static const struct machine_desc __mach_desc_##_type    \% |: Z: p( t$ C$ L6 {
  4.      52  __used                                                 \& d4 {# g. x5 V) `1 D# H
  5.      53  __attribute__((__section__(".arch.info.init"))) = {    \  O. C3 c: [- W/ j' l" @& V
  6.      54         .nr             = MACH_TYPE_##_type,            \
    6 n& l. X% l" S/ h
  7.      55         .name           = _name,
    # d6 N/ F# X; ^) |- t8 i
  8.      568 X0 H+ E; \2 m
  9.      57 #define MACHINE_END                             \7 o, u: S& [" Y  Q- p
  10.      58 };
    $ f, r! J- I: _- w; f
  11.      /* 用法 */
    $ ~' J: d7 N6 k. o
  12.      93 MACHINE_START(OMAP_GENERIC, "Generic OMAP1510/1610/1710")/ u2 t: W; G) K, F) K* u9 |1 y1 P
  13.      94         /* Maintainer: Tony Lindgren <tony@atomide.com> */
    9 g9 h) i' U$ s, |# o
  14.      95         .phys_io        = 0xfff00000,! X1 d2 U; `" C; R
  15.      96         .io_pg_offst    = ((0xfef00000) >> 18) & 0xfffc,
    ; A6 s# B( E4 C# A
  16.      97         .boot_params    = 0x10000100,% [# {$ b7 h# ^/ `
  17.      98         .map_io         = omap_generic_map_io,' y5 I" S3 h) u  C# f
  18.      99         .init_irq       = omap_generic_init_irq,
      c( q. S+ Q' G+ k& c& F& ]$ q2 c: v
  19.     100         .init_machine   = omap_generic_init,) S9 C/ Y" O+ k. @( w8 z3 \
  20.     101         .timer          = &omap_timer,& ^" [8 |, a5 g( @( l
  21.     102 MACHINE_END4 U, R( D+ ?6 i$ w) Q. H( k

  22. : a: W& ^9 C1 J: C! W
  23.     /* func */
    ( G# Z6 r% e" T
  24.     204         .type   __lookup_machine_type, %function* u8 v9 I( ^, s4 p  ]. Y! m
  25.     205 __lookup_machine_type:
    3 }. J) X/ u/ S2 f2 U  p
  26.     206         adr     r3, 3b. ~+ C/ J2 Y) l5 `$ o
  27.     207         ldmia   r3, {r4, r5, r6}
    # H5 j2 N; I5 S
  28.     208         sub     r3, r3, r4                      @ get offset between virt&phys$ D7 i; _$ V3 a/ Z  r
  29.     209         add     r5, r5, r3                      @ convert virt addresses to$ v  w* d/ I1 J) p$ ~# i
  30.     210         add     r6, r6, r3                      @ physical address space; H/ U' p4 y5 \0 ?5 K  h4 V9 v
  31.     211 1:      ldr     r3, [r5, #MACHINFO_TYPE]        @ get machine type1 R/ M- h' w- q; G. \; n4 v% `
  32.     212         teq     r3, r1                          @ matches loader number?6 n4 ^2 F0 S1 p5 |: ~
  33.     213         beq     2f                              @ found
    , O: b/ j7 p8 x; G
  34.     214         add     r5, r5, #SIZEOF_MACHINE_DESC    @ next machine_desc& b$ N: M' z/ p" f$ S% D7 ^. r* B
  35.     215         cmp     r5, r6; b' m2 w/ q9 u/ \
  36.     216         blo     1b$ Z( W! N( o; u$ l/ A9 ^3 ?
  37.     217         mov     r5, #0                          @ unknown machine
    3 N! r/ X) U% _/ f+ h+ g$ H
  38.     218 2:      mov     pc, lr
複製代碼
4#
 樓主| 發表於 2008-10-13 17:56:46 | 只看該作者
接著我們又返回到head.S,+ [& T! I9 o8 @8 M# K; _* e$ B
line 87~88也是做check動作。- x: A& L6 y. W- a* a! g
line 89跳到vet_atags。在head-common.S
  1.      87         movs    r8, r5                          @ invalid machine (r5=0)?3 H; D9 g+ @  _( R. I. q- }
  2.      88         beq     __error_a                       @ yes, error 'a': s/ i, j& `3 z1 K3 m. y
  3.      89         bl      __vet_atags7 x' }0 }! t# @# z3 R( Z4 G
  4.      90         bl      __create_page_tables
複製代碼
line 245, tst會去做and動作。並且update flags。這邊的用意是判斷位址是不是aligned。
' x! z$ f6 |6 C, E& k; a1 @line 246, 沒有aligned跳到label 1,就返回了。) a2 r5 S$ c% X* l6 o; J4 f
line 248~250, 讀取atags所在的address裡頭的值到r5,看看是否不等於ATAG_CORE_SIZE,不等於的話也是返回。
9 C, {2 Q0 B( E: Z; Tline 251~254, 判斷一下第一個達到的atag pointer是不是等於ATAG_CORE。如果正確的話,等一下要讀取atag的資料,才會正確。9 O1 P7 h: |6 y1 w" v
(atag是由bootloader帶給linux kernel的東西,用來告知booting所需要知道的參數。例如螢幕寬度,記憶體大小等等)
  1.      14 #define ATAG_CORE 0x54410001
    ) B, H4 w% @8 x' _2 ]# z2 L( t% x
  2.      15 #define ATAG_CORE_SIZE ((2*4 + 3*4) >> 2)- r: ]: X) `# r) K" n* `

  3. . p+ i% J) G5 s' e" ]
  4.     243         .type   __vet_atags, %function; D$ k* |. M) B; m$ G: o! h
  5.     244 __vet_atags:; V! w: H8 ~/ y# E) z
  6.     245         tst     r2, #0x3                        @ aligned?. i' n/ y+ t! l' F
  7.     246         bne     1f
    ' E- C! P: d( d# j0 Z9 K9 e
  8.     2471 y' M: Q1 H+ |( O3 F1 j1 m2 B% W
  9.     248         ldr     r5, [r2, #0]                    @ is first tag ATAG_CORE?
    ' R- m9 {& ^1 l5 N8 r/ d
  10.     249         subs    r5, r5, #ATAG_CORE_SIZE( l) \4 D+ ~& C0 w
  11.     250         bne     1f
    " j0 X) W6 c3 A5 s1 T5 p9 |
  12.     251         ldr     r5, [r2, #4]
    , }: I* f5 u3 ~  p/ f+ ^* g- g
  13.     252         ldr     r6, =ATAG_CORE
    5 g, @/ E, M* T" U" @; U6 e. l
  14.     253         cmp     r5, r65 u- F! ~4 R- R, t2 s
  15.     254         bne     1f
    / f0 w" }+ x5 y* D* D# Q' T
  16.     255: f' _( a+ x/ ~1 G0 s1 e$ {
  17.     256         mov     pc, lr                          @ atag pointer is ok
    4 Z( t' M, c$ H0 i
  18.     2576 @4 N' Q! T2 x: a$ F: g2 j4 `. q
  19.     258 1:      mov     r2, #0' J. W, i# [4 j: _1 E+ o+ C' ]2 B% S7 c
  20.     259         mov     pc, lr
複製代碼
接著我們又跳回去head.S。  # Z- w2 V; q" [# k( ?
line 90,又跳到 __create_page_tables。   (很累人....應該會死不少腦細胞)3 K* T! q3 h3 j3 l
哇!page table?!!如雷貫耳的東西,不知道會怎麼做。剛剛偷看了一下,@@還蠻長了,先到這邊好了,下次在繼續寫。
5#
 樓主| 發表於 2008-10-14 12:13:51 | 只看該作者
由於code看起來似乎越來越難解釋,會需要在檔案之間跳來跳去。建議一些基礎的東西可以再多複習幾次(其實是在說我自己 ),閱讀source code上收穫會比較多。例如:& o+ q4 ]# v6 B& A* E& A$ F8 ?
3 {" U3 l. W0 M* |; B
1. arm instruction set - 這個最好每個指令的意思都大略看過一次,行有餘力多看幾個版本,armv4, v5 or v6。8 t# ~$ Z" @2 [1 }. o1 _) A: [
2. compiler & assembler & linker - toolchain工具做的事情和功能,大致的流程和功能要有概念。
4 l: a0 d' e& E$ x) t! c3. Makefile & link script - 這兩個功能和撰寫的方式要有簡單的概念。$ }; z$ n0 [& u9 J

8 R: U( w/ }+ n1 v; W! }以上1是非常重要的重點,2&3只要有大致上的概念就可以,因為trace code的時候,有時需要跳到Makeflie&Link script去看最後object code編排的位址。
: T' u8 S2 h7 M" f8 X. |) `/ p1 r  {
( x; y# N' b" Q6 w, d' ?由於我們trace到了page table這個關鍵字,在開始之前,稍微簡短的解釋,為了幫助了解,儘量用易懂的概念講,有些用詞可能會和一些真實狀況不同,但是懂了之後,應該會有能力分辨,至於正式而學術上解說,很多書上應該都有,或是google一下就很多啦∼
6#
 樓主| 發表於 2008-10-14 12:14:47 | 只看該作者
page table本身是很抽象的東西,尤其是對所謂的 user-mode application programmer 來說,大部分的狀況它是不需要被考慮到的部份,但是他的產生卻對os和driver帶來了很多影響。我們從一個小小的疑問出發。
) ~* b3 r. d: p$ M
) I8 d5 g' w" S2 D『產生page table到底是要給誰用的?』
+ o2 {2 T) ~( X; `" Y! L  d
3 i- w0 b) ^% B$ G. K# B2 D2 o2 I- {其實真正作用在它上面的H/W是MMU,一旦CPU啟用了MMU,當cpu嘗試去記憶體讀取一個operand的時候,cpu內部打出去的位址訊號都會先送到mmu,mmu會拿著這個位址去對照page table,看看這個位址是不是被轉換到另外一個位置(還會確認讀寫權力),最後才會到真正的位址去讀寫。
  |2 {& V1 G1 H+ O, E! e- _
. a8 C" Z7 y0 x) e這樣來看CPU其實一開始打出去的位址訊號其實不是最後可以拿到資料的位址,我們稱為virtual address(va),mmu打出來的位址才能真正拿到資料,所以我們把mmu打出去的位址稱為physical address(pa)。那用來查詢這個位址對照關係的表格,就是page table。
9 P3 y) r8 u& D" R
- a) g' x; C& R. N1 l: O到這邊我們有一個簡單的概念。來想像一個小問題,一個普通的周邊(例如你的顯示卡),因為他並不具有MMU功能,hw只看得懂pa,但是CPU卻是作用在va,假如我想去對hw的控制暫存器做讀寫,到底要用va還是pa?
7#
 樓主| 發表於 2008-10-14 12:15:51 | 只看該作者
這時,寫driver的人就必須要小心,設定給硬體看的位址必須要先轉成pa(像是設定DMA),給CPU執行的程式碼要使用va,這對一開始嘗試寫driver但是又不瞭解va pa之間的差別的人,常常產生很多疑問。7 h1 l2 k2 l7 \: w
. O0 k* [( H: [0 v1 I; h& i/ z- r
現在我們回頭想想OS,既然我們跑到create page table,可以預期的是他想要將MMU打開,因此希望預先建立起一個page table,讓MMU知道目前os想規劃的位址對應和讀取權力是如何被安排的。所以os必須考慮到現在的系統究竟是長怎樣?應該要如何被安排?是不是有那些要被保護?好吧∼因為我們完全對os不了解,也不知道該安排什麼,只能祈禱在trace code完後,可以找到這些問題的答案,或者發現一些沒想到的問題。
! W9 k8 J# k- L! d8 r- J/ {* g
0 k0 M* c. l7 N/ y' |& l3 V5 a) g7 }$ x6 L知道了page table的大致上的功能,下篇就可以專心的研究這個table的長相,和它想規劃出的系統模樣。: G8 K& T$ Q1 e; J

3 H7 q# O$ [" \2 L5 np.s. 字數限制好像變短了。   (看來很難寫)
8#
 樓主| 發表於 2008-10-14 13:56:17 | 只看該作者
由於字數限制挑整,用詞儘量精簡,以便可以貼必要source。另外,以後寫到一段落,考慮收集成一篇完整的文章,這樣應就不會因為用回覆的方式,造成閱讀斷斷續續的問題。希望有人對文章呈現方式有想法的話,可以跟我講。
9#
 樓主| 發表於 2008-10-14 13:57:39 | 只看該作者
現在,讓我們跳入create_page_tables吧∼, Q& s) I1 U9 _- m4 j. x
line 216,會跳到pgtbl的macro去執行,其實只是載入一個位址。
% p- n# S3 S7 y( _
* Y/ F2 r! ?. U  L: f只是這個位址因為你硬體規劃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 *// W0 r4 V6 a3 d6 G
  2.      95 textofs-y       := 0x000080002 n8 s- e5 }4 ?3 \/ T+ ^% W
  3.     152 TEXT_OFFSET := $(textofs-y)
複製代碼
  1. /* include/asm-arm/arch-omap/memory.h */5 P- Y% `. @+ o, g# ^
  2.      40 #define PHYS_OFFSET             UL(0x10000000)
    & |1 q" x& o7 w+ b7 A
  3. 2 w/ t3 `2 F4 f! P- M, J5 N) T: T, C
  4.      /* arch/arm/kernel/head.S */
    - i# [3 n8 N# C8 d
  5.      29 #define KERNEL_RAM_VADDR        (PAGE_OFFSET + TEXT_OFFSET)
    ( D+ W$ N) Q0 M5 a* d
  6.      30 #define KERNEL_RAM_PADDR        (PHYS_OFFSET + TEXT_OFFSET)% S$ T- n9 u% Y8 H4 u

  7. : r- h8 h: {% M& j/ V
  8.      47         .macro  pgtbl, rd
    , v1 j4 F: }0 h! p5 |
  9.      48         ldr     \rd, =(KERNEL_RAM_PADDR - 0x4000)
    " g6 v4 {0 N) N8 A/ [, l9 K( q
  10.      49         .endm
    1 M( R/ ^$ a: p2 L" K% g

  11.   U2 l5 ]( |; n
  12.     216         pgtbl   r4                              @ page table address
複製代碼
10#
 樓主| 發表於 2008-10-14 14:16:53 | 只看該作者
得到pg table的開始位置之後,當然就是初始化囉。2 S- i. W" D( e2 E8 R5 D
line 221, 將pg table的base addr放到r0.0 M) J0 ?& y& G. x
line 223, 將pg table的end addr放到r6.
* l: R) W- |: m4 Mline 224~228, 反覆地將0x0寫到pg table的區段裡頭,每個loop寫16bytes. (4x4),直到碰到end,結果就是把它全部clear成0x0.
  1.     221         mov     r0, r4
      }3 w8 ^. F% h
  2.     222         mov     r3, #0( _, s. n5 E0 ?1 R( C
  3.     223         add     r6, r0, #0x4000
    6 ?. O, q& K% K/ t1 J
  4.     224 1:      str     r3, [r0], #4. W* U, n* z4 ?. h6 o
  5.     225         str     r3, [r0], #4
    $ N( X0 b9 N, R* E8 t2 z
  6.     226         str     r3, [r0], #4$ k; y5 i2 i  @7 W  |
  7.     227         str     r3, [r0], #49 O, x$ c* d2 v
  8.     228         teq     r0, r6
    # V$ {( M( c, N- K7 \
  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+ G+ Y# ]5 L拿出ARM的手冊,翻到MMU章節。一看發現page有很多種,還得分first level和second level。 2 H# @* [+ [; h0 Z( g+ t8 j+ T* J

( P" }1 a. O- T" t6 y9 ]. H念書時的印象,是從1st level在去查2nd level,先看怎麼設定1st level (不知以前老師有沒有亂教)
! w# i1 O! Y0 z1. [31:20]存著section base addr( Y4 _$ ]0 |6 g0 h+ b
2. [19:2]存著mmu flags
! E7 s' f% h* d! p; S) ]# f# u; D3. [1:0]用來辨別這是存放哪種page, 有四種:# d4 Q! p0 r- K
   a. fault (00) b. coarse page (01) c. section (1st level) (10) d. fine page (11)
  o# Z! B8 I/ T4 ]# T7 j* t$ o4. pg tabel資料要存放到 [31:14] = translation base, [13:2] = table index , [1:0] = 0b00 的位址
# R1 Q( e6 K. J3 Q: t
2 L- s0 `4 n* W) h! u來看code是怎麼設定。2 H# [& V7 a% t9 d/ L, |

5 s. H5 [% }9 H( k) Z! K1 ]line 239, 將pc的值往右shift 20次放到r6.這是取得前20高位元的資料,有點像是 1.。
2 w3 p) }6 D( Q- X! e# z# Cline 240, 將r6往左shift 20次之後,和r7做or的動作,剛好也是 2.提到的mmu flags和1.處理好的資料做整理。
  H# L) R, M$ a# b+ A所以前面兩個做完,就完成了bit[31:2]。
/ B% v% w. h6 ^: rline 241, 將r3的資料寫到r4+(r6<<0x2)的地方去,剛好是4.提到的位址。(lucky)
  1.     239         mov     r6, pc, lsr #20% z0 A. j8 O$ {3 d0 \2 ], o  v' @% {
  2.     240         orr     r3, r7, r6, lsl #20+ l% q4 n, z4 [: b: S5 H
  3.     241         str     r3, [r4, r6, lsl #2]
複製代碼
p.s. 用pc值剛好可以算出當前的page。
12#
 樓主| 發表於 2008-10-22 19:47:03 | 只看該作者
最近又被釘上了,開始忙碌,大概沒太多時間貼文∼
6 I6 Q3 F4 d% }5 A3 U. ]- g2 n" x% h# v0 i6 X
上篇已經將pc所屬的page table entry(pte)設定好,接著繼續看1 G$ ]1 U) z  P, I
line 247, 248, 將KERNEL_START的位址往右shift 18算出pte的offset,(等於line239&241,239&241是先shift right 20然後shift left 2),並將剛剛r3的值設給pte。『!』的意思是會把address存到r0。% d1 p; H6 s, K

0 y6 m8 J0 X/ Lline 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) >> 187 F6 g9 ~+ D# M
  2.     248         str     r3, [r0, #(KERNEL_START & 0x00f00000) >> 18]!
    ) Z5 d. b6 A9 z
  3.     249         ldr     r6, =(KERNEL_END - 1)
    + Z5 E3 s/ v6 j5 o) W6 U
  4.     250         add     r0, r0, #4
    3 N. d$ H3 \$ H" \! M9 F) @
  5.     251         add     r6, r4, r6, lsr #189 ]: H6 V4 F8 a& ~. J
  6.     252 1:      cmp     r0, r6# x9 ~# @$ G, H) u) q1 z
  7.     253         add     r3, r3, #1 << 20
      q& ]% ]- _/ [' P- W! T
  8.     254         strls   r3, [r0], #4$ K+ }9 u: C( u, ^) n; |/ s
  9.     255         bls     1b
複製代碼
13#
 樓主| 發表於 2008-10-22 20:24:58 | 只看該作者
line279,PAGE_OFFSET是規範RAM mapping完後的virtual address,我們將這個位址所屬的pte算出來放到r0。7 t6 s2 U- J1 u0 d
line 280~283,將要 map 的physical address的方式算出來放到r6。
* h2 g: F+ s. g1 N5 Qline 284,最後將結果存到r0所指到的pte。6 y. E! F/ Y- k

3 O/ g0 |) }0 f3 m以上三個動作,就是做好 ram 起頭的位址一開始map,還沒做完整塊map。由於我們目前的設定方式每塊是1MB,所以這邊是將RAM開始的1MB做map。- ~! U6 x% N0 O/ ^$ N

6 _' Z2 U7 g5 @- E+ t4 J) Tline 327,返回,結束一開始的create page table的動作,我們可以看出其實並沒有做完整的初始化page table,所以之後應該會有其他page table的細節。
  1.     279         add     r0, r4, #PAGE_OFFSET >> 18, Z1 r- A8 _; T  L/ r
  2.     280         orr     r6, r7, #(PHYS_OFFSET & 0xff000000)* T9 \4 J* }$ _" ^4 t# v/ d9 L
  3.     281         .if     (PHYS_OFFSET & 0x00f00000)+ y1 R! M. I  [9 F
  4.     282         orr     r6, r6, #(PHYS_OFFSET & 0x00f00000)
    2 T5 |; u6 a. t4 w3 V
  5.     283         .endif
    " C+ U1 ~& u- t# h- B# x6 x
  6.     284         str     r6, [r0]
    . P/ w1 G7 p, M/ Z6 Y  |# v
  7.     327         mov     pc, lr
複製代碼
附帶一提,我們這邊省略了一些用ifdef包起來的程式碼,像是一開始會印一些output message的程式碼(line286~326)。
14#
 樓主| 發表於 2008-10-22 20:37:08 | 只看該作者
自create page table返回後,我們偷偷看一下接下來的程式碼,
# }' a- ~6 C$ w; A) n6 d. pline 99, 將switch_data擺到r13
; j; e3 s4 B5 S! ?! N: z- `line 101, 將enable_mmu擺到lr
. w) H3 Y' @3 L5 x& f  o; ?line 102, 將pc改跳到r10+PROCINFO_INITFUNC的地方去. z+ L# Y9 x; F4 K3 w
' r- @8 Y( l. W) P; J  U0 V+ U2 I
其實這邊有點玄機,switch_data和enable_mmu都是function,結果位址都只是被存起來,並沒有直接跳過去執行。其實,雖然程式碼是順序switch_data->enable_mmu->proc init function,但其實執行的順序會是 procinfo 的init function-> enable_mmu -> switch_data 。至於,為什麼要這樣寫的原因?就不清楚了,也還沒仔細推敲過。
% @) i$ @. q8 A! a' z& i
* n( _; G6 _, oswitch_data最後就會跳到大家都很熟悉的start_kernel(). 詳細要賣個關子,最近會忙一下,得要過一陣子才能貼文∼  
  1.      99         ldr     r13, __switch_data              @ address to jump to after
    6 {' n$ h& Y! f" Q' v
  2.     100                                                 @ mmu has been enabled
    & h9 J8 Q, r, p2 O
  3.     101         adr     lr, __enable_mmu                @ return (PIC) address
    1 [" F/ {& n$ z$ z% \4 u
  4.     102         add     pc, r10, #PROCINFO_INITFUNC
複製代碼
15#
 樓主| 發表於 2009-7-4 01:09:36 | 只看該作者
老店重新開張~  L, x  p# e9 y0 e! d
4 p6 r: ]# Q7 ]4 h
花了一些時間把舊的貼文整理到一個blog
+ Y, d4 q* H) N有把一些敘述修改過
0 _: U2 \; {4 ^2 [希望會比較容易集中閱讀: C3 [, A& ?$ Y; J  h3 V  T
目前因為某些敘述不容易0 t0 M  q9 _0 @3 f6 L! a
還是比較偏向筆記式而且用字不夠精確
' b! Q$ h  ~4 y8 u% u希望之後能夠慢慢有系統地整理
) _! z4 {+ d  S% w5 u2 _大家有興趣的話
; [  H) J) s/ ~/ @+ R可以來看看和討論 $ G& G/ i" X* j, ]
http://gogojesseco.blogspot.com/# n) p+ ~7 P, t( f
- d$ V7 H$ B/ w  E) i6 c$ V
以後可能會採取  先在chip123貼新文章
, D7 ~5 [" ^6 i2 u5 J慢慢整理到blog上的方式5 C1 R0 S( d$ q* p# w, C5 [% L7 N9 m( P; M
因為chip123比較方便討論 =)
6 ], T" d# B  A) p- {blog編輯修改起來比較方便
0 W, K* V: L# v  O- ^& \1 h閱讀也比較集中   大家可以在這邊看到討論
/ i1 ?0 ^5 y! c: k然後在blog看到完整的文章 (類似BBS精華區的感覺)

評分

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

查看全部評分

16#
 樓主| 發表於 2009-7-15 17:07:03 | 只看該作者
隔了很長一段時間沒update/ }7 ?! a* g, C& s
之前程式碼走到 ./arch/arm/kernel/head.S 的 line 99 附近
  1.      99         ldr     r13, __switch_data              @ address to jump to after' O  b$ p+ X9 D
  2.     100                                                 @ mmu has been enabled
    $ {/ K. h- z, I& _
  3.     101         adr     lr, __enable_mmu                @ return (PIC) address
    + S' O$ m/ ?8 W+ S7 v" b+ G
  4.     102         add     pc, r10, #PROCINFO_INITFUNC
複製代碼
line 99, 將__switch_data放到r13。(留作之後用)
, t3 ]& e, ]; d; vline 101, 將__enable_mmu的addr放到lr。(留作之後用)  Z- K: ~/ G; B2 O
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
    4 _! d# E7 E/ i$ M" C
  2. 374 __arm926_setup:
    ' N& A8 H3 {3 n: H
  3. 375         mov     r0, #06 m- E& l7 ]7 d' V, M& W
  4. 376         mcr     p15, 0, r0, c7, c7              @ invalidate I,D caches on v45 a! ?3 c7 K! O) c2 p# P
  5. 377         mcr     p15, 0, r0, c7, c10, 4          @ drain write buffer on v4# ]1 K' c. ]1 B3 k/ ?3 D
  6. 378 #ifdef CONFIG_MMU3 _1 O* V. x, q
  7. 379         mcr     p15, 0, r0, c8, c7              @ invalidate I,D TLBs on v4
    0 G- G, d# B1 V- @
  8. 380 #endif9 X9 w( q- P4 A1 n1 }7 q

  9. " j' n( \8 F8 n7 U/ b5 f& D* D
  10. 388         adr     r5, arm926_crval
    . t% c0 J# A  ^6 w3 S
  11. 389         ldmia   r5, {r5, r6}" F. t- ^# M/ ]! h- A- d- z
  12. 390         mrc     p15, 0, r0, c1, c0              @ get control register v4) A. i; t3 g* u6 Q( {
  13. 391         bic     r0, r0, r5
    , S: ^4 }+ ]9 V3 ^
  14. 392         orr     r0, r0, r6! d* k" |8 J2 v9 t8 W: s
  15. / h  O, A! {( ?% f# M3 t5 f  i
  16. 396         mov     pc, lr
    / P5 n; c* x' I$ o! m: _
  17. 397         .size   __arm926_setup, . - __arm926_setup
複製代碼
這邊的程式碼就跟CPU有很大的相依性,
, j1 l) X4 h% R4 q) U) b2 \& vline 375~380, 主要就是invalidate CPU的I&D cache和清空write buffer。
$ U8 c" `  J9 x: u" M" A1 ]line 388~392, 把cp15的設定讀出來,並且將一些預設狀態做好運算放到r0。(預設值從arm926_crval這邊可以取得。)* N0 l& K" M& R. l$ O% Q
line 396, 直接把pc跳到lr,因為我們之前已經將lr = enable_mmu,所以直接跳過去。
17#
 樓主| 發表於 2009-7-15 17:29:45 | 只看該作者
  1. 155 __enable_mmu:3 T0 l" }# F4 F
  2. 170         mov     r5, #(domain_val(DOMAIN_USER, DOMAIN_MANAGER) | \7 d( v' o* ^  T+ c
  3. 171                       domain_val(DOMAIN_KERNEL, DOMAIN_MANAGER) | \
    2 I- Q  I5 I4 Y: u
  4. 172                       domain_val(DOMAIN_TABLE, DOMAIN_MANAGER) | \6 Q8 h: W7 |. j0 S$ i; W0 r
  5. 173                       domain_val(DOMAIN_IO, DOMAIN_CLIENT))
    2 U5 B: H3 S6 S2 r
  6. 174         mcr     p15, 0, r5, c3, c0, 0           @ load domain access register
    ! [- v& b/ K6 D
  7. 175         mcr     p15, 0, r4, c2, c0, 0           @ load page table pointer
    ' Y5 h/ N% N- K( P6 C
  8. 176         b       __turn_mmu_on3 X5 E1 m( t" y
  9. 177 ENDPROC(__enable_mmu)
複製代碼
line 170~174,設置好domain access。(domain access可以先當成設置access的權限,或許以後可以寫詳細的文章說明)5 Y' j7 Y1 p' n" a5 b
line 175~176,將create好的page table開頭丟給mmu。跳到turn_mmu_on
  1. 191 __turn_mmu_on:6 L' I3 l/ z& w9 i, ^# E9 T
  2. 192         mov     r0, r0
    : s' J7 S+ W! M9 u$ K3 t; m
  3. 193         mcr     p15, 0, r0, c1, c0, 0           @ write control reg
    , P9 S# C5 n, d3 a, ^6 Q
  4. 194         mrc     p15, 0, r3, c0, c0, 0           @ read id reg
    - ]! O/ w+ `' ]$ I( |$ P
  5. 195         mov     r3, r3
    $ ~# B' N6 Z+ F, g2 P& C9 b
  6. 196         mov     r3, r3# e7 s' z. L# E/ x* S
  7. 197         mov     pc, r13
    & d' O2 j! a5 L  R, @. K
  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:
    - T8 f- L# J, z2 v+ f, M/ t0 }
  2. 19         .long   __mmap_switched
    + ^4 |6 ~2 \9 c; E8 I1 f& c3 j
  3. 20         .long   __data_loc                      @ r4
    2 d. R& i; x5 d1 R
  4. 21         .long   _data                           @ r5- A, r5 y% ~3 x6 v& g5 K0 ?; @
  5. 22         .long   __bss_start                     @ r6
    1 V, Y5 J# _9 C
  6. 23         .long   _end                            @ r7! g- D9 ?5 G9 c
  7. 24         .long   processor_id                    @ r47 s( Q* r7 y( e0 w, ?2 T* C* ]
  8. 25         .long   __machine_arch_type             @ r5
    & y% E5 n& _* E4 l
  9. 26         .long   __atags_pointer                 @ r6
    # ?: W+ ]. o' ~: Q; |
  10. 27         .long   cr_alignment                    @ r7
    2 F7 C1 i# q7 m$ M2 R
  11. 28         .long   init_thread_union + THREAD_START_SP @ sp) x% f  ^+ d7 W0 t" ^4 P. T
  12. 29# L1 Z1 |* z% [! o7 Z8 B
複製代碼
switch_data的第一行就是 __mmap_switched,所以我們直接看line 39。
18#
 樓主| 發表於 2009-7-15 17:30:00 | 只看該作者
  1. 39 __mmap_switched:1 r& h+ i; c9 G
  2. 40         adr     r3, __switch_data + 4
    * u* a  K& G7 B" L
  3. 41% k4 o% t7 c( l0 s. x# k
  4. 42         ldmia   r3!, {r4, r5, r6, r7}4 d& o# r" z2 {* V% Z. E2 J
  5. 43         cmp     r4, r5                          @ Copy data segment if needed* m% @% p. @/ A# |
  6. 44 1:      cmpne   r5, r66 t8 f8 w& s; A: ?# W
  7. 45         ldrne   fp, [r4], #4
    / B& ?/ ]1 F) [5 B6 [8 d; t
  8. 46         strne   fp, [r5], #4
    7 T1 y! i; f  d" ]( v- z9 a
  9. 47         bne     1b
    + u4 n7 V+ J% i, K1 Q$ f
  10. 485 I" j% H% g: s  u% z# l
  11. 49         mov     fp, #0                          @ Clear BSS (and zero fp)1 Y* T: I7 v0 c* W9 w' B
  12. 50 1:      cmp     r6, r7! I9 ^0 G; K5 m# e: r  W8 r
  13. 51         strcc   fp, [r6],#4
    / ?2 t  i6 M1 C3 B& b0 K: X9 W
  14. 52         bcc     1b4 Z+ |9 {8 |7 U& K% p5 O
  15. 53
    ) ]4 X% z. W7 |: h8 J& x& {
  16. 54         ldmia   r3, {r4, r5, r6, r7, sp}2 D7 j1 W6 O9 Y: O! y
  17. 55         str     r9, [r4]                        @ Save processor ID
    7 \' h7 z. q- B( j2 S
  18. 56         str     r1, [r5]                        @ Save machine type% y( S0 O1 d' x& T# h: v3 @
  19. 57         str     r2, [r6]                        @ Save atags pointer3 l! X: |- k- U( {6 K% b/ ]
  20. 58         bic     r4, r0, #CR_A                   @ Clear 'A' bit
    ; k+ U. L8 C: A9 y6 I* [
  21. 59         stmia   r7, {r0, r4}                    @ Save control register values& n/ F: j' V1 x5 A( D3 |
  22. 60         b       start_kernel
    7 |0 c4 z* r$ m' G* b8 D" U" X/ l- ]
  23. 61 ENDPROC(__mmap_switched)
複製代碼
switch_data的第一行就是 __mmap_switched,所以我們直接看line 39。
+ H2 Q; D8 A5 e! H9 m4 k8 Lline 39,將__data_loc的addr放到r3/ f5 _0 M. \5 G+ V
line 42,從r3的位址,連續讀取四筆資料到r4, r5, r6, r7- K1 z+ I" H- C2 l
line 43~47,看看data segment是不是需要搬動。
. l. g3 J% [: s0 ?6 |' I+ `line 49~52, clear BSS。
1 ^, B/ w/ Q+ x2 b; c
# Z, r+ X/ P, N! r3 e由於linux kernel在進入start_kernel前有一些前提必須要滿足:
; p. q4 }2 ^' |7 [* y* X9 z8 d2 er0  = cp#15 control register
/ c) |. b9 N" p1 e6 N& t# yr1  = machine ID
* |0 c. H1 V; j& X3 b. Hr2  = atags pointer
; d0 I' J3 h8 r7 S( v/ n! zr9  = processor ID
+ W5 f5 {, b# Q  j1 g. R
1 ~. B, o" x2 j" |# Z, I6 F5 X所以line 54~59就是在做這些準備。
& E' \1 A4 k0 a" A$ F最後呢? 我們就跳到start_kernel了。(而且還是用b start_kernel,表示我們不會再回來了)
2 a" f; O: ]7 g; p; W3 ]$ d5 Y# ?
, f  y# p3 g& ^# Z# W" Y) |- D看一下start_kernel()在./init/main.c,終於跳出architecture specific的目錄,表示' I9 G) Y7 ~6 R7 k' O8 E7 }
我們真正的開始linux kernel的初始化。
/ H, A( Z* a# O* i: C8 j像是 shedule init, console init, memory init, irq init等等都在start_kernel裡頭。7 S" t( C1 X7 ]* P! H8 h, `
到這邊之後,應該就可以深入linux kernel中,各項比較跟hardware不那麼相關的軟體部分。

評分

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

查看全部評分

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

本版積分規則

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

GMT+8, 2024-6-17 01:25 AM , Processed in 0.156020 second(s), 19 queries .

Powered by Discuz! X3.2

© 2001-2013 Comsenz Inc.

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