Chip123 科技應用創新平台

 找回密碼
 申請會員

QQ登錄

只需一步,快速開始

Login

用FB帳號登入

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

trace linux kernel source - ARM - 03

[複製鏈接]
跳轉到指定樓層
1#
發表於 2008-10-7 14:00:18 | 只看該作者 回帖獎勵 |倒序瀏覽 |閱讀模式
到目前為止,我們已經進展到kernel幫自己relocate完,並解將自己解壓縮到一個地方要準備開始執行,那疑問來了?到底是跳到哪裡去了,因為./compressed/head.S最後一行居然是
% [- C3 M* ^3 X% @* P『mov pc, r4』
$ ^- {- S6 I) |$ q' jr4只代表了解壓縮完後kernel的位址,那究竟整包kernel編譯的時候,哪個function哪個東西被放在最前面咧?!- x' e* L4 L8 S3 T- B. g

( d5 k' o; _2 C/ g- R% x# z所以我們又必須開始找於kernel link的時候是怎麼被安排的,有了前面的基礎,我們可以從Makefile知道程式碼如何被編譯。至於link上的細節,例如有那些section和section先後順序等等,可以從 lds 檔來規範。
7 F4 Y- E+ g! H( s- K
8 z) q3 q  M0 w+ W  p: k) K有興趣的人可以看一下 kernel source 根目錄裡頭的 Makefile,Makefile file裡面指定了使用vmlinux.lds來當做lds檔。
  1. 659 vmlinux-lds  := arch/$(SRCARCH)/kernel/vmlinux.lds
複製代碼
打開./arch/arm/kernel/vmlinux.lds.S (會用來產生vmlinux.lds)
+ J. O& `2 l2 D7 m" m7 a% Q我們可以發現第一個section是『.text.head』,裡頭的_stext從目前的位置開始放。
  E0 K- J5 @/ L! N. d5 U/ H4 U於是我們曉得只要找到屬於.text.head這個section,並且是_stext這個symbol的程式碼,就是解壓縮完後的第一行程式碼。
  1.      26     .text.head : {
    # r' L/ u- S" K5 x
  2.      27         _stext = .;
    # _. j! H8 ?5 S9 i8 n  V" A* ~
  3.      28         _sinittext = .;4 [% e3 I1 h$ S, Q
  4.      29         *(.text.head)' ^1 ^* P+ ?% x7 x! \' g" M
  5.      30     }
複製代碼
用指令搜尋一下,發現 ./arch/arm/kernel/head.S 裡頭有關鍵字(如下),結果我們從./arch/arm/boot/compressed/head.S跳到了./arch/arm/kernel/head.S
  1.      77     .section ".text.head", "ax"
    ( W/ h3 H( H, {% v7 z( m6 e
  2.      78     .type   stext, %function2 W2 [( n5 O  S+ C: {/ c3 J
  3.      79 ENTRY(stext)
    - O' }; n3 Y2 O8 k+ G
  4.      80     msr cpsr_c, #PSR_F_BIT | PSR_I_BIT | SVC_MODE @ ensure svc mode
    9 h0 E$ O" ^$ g2 N
  5.      81                         @ and irqs disabled
    / c2 F0 `; q: q( _( P) h* `; n
  6.      82     mrc p15, 0, r9, c0, c0      @ get processor id
    2 K/ \5 E( C. z8 m
  7.      83     bl  __lookup_processor_type     @ r5=procinfo r9=cpuid/ c* Q4 l5 s2 K/ r5 i; j6 j% R5 o. V
  8.      84     movs    r10, r5             @ invalid processor (r5=0)?
      j" ^) t& R' F% i% g" O
  9.      85     beq __error_p           @ yes, error 'p'
    # X- b4 v, D6 q. o4 d" H
  10.      86     bl  __lookup_machine_type       @ r5=machinfo+ q' [" w" c! d' u' x$ F' w6 U
  11.      87     movs    r8, r5              @ invalid machine (r5=0)?; u1 l# ~' o% g+ U, s
  12.      88     beq __error_a           @ yes, error 'a'
    / X! ^- d% ~3 x2 C8 J6 S
  13.      89     bl  __vet_atags7 m8 K% P. c1 ?* R: P; U$ I
  14.      90     bl  __create_page_tables
複製代碼
既然找到了檔案,我們又可以開始繼續。
9 D, ~2 Y/ n8 _. N% E/ R' V( j
" j: y2 @/ }" m1 e1 K& E2 {+ q6 `看了一下,程式碼多了很多bl的跳躍動作,看來會在各個function跳來跳去。
分享到:  QQ好友和群QQ好友和群 QQ空間QQ空間 騰訊微博騰訊微博 騰訊朋友騰訊朋友
收藏收藏 分享分享 頂 踩 分享分享
2#
 樓主| 發表於 2008-10-9 15:32:16 | 只看該作者
既然跳到真正的kernel開始跑,表示進入重頭戲,在進入kernel之前arm的平台有一些預設的狀況,也就是說arm kernel image會預設目前的cpu和系統的狀況是在某個狀態,這樣對一個剛要跑起來的OS比較決定目前要怎麼boot起來。
! r, z8 `0 p/ o% V' x8 T& J9 ?/ F0 n' Y/ A
可以看一下comment,有清楚的描述。這也幫助我們了解為什麼decompresser的程式跑完之後,還要把cache關掉。
  1.      59 /*  Q+ E7 I  o9 o  S/ r, }1 n
  2.      60  * Kernel startup entry point.
    8 z! k. O- {% S9 ?- V" y0 f
  3.      61  * ---------------------------
    & B& ?6 O3 W7 N, k0 l0 V" ?# ^
  4.      62  *3 ?5 s$ d* r( Q, C
  5.      63  * This is normally called from the decompressor code.  The requirements
    ' |& z; R2 v  E+ c
  6.      64  * are: MMU = off, D-cache = off, I-cache = dont care, r0 = 0,
    0 ]6 i8 ]0 Y9 }) b
  7.      65  * r1 = machine nr, r2 = atags pointer.
複製代碼
基於以上的假設,當program counter指到kernel的程式的時候,就會從line 80開始跑。, Y" W- ^7 Q& T8 Z- p9 @9 ^
line 80, msr指令會把, operand的值搬到cpsr_c裡面。這是用來確保arm cpu目前是跑在svc mode, irq&fiq都disable。(設成0x1就會disable,definition在./include/asm-arm/ptrace.h)
0 p  n. {2 a2 a8 T3 lline 82, 讀取CPU ID到r9, W+ S/ c+ K4 p2 ]! A" O$ J
line 83, 跳到 __lookup_processor_type 執行(bl會記住返回位址)。
  1.      77     .section ".text.head", "ax"
    . W1 q! e6 G8 w- s* k& {7 o% `% L
  2.      78     .type   stext, %function1 \% q6 a  N4 e3 f1 b6 c) {
  3.      79 ENTRY(stext)
    1 i0 n* C  L0 g% ~  {- \. t4 q
  4.      80     msr cpsr_c, #PSR_F_BIT | PSR_I_BIT | SVC_MODE @ ensure svc mode
    " ?$ q! m5 E4 b8 q$ g  b
  5.      81                         @ and irqs disabled
    0 A4 P3 _4 @8 X8 F: i- w3 D
  6.      82     mrc p15, 0, r9, c0, c0      @ get processor id
    : k6 }2 h& O  }
  7.      83     bl  __lookup_processor_type     @ r5=procinfo r9=cpuid
複製代碼
接著會跳到head-common.S這個檔,
7 T: o+ Y: k7 N% pline 158, (3f表示forware往前找叫做『3』label)將label 3的address放到r3。! Q# V4 J0 L$ Y0 Z( W
line 159, 將r3指到的位址的資料,依序放到r7, r6, r5.(ldmda的da是要每次都位址減一次)1 X0 j$ F/ Y7 x* A$ T8 q$ {
line l60, 161, 利用真實得到位址r3減去取得資料的位址得到一個offset值,這樣可計算出r5, r6真正應該要指到的地方。7 f% b* l" |/ E+ T
line 163~169,這邊的程式應該不陌生,就是在各個CPU info裡面找尋對應的structure。找到的話就跳到line 171,返回head.S
0 K0 D5 z9 f# @. v2 r& bline 170, 找不到的話,r5的processor id就放0x0.表示unknown id。" Z& m! ]$ i# C* w) b' n

' ]6 o" W- p* W2 I6 C& R/ p__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
    , u, d4 T* `$ N/ Y: L- v: W
  2.     157 __lookup_processor_type:
    . h) D8 X, A& ]' m" i
  3.     158     adr r3, 3f, D& W; K2 c- }, P& h
  4.     159     ldmda   r3, {r5 - r7}
    ( J& `( |! X6 c7 C. T! _& h' c; {
  5.     160     sub r3, r3, r7          @ get offset between virt&phys
    3 N8 n% C: t/ }. K# t) ^# a
  6.     161     add r5, r5, r3          @ convert virt addresses to
    # A1 ]' U: h) O2 Z* B* h
  7.     162     add r6, r6, r3          @ physical address space
    5 x. t) F' b8 S" V; R
  8.     163 1:  ldmia   r5, {r3, r4}            @ value, mask4 ^( T' O. m) u1 s
  9.     164     and r4, r4, r9          @ mask wanted bits* `  R) j, H  Y/ M- j  |
  10.     165     teq r3, r40 D( U/ G4 C) u9 r0 O
  11.     166     beq 2f  t; U+ y! }- ?+ y# e* W
  12.     167     add r5, r5, #PROC_INFO_SZ       @ sizeof(proc_info_list)5 X* v3 G* y% c2 C$ h+ \. c
  13.     168     cmp r5, r6  D# g) b' p0 y7 l5 Z& F9 |
  14.     169     blo 1b
    $ {8 S0 e! z3 E5 _
  15.     170     mov r5, #0              @ unknown processor
    9 D2 F! i$ M+ S! N
  16.     171 2:  mov pc, lr
    % L" t( A+ w$ W4 H

  17. 0 C2 X' x" H% d% N4 u
  18.     187     .long   __proc_info_begin
    # H3 L3 V$ q  [; }
  19.     188     .long   __proc_info_end6 Q+ C  k" _) B( u, m, q
  20.     189 3:  .long   .
    1 N* l2 K* E* y% ^; {- S
  21.     190     .long   __arch_info_begin) P5 ]0 ~: e! ^& B
  22.     191     .long   __arch_info_end
複製代碼
跳了很多檔案,建議指令和object code如何link的概念要有,就會很清楚了。
3#
 樓主| 發表於 2008-10-13 17:20:35 | 只看該作者
我們從 head-common.S返回之後,接著繼續看。
9 [' d4 D. A5 H5 }! D  ?# G' X+ w
8 G4 \# |) p2 ]$ iline 84, movs的意思是說,做mov的動作,並且將指令的S bit設起來,最後的結果也會update CPSR。這個指令執行的過程當中,會根據你要搬動的值去把N and Z flag設好。這個是有助於下個指令做check的動作。/ t# ]! F$ k4 \4 j5 C3 B; I8 s  g
line 85, 就是r5 = 0的話,就跳到__error_p去執行。
! |/ n' C1 P4 G( o/ Vline 86, 跳到__lookup_machine_type。原理跟剛剛找proc的資料一樣。
  1.      83         bl      __lookup_processor_type         @ r5=procinfo r9=cpuid( y  k% w2 f, B) E( n  r
  2.      84         movs    r10, r5                         @ invalid processor (r5=0)?
    4 Q. Y- ?# }# C
  3.      85         beq     __error_p                       @ yes, error 'p'8 T& G1 g. @% B( v' y
  4.      86         bl      __lookup_machine_type           @ r5=machinfo
複製代碼
看得出來跟proc很像,有個兩個小地方是
& m1 m6 u' @$ R) A8 B' r+ d5 Q
6 m- ~2 ?  u1 I. o9 u! f1. line 207,用ldmia不是用ldmda。原因就在於存放arch 和 proc info 的位址剛好相反。一個在lable3的上面。一個在的下方。設計上應該是可以做修改的。
$ K! F' F  f# ^4 [; N/ j( v1 `1 W
3 D' o, H  b1 H; A: K! L* ^/ c# d2. arch定義的方式是透過macro,可以先看 ./include/asm-arm/mach/arch.h。裡頭有個 MACHINE_START ,這邊會設好macro,到時候直接使用就可以把arch的info宣告好。 例如 ./arch/arm/mach-omap1/board-generic.c
  1. /* macro */- \! u* I/ O8 N* s
  2.      50 #define MACHINE_START(_type,_name)                      \
    8 j, c; `5 N* `4 c6 U7 L1 K: _
  3.      51 static const struct machine_desc __mach_desc_##_type    \
    : b! b' U" X3 D0 {1 ?
  4.      52  __used                                                 \0 U/ p! b% f3 V: S9 L
  5.      53  __attribute__((__section__(".arch.info.init"))) = {    \3 f: z* F4 s8 n4 G# N
  6.      54         .nr             = MACH_TYPE_##_type,            \
    . E1 g" W* w5 w: X/ t
  7.      55         .name           = _name,
    " I, _, c* Q  P$ K2 w$ T, X/ X
  8.      56
    : A/ m* q2 n( T0 G; E, F; U: ?
  9.      57 #define MACHINE_END                             \
      p9 N/ l- r# ?; U  W
  10.      58 };- u2 R; s* A1 f: N0 Q3 F
  11.      /* 用法 */9 l) i6 a- q% E$ Y- J9 m
  12.      93 MACHINE_START(OMAP_GENERIC, "Generic OMAP1510/1610/1710")5 m0 G% B9 m0 z
  13.      94         /* Maintainer: Tony Lindgren <tony@atomide.com> */
    4 e1 G5 q( d7 d9 o5 m
  14.      95         .phys_io        = 0xfff00000,
    * r, m+ k1 w" z
  15.      96         .io_pg_offst    = ((0xfef00000) >> 18) & 0xfffc,
    9 l, e2 e6 Q) h' I1 B* V
  16.      97         .boot_params    = 0x10000100,
    . J( S( ~1 f' n8 b  i' B, z
  17.      98         .map_io         = omap_generic_map_io,
    1 z# h- `5 K* u9 c( l6 [, j
  18.      99         .init_irq       = omap_generic_init_irq,* G) s: _' q( k" G
  19.     100         .init_machine   = omap_generic_init,! x: X5 N, y! z' @& Z; r
  20.     101         .timer          = &omap_timer,. x! H9 u6 ^& `' e
  21.     102 MACHINE_END
    0 j9 Q2 @2 ?$ }; i; w
  22. 1 r$ i6 P  g# D1 @6 l0 X: G
  23.     /* func */* N2 N& z% ?0 @2 g' u
  24.     204         .type   __lookup_machine_type, %function2 p9 ]1 l9 T6 P- l5 U# Z' d# X
  25.     205 __lookup_machine_type:
    , [* C2 @$ W6 Q/ ?, @! ~4 e) ]
  26.     206         adr     r3, 3b( E7 F% b/ L2 n
  27.     207         ldmia   r3, {r4, r5, r6}6 @) @& a: F: q4 C2 N5 A
  28.     208         sub     r3, r3, r4                      @ get offset between virt&phys9 k4 U" }+ h6 _6 p: n& I' X
  29.     209         add     r5, r5, r3                      @ convert virt addresses to* z  z  }) S$ ?: f/ C" P6 m
  30.     210         add     r6, r6, r3                      @ physical address space
    6 B8 ~: L* I# i! F
  31.     211 1:      ldr     r3, [r5, #MACHINFO_TYPE]        @ get machine type  P5 K2 a8 M' B/ A
  32.     212         teq     r3, r1                          @ matches loader number?
    # j4 B3 o5 M4 ]9 W3 Z$ ~9 S4 B
  33.     213         beq     2f                              @ found* z  U+ b0 I5 E- M
  34.     214         add     r5, r5, #SIZEOF_MACHINE_DESC    @ next machine_desc
    ; I+ N/ O  l) i' |
  35.     215         cmp     r5, r6' ?, V$ e- h+ B! R1 o
  36.     216         blo     1b
    9 j+ R9 g$ k, n4 `; m; P( h  R
  37.     217         mov     r5, #0                          @ unknown machine, H! u8 e- v" l/ x% e
  38.     218 2:      mov     pc, lr
複製代碼
4#
 樓主| 發表於 2008-10-13 17:56:46 | 只看該作者
接著我們又返回到head.S,
7 y' I  `* X+ N) p7 K8 N: yline 87~88也是做check動作。- z+ v0 Z- A$ k
line 89跳到vet_atags。在head-common.S
  1.      87         movs    r8, r5                          @ invalid machine (r5=0)?# p" E4 ?+ V+ P! ]' _8 a( x
  2.      88         beq     __error_a                       @ yes, error 'a'
    & ~) \* J* q. O+ Y* b
  3.      89         bl      __vet_atags
    - }0 _, L# ]. q0 V4 X0 D0 m
  4.      90         bl      __create_page_tables
複製代碼
line 245, tst會去做and動作。並且update flags。這邊的用意是判斷位址是不是aligned。/ o4 Q2 R2 s7 n8 Q+ W$ _
line 246, 沒有aligned跳到label 1,就返回了。
- K6 p6 f* g. Xline 248~250, 讀取atags所在的address裡頭的值到r5,看看是否不等於ATAG_CORE_SIZE,不等於的話也是返回。
* K/ e( t! B, H: }, gline 251~254, 判斷一下第一個達到的atag pointer是不是等於ATAG_CORE。如果正確的話,等一下要讀取atag的資料,才會正確。6 w2 c6 K9 N5 x( [
(atag是由bootloader帶給linux kernel的東西,用來告知booting所需要知道的參數。例如螢幕寬度,記憶體大小等等)
  1.      14 #define ATAG_CORE 0x54410001$ P- i0 `2 }/ [+ y8 ?& z- D! l
  2.      15 #define ATAG_CORE_SIZE ((2*4 + 3*4) >> 2)
    1 Z) k! h! h0 c$ _- B$ S. Q% W

  3. 0 E1 v4 f" T' [+ B$ z$ [
  4.     243         .type   __vet_atags, %function8 |& `( h. |$ F- H& P& n. X6 V
  5.     244 __vet_atags:0 V" R, S: b- e! j1 v: c
  6.     245         tst     r2, #0x3                        @ aligned?
    , _& h2 n2 g, ~9 u" M. Q
  7.     246         bne     1f5 m3 u% \7 m( h8 l/ u# y7 R; n
  8.     247
    ; A9 @0 X; L7 |" ^6 e
  9.     248         ldr     r5, [r2, #0]                    @ is first tag ATAG_CORE?: u. X' T) R5 f4 F' R2 |. g3 K+ ~
  10.     249         subs    r5, r5, #ATAG_CORE_SIZE! p+ v' n4 Q# [+ ^
  11.     250         bne     1f4 Z* N8 y( _4 B
  12.     251         ldr     r5, [r2, #4]
    0 f5 H# c" m8 G6 e
  13.     252         ldr     r6, =ATAG_CORE* O( h7 H$ U. Y( J* Q3 S3 w
  14.     253         cmp     r5, r6
    4 H" D# G4 o+ f2 ?9 T5 W
  15.     254         bne     1f2 `  [$ h1 f- r: W
  16.     2558 m: n3 N/ B' p& b
  17.     256         mov     pc, lr                          @ atag pointer is ok
    & M1 _- U( K8 [1 n+ J
  18.     257
    0 G, \4 T% o! K( U' Y) U, G
  19.     258 1:      mov     r2, #0/ [% A( D% d- a
  20.     259         mov     pc, lr
複製代碼
接著我們又跳回去head.S。  
4 j% S7 c$ }2 n8 {( a- d1 ^line 90,又跳到 __create_page_tables。   (很累人....應該會死不少腦細胞)2 |8 ?" j7 R2 d+ v1 n: f& Z
哇!page table?!!如雷貫耳的東西,不知道會怎麼做。剛剛偷看了一下,@@還蠻長了,先到這邊好了,下次在繼續寫。
5#
 樓主| 發表於 2008-10-14 12:13:51 | 只看該作者
由於code看起來似乎越來越難解釋,會需要在檔案之間跳來跳去。建議一些基礎的東西可以再多複習幾次(其實是在說我自己 ),閱讀source code上收穫會比較多。例如:3 P0 f: e7 L% S. I: y

' ?' K1 Z; Y, C1. arm instruction set - 這個最好每個指令的意思都大略看過一次,行有餘力多看幾個版本,armv4, v5 or v6。( G! N5 M2 w* {/ n# l+ D
2. compiler & assembler & linker - toolchain工具做的事情和功能,大致的流程和功能要有概念。* G* P3 L2 E/ E. K
3. Makefile & link script - 這兩個功能和撰寫的方式要有簡單的概念。
" T2 r. Q9 ?+ T7 K2 W) ]4 n) {4 w  L8 @% J$ V3 Q
以上1是非常重要的重點,2&3只要有大致上的概念就可以,因為trace code的時候,有時需要跳到Makeflie&Link script去看最後object code編排的位址。
0 F2 ~8 P: q; A1 k6 B  B0 n8 v9 y( [  Y3 a. i  a4 i, x
由於我們trace到了page table這個關鍵字,在開始之前,稍微簡短的解釋,為了幫助了解,儘量用易懂的概念講,有些用詞可能會和一些真實狀況不同,但是懂了之後,應該會有能力分辨,至於正式而學術上解說,很多書上應該都有,或是google一下就很多啦∼
6#
 樓主| 發表於 2008-10-14 12:14:47 | 只看該作者
page table本身是很抽象的東西,尤其是對所謂的 user-mode application programmer 來說,大部分的狀況它是不需要被考慮到的部份,但是他的產生卻對os和driver帶來了很多影響。我們從一個小小的疑問出發。$ o9 X% P( C3 m
1 \+ b  D) h" H& x
『產生page table到底是要給誰用的?』
9 W% h6 D2 K0 S1 ]8 i; H1 ?. `7 ~
, H: q* I' f5 i6 k; N. `# m" b其實真正作用在它上面的H/W是MMU,一旦CPU啟用了MMU,當cpu嘗試去記憶體讀取一個operand的時候,cpu內部打出去的位址訊號都會先送到mmu,mmu會拿著這個位址去對照page table,看看這個位址是不是被轉換到另外一個位置(還會確認讀寫權力),最後才會到真正的位址去讀寫。
& @: a" R- q3 x! {6 @# Z; S4 w# n& ?/ ~
這樣來看CPU其實一開始打出去的位址訊號其實不是最後可以拿到資料的位址,我們稱為virtual address(va),mmu打出來的位址才能真正拿到資料,所以我們把mmu打出去的位址稱為physical address(pa)。那用來查詢這個位址對照關係的表格,就是page table。
, c5 o6 v: E2 D' k: a0 m
0 r  z" ~4 m' d5 o到這邊我們有一個簡單的概念。來想像一個小問題,一個普通的周邊(例如你的顯示卡),因為他並不具有MMU功能,hw只看得懂pa,但是CPU卻是作用在va,假如我想去對hw的控制暫存器做讀寫,到底要用va還是pa?
7#
 樓主| 發表於 2008-10-14 12:15:51 | 只看該作者
這時,寫driver的人就必須要小心,設定給硬體看的位址必須要先轉成pa(像是設定DMA),給CPU執行的程式碼要使用va,這對一開始嘗試寫driver但是又不瞭解va pa之間的差別的人,常常產生很多疑問。9 d7 A, K, S) L! D
) q: Q8 ^6 e! X) [) g5 V
現在我們回頭想想OS,既然我們跑到create page table,可以預期的是他想要將MMU打開,因此希望預先建立起一個page table,讓MMU知道目前os想規劃的位址對應和讀取權力是如何被安排的。所以os必須考慮到現在的系統究竟是長怎樣?應該要如何被安排?是不是有那些要被保護?好吧∼因為我們完全對os不了解,也不知道該安排什麼,只能祈禱在trace code完後,可以找到這些問題的答案,或者發現一些沒想到的問題。2 ?: |& h: K9 Z

* H5 e+ S0 _9 S/ h, L知道了page table的大致上的功能,下篇就可以專心的研究這個table的長相,和它想規劃出的系統模樣。5 h; Z1 ?: Z- y- ^

5 A/ i+ ?' v) \4 {p.s. 字數限制好像變短了。   (看來很難寫)
8#
 樓主| 發表於 2008-10-14 13:56:17 | 只看該作者
由於字數限制挑整,用詞儘量精簡,以便可以貼必要source。另外,以後寫到一段落,考慮收集成一篇完整的文章,這樣應就不會因為用回覆的方式,造成閱讀斷斷續續的問題。希望有人對文章呈現方式有想法的話,可以跟我講。
9#
 樓主| 發表於 2008-10-14 13:57:39 | 只看該作者
現在,讓我們跳入create_page_tables吧∼% m8 o' [& P! W9 `; t
line 216,會跳到pgtbl的macro去執行,其實只是載入一個位址。
5 \" L, s# h1 f, {4 [. j) g' L. `0 M, D2 a  d3 D3 p9 W
只是這個位址因為你硬體規劃dram位置不同,所以必須可以變動。一般會定義在./include/asm-arm/arch-你的平台/memory.h,我們看得出來dram開始的地方是從0x8000 offset(text_offset)開始算,猜測可能一開始有保留空間給kernel使用。實際算page table的時候有減去0x4000,表示是從DRAM+0x8000-0x4000開始放pg table.
  1. /* arch/arm/Makefile */
    " n& Y, o; J; B; e' {
  2.      95 textofs-y       := 0x00008000
    . X: o- I6 r- ~& z
  3.     152 TEXT_OFFSET := $(textofs-y)
複製代碼
  1. /* include/asm-arm/arch-omap/memory.h */6 I  z- U* N- G: C
  2.      40 #define PHYS_OFFSET             UL(0x10000000)5 V! I" T8 `+ F

  3. ' R0 s- W! G* H
  4.      /* arch/arm/kernel/head.S */7 L) q# E9 M; `8 X, U' `8 [3 j5 @
  5.      29 #define KERNEL_RAM_VADDR        (PAGE_OFFSET + TEXT_OFFSET), ~% t6 \: Y& a
  6.      30 #define KERNEL_RAM_PADDR        (PHYS_OFFSET + TEXT_OFFSET)
    4 Z0 A0 {5 F* Z+ D4 D

  7. " ?3 N/ ~! L3 n9 V
  8.      47         .macro  pgtbl, rd
    9 B4 I7 f. C7 p+ Y" r
  9.      48         ldr     \rd, =(KERNEL_RAM_PADDR - 0x4000)( V: |: N3 F. N$ F& d* u( B
  10.      49         .endm
    1 }4 Q3 C/ u  U- D( A# ]
  11. # `4 L4 H; `9 Z! p
  12.     216         pgtbl   r4                              @ page table address
複製代碼
10#
 樓主| 發表於 2008-10-14 14:16:53 | 只看該作者
得到pg table的開始位置之後,當然就是初始化囉。7 x% b( q6 P( D6 D
line 221, 將pg table的base addr放到r0.
7 S% f. i. D1 p/ ~; C& Q$ {line 223, 將pg table的end addr放到r6.3 K. G1 Y3 l- i$ ^$ f
line 224~228, 反覆地將0x0寫到pg table的區段裡頭,每個loop寫16bytes. (4x4),直到碰到end,結果就是把它全部clear成0x0.
  1.     221         mov     r0, r4
    3 b, @8 V3 o$ o
  2.     222         mov     r3, #0, k5 e0 J1 x2 j! m
  3.     223         add     r6, r0, #0x4000
    & k8 c+ N$ `% V5 y
  4.     224 1:      str     r3, [r0], #4
    3 \% q# o6 V2 ^3 [, {3 k* k
  5.     225         str     r3, [r0], #4
    9 p; f# H  X0 B4 {8 ?
  6.     226         str     r3, [r0], #4
    % v* K0 t  k! C4 t6 a
  7.     227         str     r3, [r0], #4
    6 E; R* q  m4 X, f/ D
  8.     228         teq     r0, r6
    4 B2 B# v* S8 e& y
  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 | 只看該作者
問題怎麼填值??
. J% F1 \- [% p" p* x拿出ARM的手冊,翻到MMU章節。一看發現page有很多種,還得分first level和second level。
$ @3 S- \! y7 G8 w% d7 [/ f6 P
; ~3 O: \0 Y' x: W  ~念書時的印象,是從1st level在去查2nd level,先看怎麼設定1st level (不知以前老師有沒有亂教)
; o) j# P0 h  F! G* ^9 c# T* `# ]% w1. [31:20]存著section base addr
4 p  ~- Q( C' N2. [19:2]存著mmu flags( W* H# q4 }; u. r$ Q: n$ u* W8 c
3. [1:0]用來辨別這是存放哪種page, 有四種:! b% f+ H- Z, T
   a. fault (00) b. coarse page (01) c. section (1st level) (10) d. fine page (11)
1 n$ W" P9 {* _  t: z- B! K0 @4. pg tabel資料要存放到 [31:14] = translation base, [13:2] = table index , [1:0] = 0b00 的位址( q' @; B& X4 Q" N
" \: C1 N8 @7 r  y1 O8 U; x: z
來看code是怎麼設定。) O7 J1 v$ j" x3 L
: i% [4 b8 K0 B: N
line 239, 將pc的值往右shift 20次放到r6.這是取得前20高位元的資料,有點像是 1.。
( I; C. t# y" g. H! d* K9 T' {line 240, 將r6往左shift 20次之後,和r7做or的動作,剛好也是 2.提到的mmu flags和1.處理好的資料做整理。6 E6 E0 }* v: u- n2 S3 S9 b
所以前面兩個做完,就完成了bit[31:2]。+ a" P4 j) f! D) Q) h- Q+ k
line 241, 將r3的資料寫到r4+(r6<<0x2)的地方去,剛好是4.提到的位址。(lucky)
  1.     239         mov     r6, pc, lsr #20& g$ @; d  ]2 h5 X% `
  2.     240         orr     r3, r7, r6, lsl #20+ D1 k6 \, P. ~6 M* S4 }( f
  3.     241         str     r3, [r4, r6, lsl #2]
複製代碼
p.s. 用pc值剛好可以算出當前的page。
12#
 樓主| 發表於 2008-10-22 19:47:03 | 只看該作者
最近又被釘上了,開始忙碌,大概沒太多時間貼文∼
- G9 i3 K+ o# Z& A5 t
2 E3 A8 i" [" r# e, T上篇已經將pc所屬的page table entry(pte)設定好,接著繼續看! T- u2 n- z1 \& t
line 247, 248, 將KERNEL_START的位址往右shift 18算出pte的offset,(等於line239&241,239&241是先shift right 20然後shift left 2),並將剛剛r3的值設給pte。『!』的意思是會把address存到r0。
5 G" {- z% e$ R% V" a' A9 l6 t/ G
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. T, j) ]$ f  N" P8 f
  2.     248         str     r3, [r0, #(KERNEL_START & 0x00f00000) >> 18]! & Q1 B0 H3 {0 }/ Z$ A8 c+ r2 }
  3.     249         ldr     r6, =(KERNEL_END - 1)
    5 }) c$ ?$ O. Y% F3 B3 n
  4.     250         add     r0, r0, #4
    2 z9 V3 N8 `+ K3 Z6 X: ?, j) g
  5.     251         add     r6, r4, r6, lsr #18" ?- {0 L- O5 J. `  G. n
  6.     252 1:      cmp     r0, r6
      c$ ]" {" Y( H* d; f$ o4 t. R; H
  7.     253         add     r3, r3, #1 << 20! q& J2 B! m& p' Z* @/ T  I
  8.     254         strls   r3, [r0], #4
    6 v: g5 z' t& N: {
  9.     255         bls     1b
複製代碼
13#
 樓主| 發表於 2008-10-22 20:24:58 | 只看該作者
line279,PAGE_OFFSET是規範RAM mapping完後的virtual address,我們將這個位址所屬的pte算出來放到r0。
. c* ], S9 N; z+ o6 cline 280~283,將要 map 的physical address的方式算出來放到r6。/ a1 c7 D; Z; k% d
line 284,最後將結果存到r0所指到的pte。
  Z: ?& k' O: t( m, p& t
: V2 a! l! C7 A+ q以上三個動作,就是做好 ram 起頭的位址一開始map,還沒做完整塊map。由於我們目前的設定方式每塊是1MB,所以這邊是將RAM開始的1MB做map。7 d& R* y4 {/ o% g& \% `
  X/ a: S2 G8 Y' z- E0 f
line 327,返回,結束一開始的create page table的動作,我們可以看出其實並沒有做完整的初始化page table,所以之後應該會有其他page table的細節。
  1.     279         add     r0, r4, #PAGE_OFFSET >> 18
    " X) f. C3 ?( y2 `
  2.     280         orr     r6, r7, #(PHYS_OFFSET & 0xff000000)9 ^3 d+ s+ Q1 s+ N, m
  3.     281         .if     (PHYS_OFFSET & 0x00f00000)
    $ C1 S" F6 ]! Z- E+ v. `6 n9 F+ c
  4.     282         orr     r6, r6, #(PHYS_OFFSET & 0x00f00000)
    : W; Y1 ?% B: x" c4 f
  5.     283         .endif2 q, v' [8 S! c/ L2 Z$ Z
  6.     284         str     r6, [r0]
    * w/ |) a. o0 q5 D
  7.     327         mov     pc, lr
複製代碼
附帶一提,我們這邊省略了一些用ifdef包起來的程式碼,像是一開始會印一些output message的程式碼(line286~326)。
14#
 樓主| 發表於 2008-10-22 20:37:08 | 只看該作者
自create page table返回後,我們偷偷看一下接下來的程式碼,; ^( d! N% C/ c
line 99, 將switch_data擺到r13
) ~9 |4 I+ p+ d2 Dline 101, 將enable_mmu擺到lr3 }$ v: i5 O1 f4 X, b5 h5 d
line 102, 將pc改跳到r10+PROCINFO_INITFUNC的地方去
+ c* E3 y1 c) u; ?& Y
; d8 I# m' F9 K& h9 A# p7 e其實這邊有點玄機,switch_data和enable_mmu都是function,結果位址都只是被存起來,並沒有直接跳過去執行。其實,雖然程式碼是順序switch_data->enable_mmu->proc init function,但其實執行的順序會是 procinfo 的init function-> enable_mmu -> switch_data 。至於,為什麼要這樣寫的原因?就不清楚了,也還沒仔細推敲過。 + z4 C. p7 B7 h+ M9 r4 d" H' {; N

0 B3 }( r0 h- F0 |- Bswitch_data最後就會跳到大家都很熟悉的start_kernel(). 詳細要賣個關子,最近會忙一下,得要過一陣子才能貼文∼  
  1.      99         ldr     r13, __switch_data              @ address to jump to after
    7 V4 k* }! Y  V' M0 _
  2.     100                                                 @ mmu has been enabled
    5 ]! O/ A1 x8 }  N2 n4 }2 p
  3.     101         adr     lr, __enable_mmu                @ return (PIC) address' ?( T4 G" o+ c
  4.     102         add     pc, r10, #PROCINFO_INITFUNC
複製代碼
15#
 樓主| 發表於 2009-7-4 01:09:36 | 只看該作者
老店重新開張~
2 ?) j, I/ \7 y0 _9 [! }, I
* d) T3 N' ?; j花了一些時間把舊的貼文整理到一個blog
! |  ]  `  U' r有把一些敘述修改過: J# i0 A8 g1 M
希望會比較容易集中閱讀) J: G) V. b9 X
目前因為某些敘述不容易- g8 D% n2 F# D2 T/ u
還是比較偏向筆記式而且用字不夠精確
# w3 q% f' M! j7 a4 l" w% R- {希望之後能夠慢慢有系統地整理
& g' z3 f' `# s, B$ p大家有興趣的話
+ n. P. j! a6 }5 o' g& l3 z9 G可以來看看和討論
  h# I4 F! a/ x# Ihttp://gogojesseco.blogspot.com/
" d' c  c! t4 s# N5 ]6 u; Z  G3 b& {8 A( Z
以後可能會採取  先在chip123貼新文章
& B4 ]4 R2 N/ P( q4 u$ S  z3 D慢慢整理到blog上的方式
2 {% a( W: h. v$ K8 ~因為chip123比較方便討論 =)# S% e" O* p, j0 ^( h
blog編輯修改起來比較方便: E1 A9 b' U5 a, Z7 w4 V4 E9 i# l
閱讀也比較集中   大家可以在這邊看到討論
4 A& p7 c0 m- F  W4 }1 s然後在blog看到完整的文章 (類似BBS精華區的感覺)

評分

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

查看全部評分

16#
 樓主| 發表於 2009-7-15 17:07:03 | 只看該作者
隔了很長一段時間沒update$ j& Q7 f4 z& }- _4 d
之前程式碼走到 ./arch/arm/kernel/head.S 的 line 99 附近
  1.      99         ldr     r13, __switch_data              @ address to jump to after
    + G( \5 ^' h! k) b% `7 n
  2.     100                                                 @ mmu has been enabled( I& d5 u6 s* o
  3.     101         adr     lr, __enable_mmu                @ return (PIC) address, b: \, f- R; t! M% F
  4.     102         add     pc, r10, #PROCINFO_INITFUNC
複製代碼
line 99, 將__switch_data放到r13。(留作之後用)
; K3 q* h& X' j/ zline 101, 將__enable_mmu的addr放到lr。(留作之後用); y- i" \8 `: |1 k- z9 j
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, #function5 d5 o. \! O3 _9 F- Q% V
  2. 374 __arm926_setup:! T6 x8 n2 w( `* h8 }
  3. 375         mov     r0, #02 g9 R7 ^6 h6 y6 O$ W$ |
  4. 376         mcr     p15, 0, r0, c7, c7              @ invalidate I,D caches on v4  b; r7 Y) C+ T7 R  `8 i- V* w
  5. 377         mcr     p15, 0, r0, c7, c10, 4          @ drain write buffer on v46 x1 A# y- K- W/ \
  6. 378 #ifdef CONFIG_MMU; W7 q5 f0 Y, d8 j" {
  7. 379         mcr     p15, 0, r0, c8, c7              @ invalidate I,D TLBs on v4' j- R* n- y7 `. O4 m
  8. 380 #endif& t. q5 q$ ?9 e) q7 N
  9. 7 g: @7 W9 @! O* m7 w
  10. 388         adr     r5, arm926_crval5 w! p6 Y9 I4 ^3 Q; V8 t
  11. 389         ldmia   r5, {r5, r6}& B3 w3 G5 C) K( `
  12. 390         mrc     p15, 0, r0, c1, c0              @ get control register v4
    7 A( V# C9 r. H. C$ V( p: p
  13. 391         bic     r0, r0, r57 E/ |9 @" d* s
  14. 392         orr     r0, r0, r6
    : C) [! {  a& E3 B; B

  15. # w0 f7 W+ }) H$ r& G5 H
  16. 396         mov     pc, lr+ @* p! J8 X3 j  H; ~5 G; A8 ]
  17. 397         .size   __arm926_setup, . - __arm926_setup
複製代碼
這邊的程式碼就跟CPU有很大的相依性,
+ |2 W6 P# v! X+ S$ p! rline 375~380, 主要就是invalidate CPU的I&D cache和清空write buffer。
4 H- ?  X4 @  {( r+ Z, m/ ?line 388~392, 把cp15的設定讀出來,並且將一些預設狀態做好運算放到r0。(預設值從arm926_crval這邊可以取得。)1 m3 [8 o7 ]5 I! }5 I3 j3 m
line 396, 直接把pc跳到lr,因為我們之前已經將lr = enable_mmu,所以直接跳過去。
17#
 樓主| 發表於 2009-7-15 17:29:45 | 只看該作者
  1. 155 __enable_mmu:7 u: v! S8 C+ H% g
  2. 170         mov     r5, #(domain_val(DOMAIN_USER, DOMAIN_MANAGER) | \
    * M5 T1 q& c1 m* X' g! m
  3. 171                       domain_val(DOMAIN_KERNEL, DOMAIN_MANAGER) | \! |. H9 }. g$ w; {, t
  4. 172                       domain_val(DOMAIN_TABLE, DOMAIN_MANAGER) | \* j, s9 C" |- ?/ E  v- p
  5. 173                       domain_val(DOMAIN_IO, DOMAIN_CLIENT))
    0 R4 |& W2 x2 |9 |& m( S( s9 o
  6. 174         mcr     p15, 0, r5, c3, c0, 0           @ load domain access register
    - _" L% A. c9 F/ h9 A: n, U" e8 W  K
  7. 175         mcr     p15, 0, r4, c2, c0, 0           @ load page table pointer" p+ J8 d! M( ~! h8 e
  8. 176         b       __turn_mmu_on# R7 o8 A1 o9 U8 _! [) n" Y) z
  9. 177 ENDPROC(__enable_mmu)
複製代碼
line 170~174,設置好domain access。(domain access可以先當成設置access的權限,或許以後可以寫詳細的文章說明)8 B% ~% a$ m+ B; A5 S5 y
line 175~176,將create好的page table開頭丟給mmu。跳到turn_mmu_on
  1. 191 __turn_mmu_on:# Q* o+ m9 Y$ J6 A* W
  2. 192         mov     r0, r0+ W# a4 v, v7 ^6 k% G8 h
  3. 193         mcr     p15, 0, r0, c1, c0, 0           @ write control reg4 L( y% j) `8 |; S: [# l
  4. 194         mrc     p15, 0, r3, c0, c0, 0           @ read id reg
    $ x9 G" D9 x0 [% u$ i6 @
  5. 195         mov     r3, r3% h+ K* s" Y) \$ Q) {# x# i# H5 Q! [
  6. 196         mov     r3, r3
    ' f5 `, @1 M. W' s  p
  7. 197         mov     pc, r13# t/ n/ p3 d! z/ b) w
  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:1 ^+ y( g: D  r6 y4 |- ?' i9 K
  2. 19         .long   __mmap_switched
    / K8 o+ q" A% A& ^5 M
  3. 20         .long   __data_loc                      @ r4$ V% p5 w5 w+ C* F/ v; B7 L/ z
  4. 21         .long   _data                           @ r5
    ; x9 a( s1 c3 |8 J" f; M4 j6 _, F
  5. 22         .long   __bss_start                     @ r6
    " D: `) X( m  E8 @# b8 f% P% Q! F
  6. 23         .long   _end                            @ r7; y7 {: x+ s" f" u/ X; D7 }
  7. 24         .long   processor_id                    @ r47 [& o: ?! h, @% H7 P
  8. 25         .long   __machine_arch_type             @ r56 z+ @1 k' ?! K8 c
  9. 26         .long   __atags_pointer                 @ r6
    . V6 e+ d  n( j) Z2 Y
  10. 27         .long   cr_alignment                    @ r7" e; B! z% p8 F, x8 R
  11. 28         .long   init_thread_union + THREAD_START_SP @ sp0 T& v: }5 Z* V" F7 Z- E6 n2 Z
  12. 29
    9 S6 L5 g8 _8 L( [
複製代碼
switch_data的第一行就是 __mmap_switched,所以我們直接看line 39。
18#
 樓主| 發表於 2009-7-15 17:30:00 | 只看該作者
  1. 39 __mmap_switched:1 @" A  k! w2 a: e7 S/ }3 M8 v
  2. 40         adr     r3, __switch_data + 49 w- x. \' ~( [, f: P- A
  3. 41; S2 R/ Q  ~% C% g! ^
  4. 42         ldmia   r3!, {r4, r5, r6, r7}
    0 o. M9 r6 L5 u. C! M5 S
  5. 43         cmp     r4, r5                          @ Copy data segment if needed
    : x3 J7 p' z8 [, B
  6. 44 1:      cmpne   r5, r60 K/ M) \: i: J; }1 w' A
  7. 45         ldrne   fp, [r4], #4
    7 b- t" _5 p* O0 _
  8. 46         strne   fp, [r5], #4
    . ^. x# m' j7 y9 Y! ^
  9. 47         bne     1b( J% M% W  x" W5 W4 T: D
  10. 48% `5 [3 q- N! ~2 Q1 W' n* G
  11. 49         mov     fp, #0                          @ Clear BSS (and zero fp)
    * _- Z" P- s' ~( I1 M
  12. 50 1:      cmp     r6, r7
    ! n. Y( D" z/ y& `  M1 E3 N
  13. 51         strcc   fp, [r6],#4. P! ~8 d* o- p/ m- g9 {6 V
  14. 52         bcc     1b8 p5 N* u$ d7 C) D# p
  15. 53) G# }; \+ Z8 q# P; a
  16. 54         ldmia   r3, {r4, r5, r6, r7, sp}/ E8 r1 @; a) s+ t9 @9 [
  17. 55         str     r9, [r4]                        @ Save processor ID, N, |, y6 W$ I1 x
  18. 56         str     r1, [r5]                        @ Save machine type( R4 @6 w4 |0 _; B5 e3 }( S7 Y
  19. 57         str     r2, [r6]                        @ Save atags pointer
      a# R6 M7 b7 e0 E) F
  20. 58         bic     r4, r0, #CR_A                   @ Clear 'A' bit
    % J- K, @5 b. N" c, L  H8 x
  21. 59         stmia   r7, {r0, r4}                    @ Save control register values8 J2 U+ l1 b/ m% @* m- o
  22. 60         b       start_kernel
    + I# _" y: K/ |3 v5 k* H3 P% s' c
  23. 61 ENDPROC(__mmap_switched)
複製代碼
switch_data的第一行就是 __mmap_switched,所以我們直接看line 39。/ B; o& o( J1 x4 f  D8 M
line 39,將__data_loc的addr放到r3
* w1 ]+ L3 ?4 n3 p5 e6 Bline 42,從r3的位址,連續讀取四筆資料到r4, r5, r6, r7% G0 a% [3 z/ c% [
line 43~47,看看data segment是不是需要搬動。
, V3 g0 s# \& F8 K# ]( \$ sline 49~52, clear BSS。( x* X3 F, n4 V; Z
9 s( E* S  G0 ]4 B3 I1 t
由於linux kernel在進入start_kernel前有一些前提必須要滿足:( T& R8 F6 Z4 r* T
r0  = cp#15 control register  p/ P/ o: E' i$ e/ r, r+ b3 {
r1  = machine ID. @: E  ?+ V9 n" b
r2  = atags pointer
. X$ \3 x" ^! [* [) x4 Q6 D! }r9  = processor ID
* o( N( j4 G; u. |( h, z7 }# t- x9 M% B  Y' d. ]+ r9 r
所以line 54~59就是在做這些準備。% n: Z. H& F& ~. W$ L
最後呢? 我們就跳到start_kernel了。(而且還是用b start_kernel,表示我們不會再回來了)
5 k& v7 C. ~1 R2 R5 ?; X$ V' l' v9 t/ V3 N+ f
看一下start_kernel()在./init/main.c,終於跳出architecture specific的目錄,表示
" W6 A2 ~  X, g: ?1 I我們真正的開始linux kernel的初始化。* N4 I5 O  w) o" F" L6 h
像是 shedule init, console init, memory init, irq init等等都在start_kernel裡頭。
. c9 G) j; `/ u/ o3 Q  T到這邊之後,應該就可以深入linux kernel中,各項比較跟hardware不那麼相關的軟體部分。

評分

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

查看全部評分

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

本版積分規則

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

GMT+8, 2024-6-7 11:55 PM , Processed in 0.157520 second(s), 23 queries .

Powered by Discuz! X3.2

© 2001-2013 Comsenz Inc.

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