Chip123 科技應用創新平台

 找回密碼
 申請會員

QQ登錄

只需一步,快速開始

Login

用FB帳號登入

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

trace linux kernel source - ARM - 03

[複製鏈接]
跳轉到指定樓層
1#
發表於 2008-10-7 14:00:18 | 只看該作者 回帖獎勵 |倒序瀏覽 |閱讀模式
到目前為止,我們已經進展到kernel幫自己relocate完,並解將自己解壓縮到一個地方要準備開始執行,那疑問來了?到底是跳到哪裡去了,因為./compressed/head.S最後一行居然是
1 g+ {* j0 a" H8 x, E3 i  l『mov pc, r4』
: y5 a2 X. U6 Kr4只代表了解壓縮完後kernel的位址,那究竟整包kernel編譯的時候,哪個function哪個東西被放在最前面咧?!
8 n% m8 n5 C4 p9 p* e( Q
# A' R  G8 @2 K! e+ a所以我們又必須開始找於kernel link的時候是怎麼被安排的,有了前面的基礎,我們可以從Makefile知道程式碼如何被編譯。至於link上的細節,例如有那些section和section先後順序等等,可以從 lds 檔來規範。! N0 O. i1 N) ~# W+ _
$ c! w* l  H% s
有興趣的人可以看一下 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)
* g1 {8 F& v- D9 V( u  ~& Y% M我們可以發現第一個section是『.text.head』,裡頭的_stext從目前的位置開始放。% z6 Q. B. h5 @/ `0 P* C7 C1 u3 F9 U- o
於是我們曉得只要找到屬於.text.head這個section,並且是_stext這個symbol的程式碼,就是解壓縮完後的第一行程式碼。
  1.      26     .text.head : {. @+ c) _7 G) l
  2.      27         _stext = .;
    ) ?1 n# [4 K* b4 W, j. V6 B
  3.      28         _sinittext = .;
    1 _4 U4 e) C" }
  4.      29         *(.text.head); y' ~, ^7 L5 O/ @" ?
  5.      30     }
複製代碼
用指令搜尋一下,發現 ./arch/arm/kernel/head.S 裡頭有關鍵字(如下),結果我們從./arch/arm/boot/compressed/head.S跳到了./arch/arm/kernel/head.S
  1.      77     .section ".text.head", "ax"6 k* f/ I8 a" o; L
  2.      78     .type   stext, %function
    2 y9 |% j' b2 W* a/ I
  3.      79 ENTRY(stext). U) v. G4 m/ |6 @8 E
  4.      80     msr cpsr_c, #PSR_F_BIT | PSR_I_BIT | SVC_MODE @ ensure svc mode2 f3 K' `9 p; t/ C" p$ m
  5.      81                         @ and irqs disabled
    & w- l7 z6 h0 _7 x
  6.      82     mrc p15, 0, r9, c0, c0      @ get processor id# l! a7 O3 v; ]9 d" o
  7.      83     bl  __lookup_processor_type     @ r5=procinfo r9=cpuid- ^2 Z# A9 t2 g) R* [5 p" B
  8.      84     movs    r10, r5             @ invalid processor (r5=0)?
    8 t9 |; o; f2 ^% o  l
  9.      85     beq __error_p           @ yes, error 'p'
    6 U  j" J6 r# B9 C; @7 v. W
  10.      86     bl  __lookup_machine_type       @ r5=machinfo" W* g1 p2 V& @' d" n
  11.      87     movs    r8, r5              @ invalid machine (r5=0)?& \0 {+ I! K5 B
  12.      88     beq __error_a           @ yes, error 'a': P' N) b4 X, I# ^5 L
  13.      89     bl  __vet_atags
    1 Q* g* \' q% Q+ n0 E9 |. q  ]  j
  14.      90     bl  __create_page_tables
複製代碼
既然找到了檔案,我們又可以開始繼續。
' ^1 q% Z" K, w5 F; t* t2 H
. J# r- J  X$ o看了一下,程式碼多了很多bl的跳躍動作,看來會在各個function跳來跳去。
分享到:  QQ好友和群QQ好友和群 QQ空間QQ空間 騰訊微博騰訊微博 騰訊朋友騰訊朋友
收藏收藏 分享分享 頂 踩 分享分享
2#
 樓主| 發表於 2008-10-9 15:32:16 | 只看該作者
既然跳到真正的kernel開始跑,表示進入重頭戲,在進入kernel之前arm的平台有一些預設的狀況,也就是說arm kernel image會預設目前的cpu和系統的狀況是在某個狀態,這樣對一個剛要跑起來的OS比較決定目前要怎麼boot起來。& x! {; D$ Q# r8 i

  D3 V, ^0 l8 Z6 g7 k可以看一下comment,有清楚的描述。這也幫助我們了解為什麼decompresser的程式跑完之後,還要把cache關掉。
  1.      59 /*
    : `$ ~2 N+ ^5 j( O
  2.      60  * Kernel startup entry point.7 }! l5 ^  F. I, d9 V1 @* w$ g
  3.      61  * ---------------------------
      o+ z  Q) T. [0 _; `! a
  4.      62  *3 b+ |& D. i0 Q' f: H) z  T" M# e: o
  5.      63  * This is normally called from the decompressor code.  The requirements
    9 T3 d; W* l+ I; {" V7 A5 U* x
  6.      64  * are: MMU = off, D-cache = off, I-cache = dont care, r0 = 0,, T: ?! \! v7 N3 e, O. k
  7.      65  * r1 = machine nr, r2 = atags pointer.
複製代碼
基於以上的假設,當program counter指到kernel的程式的時候,就會從line 80開始跑。
  A1 M) v, c7 S2 `9 mline 80, msr指令會把, operand的值搬到cpsr_c裡面。這是用來確保arm cpu目前是跑在svc mode, irq&fiq都disable。(設成0x1就會disable,definition在./include/asm-arm/ptrace.h)( q/ z+ B# w4 v: Y
line 82, 讀取CPU ID到r9
2 R, S! Y0 t6 y. k' B9 }$ Fline 83, 跳到 __lookup_processor_type 執行(bl會記住返回位址)。
  1.      77     .section ".text.head", "ax"
    ! r1 p0 ^  i+ u0 G; S) a; V
  2.      78     .type   stext, %function1 F6 j3 o5 }+ E8 B" p7 {  L
  3.      79 ENTRY(stext)
    # i- H* d# C7 F$ S" V/ O
  4.      80     msr cpsr_c, #PSR_F_BIT | PSR_I_BIT | SVC_MODE @ ensure svc mode( `, c# _0 F) {; }
  5.      81                         @ and irqs disabled
    3 Q; k5 J: |$ M
  6.      82     mrc p15, 0, r9, c0, c0      @ get processor id+ l+ E+ D' q& [; \; [$ [5 a
  7.      83     bl  __lookup_processor_type     @ r5=procinfo r9=cpuid
複製代碼
接著會跳到head-common.S這個檔,
/ ~1 h9 F; m* r, M# N9 ]line 158, (3f表示forware往前找叫做『3』label)將label 3的address放到r3。
) ~- f- H" k! c9 vline 159, 將r3指到的位址的資料,依序放到r7, r6, r5.(ldmda的da是要每次都位址減一次)
* v: Q8 e+ o# p( F( Bline l60, 161, 利用真實得到位址r3減去取得資料的位址得到一個offset值,這樣可計算出r5, r6真正應該要指到的地方。
2 A4 Q& G7 d3 i+ lline 163~169,這邊的程式應該不陌生,就是在各個CPU info裡面找尋對應的structure。找到的話就跳到line 171,返回head.S
7 y* @5 C) c: E7 s4 uline 170, 找不到的話,r5的processor id就放0x0.表示unknown id。# @* d& [4 c; U; t( s0 \0 F  ]
" U& Q. P. s  y& a+ g6 s/ k
__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
    2 ~9 `' k8 P' j0 g+ _5 u. i
  2.     157 __lookup_processor_type:, h- q" D- [9 Y/ R
  3.     158     adr r3, 3f
    : b; K7 w' |. f1 r9 @
  4.     159     ldmda   r3, {r5 - r7}
    ' m) U8 T: u5 e$ J
  5.     160     sub r3, r3, r7          @ get offset between virt&phys
    * h. d0 y0 d+ J* A3 k+ }
  6.     161     add r5, r5, r3          @ convert virt addresses to
    ; K* T' W( A0 {5 x
  7.     162     add r6, r6, r3          @ physical address space
      f7 C" B, }+ l! j
  8.     163 1:  ldmia   r5, {r3, r4}            @ value, mask
    2 F" S; N% S( f) o7 J; C
  9.     164     and r4, r4, r9          @ mask wanted bits2 C- q! G# W2 \! u, t8 j) D0 T+ K1 Q
  10.     165     teq r3, r49 X% U3 M; Z' y. n* ^
  11.     166     beq 2f8 j# t8 v- A2 W
  12.     167     add r5, r5, #PROC_INFO_SZ       @ sizeof(proc_info_list)! U. V+ r/ X7 j7 A' ?
  13.     168     cmp r5, r6
    ' I% @; \) {" I+ \* \
  14.     169     blo 1b
    & H$ m! I; S: o9 _  l
  15.     170     mov r5, #0              @ unknown processor
    8 h7 L& m& d* G( I7 R
  16.     171 2:  mov pc, lr' K) i$ T0 x0 H5 g; O# [
  17. , k" m0 W- ?5 I! l8 F. }
  18.     187     .long   __proc_info_begin( |5 T8 M' T2 `4 t$ U: z
  19.     188     .long   __proc_info_end
    ! n& z3 }1 R* K- p& o; o4 c& g
  20.     189 3:  .long   .
    1 U4 B- Z6 v3 X& c% v9 o
  21.     190     .long   __arch_info_begin. C6 Q7 _. n* l$ ~$ w2 l& B
  22.     191     .long   __arch_info_end
複製代碼
跳了很多檔案,建議指令和object code如何link的概念要有,就會很清楚了。
3#
 樓主| 發表於 2008-10-13 17:20:35 | 只看該作者
我們從 head-common.S返回之後,接著繼續看。
9 N5 }, Y( {( v% d7 A4 f
5 j7 L- {8 ]/ I7 C: K: w" ^line 84, movs的意思是說,做mov的動作,並且將指令的S bit設起來,最後的結果也會update CPSR。這個指令執行的過程當中,會根據你要搬動的值去把N and Z flag設好。這個是有助於下個指令做check的動作。
9 I) `6 y: l! E( xline 85, 就是r5 = 0的話,就跳到__error_p去執行。
: e( w* Q$ ^  L2 e* c3 Tline 86, 跳到__lookup_machine_type。原理跟剛剛找proc的資料一樣。
  1.      83         bl      __lookup_processor_type         @ r5=procinfo r9=cpuid
    8 G6 m7 R) k" ^
  2.      84         movs    r10, r5                         @ invalid processor (r5=0)?
    5 n9 i0 u* S, S+ y
  3.      85         beq     __error_p                       @ yes, error 'p'
    ! U3 o' y) ^& k. l  @) ?- E7 b
  4.      86         bl      __lookup_machine_type           @ r5=machinfo
複製代碼
看得出來跟proc很像,有個兩個小地方是
& ^* n. ~0 k6 B# I- f
8 a; B8 B. `4 T* `% G1. line 207,用ldmia不是用ldmda。原因就在於存放arch 和 proc info 的位址剛好相反。一個在lable3的上面。一個在的下方。設計上應該是可以做修改的。' i' K1 |' E: V3 ^
) R9 J, S! \  O: P
2. arch定義的方式是透過macro,可以先看 ./include/asm-arm/mach/arch.h。裡頭有個 MACHINE_START ,這邊會設好macro,到時候直接使用就可以把arch的info宣告好。 例如 ./arch/arm/mach-omap1/board-generic.c
  1. /* macro */5 F5 i6 M* w0 Y1 D$ v8 J
  2.      50 #define MACHINE_START(_type,_name)                      \/ ?& K6 `( l* j1 _
  3.      51 static const struct machine_desc __mach_desc_##_type    \" R5 v4 G2 f0 ^, }: Q( A5 ]  T
  4.      52  __used                                                 \
    7 t/ E. D' n3 l. e* C. H9 C
  5.      53  __attribute__((__section__(".arch.info.init"))) = {    \
    " M* ]- o! O* I. ~5 _; i1 _& D. u
  6.      54         .nr             = MACH_TYPE_##_type,            \
    1 ~  Y% _2 ^% p
  7.      55         .name           = _name,
    ' f7 i& ?* n/ f& x& E* O
  8.      56( I8 ?; l. y+ @% m
  9.      57 #define MACHINE_END                             \/ ^! y! K& A. `5 M
  10.      58 };* v  t5 J  K8 H2 o
  11.      /* 用法 */' P8 {1 D9 U# j! b# d
  12.      93 MACHINE_START(OMAP_GENERIC, "Generic OMAP1510/1610/1710")/ Q- d: l7 {' T! x
  13.      94         /* Maintainer: Tony Lindgren <tony@atomide.com> */
    ( ~+ N% ?/ k& N! t; i# y% O/ w
  14.      95         .phys_io        = 0xfff00000,# q& X- A& T  S% _( \5 \
  15.      96         .io_pg_offst    = ((0xfef00000) >> 18) & 0xfffc,
    8 h3 m3 U) u+ q, c
  16.      97         .boot_params    = 0x10000100,
    ) |% K% V" e, X
  17.      98         .map_io         = omap_generic_map_io,
    , I# ^2 O/ D: K. O: T/ e& G
  18.      99         .init_irq       = omap_generic_init_irq,
    8 s$ ]& {! I/ v# u+ y
  19.     100         .init_machine   = omap_generic_init,
    " z1 Z3 \* L& {. _. U# [! H
  20.     101         .timer          = &omap_timer,  d5 X! Z" c6 M- |
  21.     102 MACHINE_END
    " c' x+ m( z+ V0 L) o2 }- k
  22. + u' s, K2 [" e4 h- t0 m" v8 E
  23.     /* func */
    $ C9 u8 s' x2 O" \/ _0 r
  24.     204         .type   __lookup_machine_type, %function
    ) V- H0 A1 d9 l
  25.     205 __lookup_machine_type:6 M- h6 j) v/ ]: {9 |: [% b
  26.     206         adr     r3, 3b7 e$ M0 U0 r- n" Q
  27.     207         ldmia   r3, {r4, r5, r6}. h9 z4 G, |, l$ ^, b6 P
  28.     208         sub     r3, r3, r4                      @ get offset between virt&phys( h* x' \/ g6 B! L
  29.     209         add     r5, r5, r3                      @ convert virt addresses to# t2 ^9 h" v% n% f
  30.     210         add     r6, r6, r3                      @ physical address space
    % O9 @8 Q* g# v: D4 N6 `
  31.     211 1:      ldr     r3, [r5, #MACHINFO_TYPE]        @ get machine type
    " J  W$ \. N8 f$ ~( L
  32.     212         teq     r3, r1                          @ matches loader number?
    : n* a! X, x: ]. S, w  C9 S. O# H
  33.     213         beq     2f                              @ found/ i3 G% }" o9 O" l
  34.     214         add     r5, r5, #SIZEOF_MACHINE_DESC    @ next machine_desc; Q& Q) E! o, o  K
  35.     215         cmp     r5, r6. p  ^3 I; E  `' i7 J1 T3 `
  36.     216         blo     1b
    3 \% J. I% l! ?
  37.     217         mov     r5, #0                          @ unknown machine
    5 F9 S: d' S1 b* |
  38.     218 2:      mov     pc, lr
複製代碼
4#
 樓主| 發表於 2008-10-13 17:56:46 | 只看該作者
接著我們又返回到head.S,1 P5 n+ b# G0 T) ]
line 87~88也是做check動作。; z; U/ H/ y. _
line 89跳到vet_atags。在head-common.S
  1.      87         movs    r8, r5                          @ invalid machine (r5=0)?: u6 @8 o$ ]* x: o1 n% }
  2.      88         beq     __error_a                       @ yes, error 'a'
    8 E+ J0 Q* g* ~* }1 v: A
  3.      89         bl      __vet_atags
    % l3 v1 j, {9 \4 ^- j9 b
  4.      90         bl      __create_page_tables
複製代碼
line 245, tst會去做and動作。並且update flags。這邊的用意是判斷位址是不是aligned。& y# t% x9 C7 h/ z/ u
line 246, 沒有aligned跳到label 1,就返回了。
( `% V  W0 a& ^3 K' G' u6 _line 248~250, 讀取atags所在的address裡頭的值到r5,看看是否不等於ATAG_CORE_SIZE,不等於的話也是返回。
2 C& w) d) q- `% s1 Hline 251~254, 判斷一下第一個達到的atag pointer是不是等於ATAG_CORE。如果正確的話,等一下要讀取atag的資料,才會正確。1 G7 ]/ |/ ^4 C6 F/ G
(atag是由bootloader帶給linux kernel的東西,用來告知booting所需要知道的參數。例如螢幕寬度,記憶體大小等等)
  1.      14 #define ATAG_CORE 0x54410001
    6 t( f! Q- ~+ c* m5 L  w; H6 P4 }
  2.      15 #define ATAG_CORE_SIZE ((2*4 + 3*4) >> 2)0 W+ y+ T" c2 Q' c8 l% `

  3. 9 ~/ `/ {! w0 f5 t7 J8 Y
  4.     243         .type   __vet_atags, %function
    ; x9 ~8 q  ^9 R0 G+ t% S/ P
  5.     244 __vet_atags:
    & N, s4 D& ]! }) {9 `
  6.     245         tst     r2, #0x3                        @ aligned?* G  o! [3 ~& @5 Q" b0 p
  7.     246         bne     1f5 G( ]  y* ^0 I
  8.     247/ |: |3 j- W% B/ ?% D" ^4 x) E/ i
  9.     248         ldr     r5, [r2, #0]                    @ is first tag ATAG_CORE?
    ' o8 ?0 D8 t& U3 {) v# l1 C
  10.     249         subs    r5, r5, #ATAG_CORE_SIZE
    . S9 V) H7 O9 J+ U
  11.     250         bne     1f
    8 T, x. W! R" ~; i% z8 S
  12.     251         ldr     r5, [r2, #4]. a$ q2 t4 H* [0 e
  13.     252         ldr     r6, =ATAG_CORE
    8 y# }- d6 J. N) J
  14.     253         cmp     r5, r6) [: D8 \7 s& e$ p) ~( G
  15.     254         bne     1f
    $ }- {. Y( C* z6 K# e8 w
  16.     255
    8 q' e1 a8 _5 L8 Y2 |/ @
  17.     256         mov     pc, lr                          @ atag pointer is ok: q, p6 T( N3 N1 Q7 W$ {( {( L
  18.     2577 T. N0 b. x: u2 N8 U. v7 ~
  19.     258 1:      mov     r2, #0
    ) u/ B. r$ H, r$ Z  \* G, Q6 G
  20.     259         mov     pc, lr
複製代碼
接著我們又跳回去head.S。  
/ u! l& d9 G. Sline 90,又跳到 __create_page_tables。   (很累人....應該會死不少腦細胞)% J9 x; Y; ^  W1 _& J
哇!page table?!!如雷貫耳的東西,不知道會怎麼做。剛剛偷看了一下,@@還蠻長了,先到這邊好了,下次在繼續寫。
5#
 樓主| 發表於 2008-10-14 12:13:51 | 只看該作者
由於code看起來似乎越來越難解釋,會需要在檔案之間跳來跳去。建議一些基礎的東西可以再多複習幾次(其實是在說我自己 ),閱讀source code上收穫會比較多。例如:
3 z0 n5 N: d2 c( E# t* b; S6 \8 q5 n
1. arm instruction set - 這個最好每個指令的意思都大略看過一次,行有餘力多看幾個版本,armv4, v5 or v6。
) F/ P' _0 A+ c  ^& i1 M  ]2 w& }7 N2. compiler & assembler & linker - toolchain工具做的事情和功能,大致的流程和功能要有概念。4 C# c/ J* x+ |/ s5 m
3. Makefile & link script - 這兩個功能和撰寫的方式要有簡單的概念。
6 l4 }( `+ c, U6 }0 |+ u: |" p, N& @8 g9 T
以上1是非常重要的重點,2&3只要有大致上的概念就可以,因為trace code的時候,有時需要跳到Makeflie&Link script去看最後object code編排的位址。
. ^( a( J3 l2 L" A; F
. J1 ^: W) w; S* M8 j由於我們trace到了page table這個關鍵字,在開始之前,稍微簡短的解釋,為了幫助了解,儘量用易懂的概念講,有些用詞可能會和一些真實狀況不同,但是懂了之後,應該會有能力分辨,至於正式而學術上解說,很多書上應該都有,或是google一下就很多啦∼
6#
 樓主| 發表於 2008-10-14 12:14:47 | 只看該作者
page table本身是很抽象的東西,尤其是對所謂的 user-mode application programmer 來說,大部分的狀況它是不需要被考慮到的部份,但是他的產生卻對os和driver帶來了很多影響。我們從一個小小的疑問出發。
& I0 N. b$ z$ W8 H& Y9 C2 h: i* z8 j$ ~( D9 f
『產生page table到底是要給誰用的?』. `; u$ d$ w/ u6 p8 r6 u

" n; T3 q; p7 F0 t) d3 W3 k- N其實真正作用在它上面的H/W是MMU,一旦CPU啟用了MMU,當cpu嘗試去記憶體讀取一個operand的時候,cpu內部打出去的位址訊號都會先送到mmu,mmu會拿著這個位址去對照page table,看看這個位址是不是被轉換到另外一個位置(還會確認讀寫權力),最後才會到真正的位址去讀寫。0 o$ \& e2 p6 u, c

' t7 c1 k1 [+ |; `1 m# g: {這樣來看CPU其實一開始打出去的位址訊號其實不是最後可以拿到資料的位址,我們稱為virtual address(va),mmu打出來的位址才能真正拿到資料,所以我們把mmu打出去的位址稱為physical address(pa)。那用來查詢這個位址對照關係的表格,就是page table。2 A/ r! }6 O9 b: T- Z6 D6 k
8 B3 R# F- s$ f6 L* m( a
到這邊我們有一個簡單的概念。來想像一個小問題,一個普通的周邊(例如你的顯示卡),因為他並不具有MMU功能,hw只看得懂pa,但是CPU卻是作用在va,假如我想去對hw的控制暫存器做讀寫,到底要用va還是pa?
7#
 樓主| 發表於 2008-10-14 12:15:51 | 只看該作者
這時,寫driver的人就必須要小心,設定給硬體看的位址必須要先轉成pa(像是設定DMA),給CPU執行的程式碼要使用va,這對一開始嘗試寫driver但是又不瞭解va pa之間的差別的人,常常產生很多疑問。4 u- \2 M8 L# x* Y5 u

  e' Q& g2 K2 @( E  w現在我們回頭想想OS,既然我們跑到create page table,可以預期的是他想要將MMU打開,因此希望預先建立起一個page table,讓MMU知道目前os想規劃的位址對應和讀取權力是如何被安排的。所以os必須考慮到現在的系統究竟是長怎樣?應該要如何被安排?是不是有那些要被保護?好吧∼因為我們完全對os不了解,也不知道該安排什麼,只能祈禱在trace code完後,可以找到這些問題的答案,或者發現一些沒想到的問題。- ?3 {3 a2 i/ A+ ^3 n- M
% j* d9 G% l4 r' g" A4 o" P
知道了page table的大致上的功能,下篇就可以專心的研究這個table的長相,和它想規劃出的系統模樣。
. h* w0 P6 \- `) [- {, G8 c' S( Q9 ]5 }, V6 ^1 |! ]
p.s. 字數限制好像變短了。   (看來很難寫)
8#
 樓主| 發表於 2008-10-14 13:56:17 | 只看該作者
由於字數限制挑整,用詞儘量精簡,以便可以貼必要source。另外,以後寫到一段落,考慮收集成一篇完整的文章,這樣應就不會因為用回覆的方式,造成閱讀斷斷續續的問題。希望有人對文章呈現方式有想法的話,可以跟我講。
9#
 樓主| 發表於 2008-10-14 13:57:39 | 只看該作者
現在,讓我們跳入create_page_tables吧∼
0 q) C* @4 j2 v6 ^8 `line 216,會跳到pgtbl的macro去執行,其實只是載入一個位址。
" a$ d& a2 Z9 Y
# g6 h1 H- j. `" Q' X/ S  s只是這個位址因為你硬體規劃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 */+ L' M6 Z1 {" v1 S& N
  2.      95 textofs-y       := 0x00008000
    9 v, G# O% y$ `1 H) W7 q
  3.     152 TEXT_OFFSET := $(textofs-y)
複製代碼
  1. /* include/asm-arm/arch-omap/memory.h */- g. H5 _$ f0 ~
  2.      40 #define PHYS_OFFSET             UL(0x10000000)5 j4 ?3 ~) ~( o7 J& I5 U" i! Z
  3. 5 F; u) H% @, s, ?
  4.      /* arch/arm/kernel/head.S */5 q  f, O/ K4 T2 x8 g+ t8 p* \
  5.      29 #define KERNEL_RAM_VADDR        (PAGE_OFFSET + TEXT_OFFSET)6 g9 i5 y! f) D" {  e) y: u& j8 f
  6.      30 #define KERNEL_RAM_PADDR        (PHYS_OFFSET + TEXT_OFFSET)) P6 ^$ I& S! Q- i* A5 L# m5 \

  7. & L  I. X. L# Q
  8.      47         .macro  pgtbl, rd) N9 B# l1 D9 p) O/ _! ?7 T: Z
  9.      48         ldr     \rd, =(KERNEL_RAM_PADDR - 0x4000)
    + @5 T" o7 f, y. ~; F- I2 Z2 \8 ~
  10.      49         .endm
    " z, F' d; |* f1 P
  11. ) G( I% _$ f- b5 v0 G, l3 F7 K& ]; h; ]
  12.     216         pgtbl   r4                              @ page table address
複製代碼
10#
 樓主| 發表於 2008-10-14 14:16:53 | 只看該作者
得到pg table的開始位置之後,當然就是初始化囉。% ?7 G* X( y( G: |; q; b' i
line 221, 將pg table的base addr放到r0.  z5 U0 J+ v  Q" D, y& i0 T
line 223, 將pg table的end addr放到r6.. `; z5 b* Z1 h' h/ R
line 224~228, 反覆地將0x0寫到pg table的區段裡頭,每個loop寫16bytes. (4x4),直到碰到end,結果就是把它全部clear成0x0.
  1.     221         mov     r0, r49 n! h2 V6 {$ Y  @& P; m
  2.     222         mov     r3, #0
    * @$ a# [( ^7 h8 c4 V' H; ~
  3.     223         add     r6, r0, #0x4000! X6 ^+ {5 Q/ J0 O
  4.     224 1:      str     r3, [r0], #4/ E: K2 c1 u' m+ `% s) |# x
  5.     225         str     r3, [r0], #4, U: x' ^0 ]! Y; u  J7 e
  6.     226         str     r3, [r0], #4
    8 T/ J) B7 _+ Y4 S4 P. q
  7.     227         str     r3, [r0], #49 N. P% E" g* S7 h) f5 \; x# ~
  8.     228         teq     r0, r6
    7 v0 n) L5 q9 F- W' |
  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 | 只看該作者
問題怎麼填值??
6 }2 P! K$ ]1 I( \! o2 o( i拿出ARM的手冊,翻到MMU章節。一看發現page有很多種,還得分first level和second level。
3 D+ |* h$ O( ?& R4 \& p7 n! i1 x! o8 G: v6 A# Y8 l9 Q5 W8 C
念書時的印象,是從1st level在去查2nd level,先看怎麼設定1st level (不知以前老師有沒有亂教)
- `% l8 I6 B) C: Q* M1. [31:20]存著section base addr! U) o! q6 _0 o$ V
2. [19:2]存著mmu flags
+ s. I3 y' g9 I9 V( @  L8 Q) ]0 K3. [1:0]用來辨別這是存放哪種page, 有四種:
$ T% L: K8 O8 P9 [   a. fault (00) b. coarse page (01) c. section (1st level) (10) d. fine page (11); s  F6 C% F( Q- s' M9 s
4. pg tabel資料要存放到 [31:14] = translation base, [13:2] = table index , [1:0] = 0b00 的位址: ?: @2 F& t( H' A$ w5 l

: U% b' s8 G9 q  \4 N) y來看code是怎麼設定。7 p2 {' u) x8 M' q

1 P0 L& B- W3 Sline 239, 將pc的值往右shift 20次放到r6.這是取得前20高位元的資料,有點像是 1.。; t- w/ i) R5 S. Z
line 240, 將r6往左shift 20次之後,和r7做or的動作,剛好也是 2.提到的mmu flags和1.處理好的資料做整理。: K% M, z' w: I* O* }# I
所以前面兩個做完,就完成了bit[31:2]。5 g5 h) m7 y7 u3 B7 y6 K
line 241, 將r3的資料寫到r4+(r6<<0x2)的地方去,剛好是4.提到的位址。(lucky)
  1.     239         mov     r6, pc, lsr #20
    $ F, b& L0 f6 G" v. T3 G2 I4 T9 j4 E  g
  2.     240         orr     r3, r7, r6, lsl #20' S: a* q5 i; y
  3.     241         str     r3, [r4, r6, lsl #2]
複製代碼
p.s. 用pc值剛好可以算出當前的page。
12#
 樓主| 發表於 2008-10-22 19:47:03 | 只看該作者
最近又被釘上了,開始忙碌,大概沒太多時間貼文∼
3 g1 |  l: V" r  B8 W! h4 f. \% G0 k9 ?) _2 W7 b
上篇已經將pc所屬的page table entry(pte)設定好,接著繼續看5 V. k% Q  e' O8 D. P# E
line 247, 248, 將KERNEL_START的位址往右shift 18算出pte的offset,(等於line239&241,239&241是先shift right 20然後shift left 2),並將剛剛r3的值設給pte。『!』的意思是會把address存到r0。
1 ^: o; |# [* x' i$ y& D! d$ m" p
( U; g9 \* V; Z4 S( l# k, @1 Rline 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
    ' C7 `0 Q0 h6 @4 V
  2.     248         str     r3, [r0, #(KERNEL_START & 0x00f00000) >> 18]! & \3 {& M3 Q- ~4 g6 X% a+ `' n
  3.     249         ldr     r6, =(KERNEL_END - 1)9 ~3 N, z4 f0 p  B
  4.     250         add     r0, r0, #4
    . K9 r, }3 ?/ c! a; H  K' J
  5.     251         add     r6, r4, r6, lsr #18, }  ?0 m* Z. Z3 c
  6.     252 1:      cmp     r0, r6
    ( S/ u' K( C4 _" n
  7.     253         add     r3, r3, #1 << 20) n! ]/ s5 ?% C; i2 P/ ^7 l
  8.     254         strls   r3, [r0], #4) H7 C( c# p+ J9 P$ F
  9.     255         bls     1b
複製代碼
13#
 樓主| 發表於 2008-10-22 20:24:58 | 只看該作者
line279,PAGE_OFFSET是規範RAM mapping完後的virtual address,我們將這個位址所屬的pte算出來放到r0。) a- S" s' J, {8 _4 {
line 280~283,將要 map 的physical address的方式算出來放到r6。
+ n; O; g: [6 ]/ \: e6 Oline 284,最後將結果存到r0所指到的pte。
2 \! F5 W$ G  Z+ n  l) u) g" h4 q; ~$ @' W1 t) X$ _9 n
以上三個動作,就是做好 ram 起頭的位址一開始map,還沒做完整塊map。由於我們目前的設定方式每塊是1MB,所以這邊是將RAM開始的1MB做map。* A7 s# J4 [' I+ i. W) H$ p

! e9 I- A# w% O! n2 S0 a# |line 327,返回,結束一開始的create page table的動作,我們可以看出其實並沒有做完整的初始化page table,所以之後應該會有其他page table的細節。
  1.     279         add     r0, r4, #PAGE_OFFSET >> 18
    8 [1 v$ O1 @( `& v  a1 L
  2.     280         orr     r6, r7, #(PHYS_OFFSET & 0xff000000)
    - S1 {  I5 y# \; E, p  c8 [
  3.     281         .if     (PHYS_OFFSET & 0x00f00000)3 J) r: A. Q/ k) B
  4.     282         orr     r6, r6, #(PHYS_OFFSET & 0x00f00000)
    % \4 `8 D( b4 a5 Y) H
  5.     283         .endif/ I# x: ?- ~: h
  6.     284         str     r6, [r0]7 v8 Y7 O& \) A0 ~3 k7 x
  7.     327         mov     pc, lr
複製代碼
附帶一提,我們這邊省略了一些用ifdef包起來的程式碼,像是一開始會印一些output message的程式碼(line286~326)。
14#
 樓主| 發表於 2008-10-22 20:37:08 | 只看該作者
自create page table返回後,我們偷偷看一下接下來的程式碼,  l% I$ D( ~1 B- P
line 99, 將switch_data擺到r13
! D& `& ?  p; J; G4 wline 101, 將enable_mmu擺到lr
4 E3 Y' j, i4 F. [3 n% ^2 |7 |line 102, 將pc改跳到r10+PROCINFO_INITFUNC的地方去+ h( A/ f; F6 b7 _2 l/ _

4 y: T. I+ F. w  A8 P8 O4 ]' e6 {其實這邊有點玄機,switch_data和enable_mmu都是function,結果位址都只是被存起來,並沒有直接跳過去執行。其實,雖然程式碼是順序switch_data->enable_mmu->proc init function,但其實執行的順序會是 procinfo 的init function-> enable_mmu -> switch_data 。至於,為什麼要這樣寫的原因?就不清楚了,也還沒仔細推敲過。
* {0 ]$ J8 e' G$ }: u$ A$ _9 u( p2 l- ^# S' J  P. D
switch_data最後就會跳到大家都很熟悉的start_kernel(). 詳細要賣個關子,最近會忙一下,得要過一陣子才能貼文∼  
  1.      99         ldr     r13, __switch_data              @ address to jump to after  S" b1 q3 e3 ]5 n! A+ g/ D, H
  2.     100                                                 @ mmu has been enabled
    : F9 G) T( Y. F; Q3 q
  3.     101         adr     lr, __enable_mmu                @ return (PIC) address
    % {9 T# `' {. t5 j% j) a
  4.     102         add     pc, r10, #PROCINFO_INITFUNC
複製代碼
15#
 樓主| 發表於 2009-7-4 01:09:36 | 只看該作者
老店重新開張~
" ~' X8 _5 _" Q# f! c6 k3 A- o+ K! w# [6 b6 A. B9 Y
花了一些時間把舊的貼文整理到一個blog
7 \# `0 ]' `8 j+ F有把一些敘述修改過$ D. N+ n# w- N! \2 p
希望會比較容易集中閱讀
% A1 m. F) s5 s3 X2 z* O" P目前因為某些敘述不容易4 _! D. W7 C6 ^- e+ |
還是比較偏向筆記式而且用字不夠精確5 ^* n" m% {/ ]; Z. }( S+ c
希望之後能夠慢慢有系統地整理( B  \' C: x+ J/ h2 _
大家有興趣的話
9 A* M8 c6 F" e可以來看看和討論
7 I7 b: a0 u6 w, h4 h  w0 ?http://gogojesseco.blogspot.com/) n1 k3 J5 _) n+ {- t
) r# `. L5 ?' j
以後可能會採取  先在chip123貼新文章
) d0 H% N; g+ p& E2 D% d慢慢整理到blog上的方式+ E; ]" L/ o" F  Q/ e
因為chip123比較方便討論 =)
; E3 i9 T# o  g/ Y/ M( Y  J8 u0 X. sblog編輯修改起來比較方便
' Q0 v5 `* v4 I" e$ g3 T$ q閱讀也比較集中   大家可以在這邊看到討論
# S' R: {  h& |( {然後在blog看到完整的文章 (類似BBS精華區的感覺)

評分

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

查看全部評分

16#
 樓主| 發表於 2009-7-15 17:07:03 | 只看該作者
隔了很長一段時間沒update
5 s' O8 y2 h& R: n之前程式碼走到 ./arch/arm/kernel/head.S 的 line 99 附近
  1.      99         ldr     r13, __switch_data              @ address to jump to after
    9 z0 x5 v1 r! c$ j4 I% @2 g
  2.     100                                                 @ mmu has been enabled
    3 s" X6 k% z2 G) P% m
  3.     101         adr     lr, __enable_mmu                @ return (PIC) address6 k! E: t, R8 A2 t/ w- j
  4.     102         add     pc, r10, #PROCINFO_INITFUNC
複製代碼
line 99, 將__switch_data放到r13。(留作之後用)6 G7 y  Q8 e" W) y
line 101, 將__enable_mmu的addr放到lr。(留作之後用)" N  s/ Y% o) l2 F8 i) f' 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
    ; R, w. s6 l/ F3 L* g$ v8 o3 f0 f" {# M
  2. 374 __arm926_setup:
    1 }8 v+ s9 s$ q- Y* ?
  3. 375         mov     r0, #0
    3 Y. b# f8 N  i) h0 d9 g" D
  4. 376         mcr     p15, 0, r0, c7, c7              @ invalidate I,D caches on v43 p+ S- w+ w' h5 \+ A
  5. 377         mcr     p15, 0, r0, c7, c10, 4          @ drain write buffer on v46 _# S$ h: i% @  D, u: t' w4 C
  6. 378 #ifdef CONFIG_MMU- Y6 K1 d; N$ x2 w# y$ r
  7. 379         mcr     p15, 0, r0, c8, c7              @ invalidate I,D TLBs on v49 s; H$ L7 S9 _, L, @  h4 X
  8. 380 #endif
    7 s" N" C# K( e& g& o

  9. 9 d- ~8 H. ^6 k% `$ E
  10. 388         adr     r5, arm926_crval
    % A6 e, X; e- k  V: r
  11. 389         ldmia   r5, {r5, r6}
    * L1 y% S) {! \# V6 P5 m. T
  12. 390         mrc     p15, 0, r0, c1, c0              @ get control register v4
    5 M8 Q: @6 l& ]1 ?8 U
  13. 391         bic     r0, r0, r5% Q- Q" k) U* _" {% D
  14. 392         orr     r0, r0, r6
    3 H8 s, h& ?2 s% q

  15. 9 \+ H9 Q' M. }: w
  16. 396         mov     pc, lr- _7 D" x; T3 G  S
  17. 397         .size   __arm926_setup, . - __arm926_setup
複製代碼
這邊的程式碼就跟CPU有很大的相依性,
8 T/ z( c8 x& [1 i. ]line 375~380, 主要就是invalidate CPU的I&D cache和清空write buffer。
+ T+ q" F3 ]2 Bline 388~392, 把cp15的設定讀出來,並且將一些預設狀態做好運算放到r0。(預設值從arm926_crval這邊可以取得。)
( l' ]2 Z$ w7 Z: a2 `line 396, 直接把pc跳到lr,因為我們之前已經將lr = enable_mmu,所以直接跳過去。
17#
 樓主| 發表於 2009-7-15 17:29:45 | 只看該作者
  1. 155 __enable_mmu:
    2 ?3 e( @; D6 V, A
  2. 170         mov     r5, #(domain_val(DOMAIN_USER, DOMAIN_MANAGER) | \
    # t/ t4 b( g; X7 {
  3. 171                       domain_val(DOMAIN_KERNEL, DOMAIN_MANAGER) | \) D  K, X! n; K( c' a
  4. 172                       domain_val(DOMAIN_TABLE, DOMAIN_MANAGER) | \5 Q" _/ L3 h/ Q" p& e8 Y8 q- ?
  5. 173                       domain_val(DOMAIN_IO, DOMAIN_CLIENT))" K3 @6 o* `! L) n) N* s' ~% _( }
  6. 174         mcr     p15, 0, r5, c3, c0, 0           @ load domain access register- w6 @4 ?- }4 g9 b# g
  7. 175         mcr     p15, 0, r4, c2, c0, 0           @ load page table pointer
    ( b5 Z( h! p* J5 W) y8 q* ?$ L+ q3 c
  8. 176         b       __turn_mmu_on6 Z) Y% l$ b9 L' }' U  e
  9. 177 ENDPROC(__enable_mmu)
複製代碼
line 170~174,設置好domain access。(domain access可以先當成設置access的權限,或許以後可以寫詳細的文章說明)5 z) @% y$ ^" r$ g2 c6 N  d- w) M
line 175~176,將create好的page table開頭丟給mmu。跳到turn_mmu_on
  1. 191 __turn_mmu_on:7 ?, e# a" w6 R. i5 d% E2 C
  2. 192         mov     r0, r00 x# j* H0 f" `
  3. 193         mcr     p15, 0, r0, c1, c0, 0           @ write control reg
    " L7 z. {5 a# H, }& T. r
  4. 194         mrc     p15, 0, r3, c0, c0, 0           @ read id reg' J- l& |# `8 s4 U
  5. 195         mov     r3, r33 }! m4 G& W" e6 \0 `
  6. 196         mov     r3, r31 R, w* G- G8 a
  7. 197         mov     pc, r13, h7 t$ U4 L) C5 ?" K3 T
  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:
    5 f/ G$ J8 R4 f! c0 Q$ E7 V8 b* W
  2. 19         .long   __mmap_switched9 ?* x" O8 q# {8 r
  3. 20         .long   __data_loc                      @ r4
    ' v& K2 v8 I: H5 T
  4. 21         .long   _data                           @ r5' R% V2 Y. A+ M% _& r/ d  H4 m; I
  5. 22         .long   __bss_start                     @ r62 R8 r" o& Q4 G; D( F
  6. 23         .long   _end                            @ r7
    + g' L6 c6 s6 [
  7. 24         .long   processor_id                    @ r4
    4 y6 r  \6 \$ T- k
  8. 25         .long   __machine_arch_type             @ r5; M" s( v3 z& r9 B) }% V4 e9 n: m5 U
  9. 26         .long   __atags_pointer                 @ r6
    # o0 ^/ x' H: ], P
  10. 27         .long   cr_alignment                    @ r7! d/ r& L/ S4 D; m8 N
  11. 28         .long   init_thread_union + THREAD_START_SP @ sp
    ( @1 F$ C* P2 ^7 A  ~/ |
  12. 29; s6 d0 s2 ]% V7 P5 o) [( c. f
複製代碼
switch_data的第一行就是 __mmap_switched,所以我們直接看line 39。
18#
 樓主| 發表於 2009-7-15 17:30:00 | 只看該作者
  1. 39 __mmap_switched:2 {% b+ q7 l2 |8 O  O
  2. 40         adr     r3, __switch_data + 4
    * f/ ?6 I6 ^. x; X- a" f  h$ ~/ G0 G9 z' `
  3. 41
    ! j9 b  s- H3 D/ Y/ S5 o
  4. 42         ldmia   r3!, {r4, r5, r6, r7}- T! P/ ^: r. ]1 S3 [) u
  5. 43         cmp     r4, r5                          @ Copy data segment if needed
    3 [) [  |1 F6 I5 L
  6. 44 1:      cmpne   r5, r69 x  l% b. G2 S1 ^- y1 ]! W
  7. 45         ldrne   fp, [r4], #4
    & i# B" q, u3 `5 x
  8. 46         strne   fp, [r5], #4. T7 W# f8 I' N
  9. 47         bne     1b
    2 B! w, u- {3 v; s4 p  b
  10. 48
    - K5 P9 j5 n* U1 O) }' m
  11. 49         mov     fp, #0                          @ Clear BSS (and zero fp)
    - g' s2 w8 N3 \: G  h
  12. 50 1:      cmp     r6, r7
    ' f" _! j+ `3 y8 f
  13. 51         strcc   fp, [r6],#4! l) j8 G! m# x$ W  V* u$ k
  14. 52         bcc     1b
    1 y  k. {/ y* Y  D4 w( b0 ~
  15. 539 ]( I, K) x+ k; M) ^, s
  16. 54         ldmia   r3, {r4, r5, r6, r7, sp}1 i: F; M" J5 s1 |% c9 b
  17. 55         str     r9, [r4]                        @ Save processor ID: w7 x: A1 O! G# v( Q
  18. 56         str     r1, [r5]                        @ Save machine type
    + [( d( d6 J' }
  19. 57         str     r2, [r6]                        @ Save atags pointer" D3 s1 i9 F% X" f" ^3 B% w8 |6 }
  20. 58         bic     r4, r0, #CR_A                   @ Clear 'A' bit4 D3 n; \8 w* |$ v& [) R
  21. 59         stmia   r7, {r0, r4}                    @ Save control register values
    5 a/ p( ^( g; G( Y. G
  22. 60         b       start_kernel
    1 i- h0 ]+ P& p5 m; n
  23. 61 ENDPROC(__mmap_switched)
複製代碼
switch_data的第一行就是 __mmap_switched,所以我們直接看line 39。
* p/ ~  m7 b+ |$ [line 39,將__data_loc的addr放到r3
5 S! H3 Y" B3 `/ ~line 42,從r3的位址,連續讀取四筆資料到r4, r5, r6, r7
: o5 W: W" I% o. D& g. P  Oline 43~47,看看data segment是不是需要搬動。2 }5 C) b7 w6 H8 g1 C) S% ~; X
line 49~52, clear BSS。! p$ B& n; o7 N; G/ |# r
/ c' z" y8 |) D3 k9 e1 L0 I
由於linux kernel在進入start_kernel前有一些前提必須要滿足:
  ^# S$ }( j. n; Or0  = cp#15 control register2 D0 Z1 G" t3 w2 O9 B" k
r1  = machine ID. J- K* N5 u- @7 C, U4 E1 Z7 _- i" v
r2  = atags pointer
& i% M5 d8 c8 r9 m; Pr9  = processor ID
6 n. f" _! _( y! w: t2 F  \& T5 @1 j, K3 u4 _  J* c
所以line 54~59就是在做這些準備。
8 ~1 @- U  Z- ~最後呢? 我們就跳到start_kernel了。(而且還是用b start_kernel,表示我們不會再回來了)" x, J& P: K5 R& G: ]- h% P- B0 P: t
) {1 X1 y. S2 q) t- c
看一下start_kernel()在./init/main.c,終於跳出architecture specific的目錄,表示
7 r1 a4 _+ h3 ]0 ^) [9 ^我們真正的開始linux kernel的初始化。2 C- o; O6 R7 r' H
像是 shedule init, console init, memory init, irq init等等都在start_kernel裡頭。
9 y: i& Y% G0 y到這邊之後,應該就可以深入linux kernel中,各項比較跟hardware不那麼相關的軟體部分。

評分

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

查看全部評分

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

本版積分規則

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

GMT+8, 2024-6-17 04:41 AM , Processed in 0.162021 second(s), 18 queries .

Powered by Discuz! X3.2

© 2001-2013 Comsenz Inc.

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