Chip123 科技應用創新平台

 找回密碼
 申請會員

QQ登錄

只需一步,快速開始

Login

用FB帳號登入

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

trace linux kernel source - ARM - 03

[複製鏈接]
跳轉到指定樓層
1#
發表於 2008-10-7 14:00:18 | 只看該作者 回帖獎勵 |倒序瀏覽 |閱讀模式
到目前為止,我們已經進展到kernel幫自己relocate完,並解將自己解壓縮到一個地方要準備開始執行,那疑問來了?到底是跳到哪裡去了,因為./compressed/head.S最後一行居然是
6 o4 C3 _* l& T& Z『mov pc, r4』. _4 k9 Z8 \8 N! o6 `
r4只代表了解壓縮完後kernel的位址,那究竟整包kernel編譯的時候,哪個function哪個東西被放在最前面咧?!
0 j7 X6 r/ O" ]9 s. z
5 z1 z( i5 F( D; t- j: B所以我們又必須開始找於kernel link的時候是怎麼被安排的,有了前面的基礎,我們可以從Makefile知道程式碼如何被編譯。至於link上的細節,例如有那些section和section先後順序等等,可以從 lds 檔來規範。- S6 \" N' |( W# O2 T1 ~, \1 N
+ C* J6 `7 B) f6 V6 E* J7 p
有興趣的人可以看一下 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)
; H7 w0 {/ @2 h! L' J, K, }% |我們可以發現第一個section是『.text.head』,裡頭的_stext從目前的位置開始放。
4 K' h1 V2 L! t2 X於是我們曉得只要找到屬於.text.head這個section,並且是_stext這個symbol的程式碼,就是解壓縮完後的第一行程式碼。
  1.      26     .text.head : {- ~4 a$ b7 `6 l# y* r5 P# i
  2.      27         _stext = .;
    / b/ R' g/ e5 b& \7 L
  3.      28         _sinittext = .;
    7 {5 A" x" o; k
  4.      29         *(.text.head)- d2 h! C2 K" v$ C0 @8 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") X8 ^9 t  N- u4 ?3 P) v
  2.      78     .type   stext, %function, L2 z; f. A- b  }0 J4 E% j: s4 `
  3.      79 ENTRY(stext)
    . o* g4 O8 j/ E" T- s. K, p+ R
  4.      80     msr cpsr_c, #PSR_F_BIT | PSR_I_BIT | SVC_MODE @ ensure svc mode. ^, p- x, S9 N% k0 n* H0 d6 z
  5.      81                         @ and irqs disabled  ]: S- b) y; j% q! i1 e
  6.      82     mrc p15, 0, r9, c0, c0      @ get processor id
    , Q0 J! ?0 v; `; b; P9 u$ S. W! R
  7.      83     bl  __lookup_processor_type     @ r5=procinfo r9=cpuid
    1 e1 z9 q7 u; f5 [! b. ~" c
  8.      84     movs    r10, r5             @ invalid processor (r5=0)?
    + e% _! r' u* E: u/ n
  9.      85     beq __error_p           @ yes, error 'p'8 O! b& ^3 T; D/ Q# [
  10.      86     bl  __lookup_machine_type       @ r5=machinfo
    # J4 X1 B* D4 N  t# u4 b
  11.      87     movs    r8, r5              @ invalid machine (r5=0)?" W: u0 m: u; i
  12.      88     beq __error_a           @ yes, error 'a'
    5 b. s; w; ]7 C" [
  13.      89     bl  __vet_atags
    " U) E4 Q- Q# G. S2 X
  14.      90     bl  __create_page_tables
複製代碼
既然找到了檔案,我們又可以開始繼續。4 t6 M+ P7 r; `# U3 g. E
1 b' K! w+ Y( \9 l/ {5 b% o  t. B; ~
看了一下,程式碼多了很多bl的跳躍動作,看來會在各個function跳來跳去。
分享到:  QQ好友和群QQ好友和群 QQ空間QQ空間 騰訊微博騰訊微博 騰訊朋友騰訊朋友
收藏收藏 分享分享 頂 踩 分享分享
2#
 樓主| 發表於 2008-10-9 15:32:16 | 只看該作者
既然跳到真正的kernel開始跑,表示進入重頭戲,在進入kernel之前arm的平台有一些預設的狀況,也就是說arm kernel image會預設目前的cpu和系統的狀況是在某個狀態,這樣對一個剛要跑起來的OS比較決定目前要怎麼boot起來。7 ^# f" i7 \3 C6 D* V% {

' q- ?' l3 A0 A# M5 g  n8 _# c可以看一下comment,有清楚的描述。這也幫助我們了解為什麼decompresser的程式跑完之後,還要把cache關掉。
  1.      59 /*
    0 D& |; [( I7 y1 i; V7 U  z; G: a  e
  2.      60  * Kernel startup entry point.
    # |$ l& ?0 [1 N' h) }- C
  3.      61  * ---------------------------- `0 J5 H2 A' z/ ]+ E$ g) {
  4.      62  *
    6 e6 \- D8 Q7 X5 Y/ T/ G
  5.      63  * This is normally called from the decompressor code.  The requirements
    . Y) i" |3 J# J9 b
  6.      64  * are: MMU = off, D-cache = off, I-cache = dont care, r0 = 0,
    - c5 ~0 J1 a, G
  7.      65  * r1 = machine nr, r2 = atags pointer.
複製代碼
基於以上的假設,當program counter指到kernel的程式的時候,就會從line 80開始跑。% A+ Y1 i( {# }$ t5 v& Z5 v( Q, c
line 80, msr指令會把, operand的值搬到cpsr_c裡面。這是用來確保arm cpu目前是跑在svc mode, irq&fiq都disable。(設成0x1就會disable,definition在./include/asm-arm/ptrace.h)6 V- |7 [9 F  h! v; [! n
line 82, 讀取CPU ID到r9
5 `# @; b- ?  L- _; g  B% T1 Qline 83, 跳到 __lookup_processor_type 執行(bl會記住返回位址)。
  1.      77     .section ".text.head", "ax"  v1 h+ m( ^9 |. E2 }& x& H
  2.      78     .type   stext, %function
    - H" j, @3 ?2 s) S/ ]
  3.      79 ENTRY(stext)
    " D/ T9 Q: X. X8 @8 I" \+ C
  4.      80     msr cpsr_c, #PSR_F_BIT | PSR_I_BIT | SVC_MODE @ ensure svc mode
    * {/ K7 q7 L1 Z: L
  5.      81                         @ and irqs disabled- ~1 M5 A/ N3 q! ^* \
  6.      82     mrc p15, 0, r9, c0, c0      @ get processor id
    - U: y) a/ c6 y6 b$ s" w
  7.      83     bl  __lookup_processor_type     @ r5=procinfo r9=cpuid
複製代碼
接著會跳到head-common.S這個檔,
: l# ?9 S( A/ B2 M$ u- eline 158, (3f表示forware往前找叫做『3』label)將label 3的address放到r3。! q4 V5 ~0 }7 c! s. J# `
line 159, 將r3指到的位址的資料,依序放到r7, r6, r5.(ldmda的da是要每次都位址減一次)
, ?1 E. o  x) G  b4 Z# ~( C" D6 Wline l60, 161, 利用真實得到位址r3減去取得資料的位址得到一個offset值,這樣可計算出r5, r6真正應該要指到的地方。
  ?7 n& D3 }, W, q0 K) o) aline 163~169,這邊的程式應該不陌生,就是在各個CPU info裡面找尋對應的structure。找到的話就跳到line 171,返回head.S" b- ]. B( R- e% d
line 170, 找不到的話,r5的processor id就放0x0.表示unknown id。( |  n0 {5 ~$ q! U% M9 t
3 B+ u8 m% @' g) q2 x+ n$ \3 w
__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
    . {; @9 ^0 C. p7 \* }* z
  2.     157 __lookup_processor_type:! s5 C2 v; p4 Z; j
  3.     158     adr r3, 3f0 \* E! ?, l" P% _% X
  4.     159     ldmda   r3, {r5 - r7}
    # n- _+ F& w1 I  n( e
  5.     160     sub r3, r3, r7          @ get offset between virt&phys
    8 k' P; J- |" C4 f8 z, n
  6.     161     add r5, r5, r3          @ convert virt addresses to
    ' b0 Y; `- g- O2 h
  7.     162     add r6, r6, r3          @ physical address space. q: J! H% x) i, U, x2 N
  8.     163 1:  ldmia   r5, {r3, r4}            @ value, mask9 S9 |9 G- H; W0 [# X0 K8 B0 [
  9.     164     and r4, r4, r9          @ mask wanted bits
    8 E. x1 m) ~& [  K6 {5 d; d$ |
  10.     165     teq r3, r4
    ; d8 i5 L) X( H5 `% S& t* y
  11.     166     beq 2f3 I6 X! G/ E) }) h0 \! F
  12.     167     add r5, r5, #PROC_INFO_SZ       @ sizeof(proc_info_list)' g/ n- F* m. \2 l
  13.     168     cmp r5, r6
    4 r) u1 i6 k% J% a
  14.     169     blo 1b
    5 w$ p- A. w4 r! G3 o8 N6 M1 I
  15.     170     mov r5, #0              @ unknown processor8 c$ I! T( ^& Q1 [! M7 O& A2 f
  16.     171 2:  mov pc, lr- j  r  O2 S% S
  17. , ?7 X5 o4 s, C( {$ Z
  18.     187     .long   __proc_info_begin( n3 j/ N" f3 o1 G, ~( k
  19.     188     .long   __proc_info_end
    ! i: J8 Y! \: j* E9 R& x7 Z
  20.     189 3:  .long   .
    9 |: q$ {- S+ @/ ?
  21.     190     .long   __arch_info_begin4 _1 V4 i/ K9 A- f7 \
  22.     191     .long   __arch_info_end
複製代碼
跳了很多檔案,建議指令和object code如何link的概念要有,就會很清楚了。
3#
 樓主| 發表於 2008-10-13 17:20:35 | 只看該作者
我們從 head-common.S返回之後,接著繼續看。, W7 ^4 l8 ]4 x$ G6 W+ {

/ Q8 `' h2 J4 ~, X: t) m. Iline 84, movs的意思是說,做mov的動作,並且將指令的S bit設起來,最後的結果也會update CPSR。這個指令執行的過程當中,會根據你要搬動的值去把N and Z flag設好。這個是有助於下個指令做check的動作。% o+ Y4 z9 O7 \' ]0 P
line 85, 就是r5 = 0的話,就跳到__error_p去執行。* f. V; E7 N% \3 i' D
line 86, 跳到__lookup_machine_type。原理跟剛剛找proc的資料一樣。
  1.      83         bl      __lookup_processor_type         @ r5=procinfo r9=cpuid
    - F1 H) F6 d& {6 `+ a
  2.      84         movs    r10, r5                         @ invalid processor (r5=0)?1 p  l8 }4 t/ o; J+ G, m( q0 d% X. ]
  3.      85         beq     __error_p                       @ yes, error 'p'+ P; N$ \$ n2 u! \
  4.      86         bl      __lookup_machine_type           @ r5=machinfo
複製代碼
看得出來跟proc很像,有個兩個小地方是1 [) A+ Q! y8 S( E' l1 f
7 Z& |& m9 y8 K- d$ m
1. line 207,用ldmia不是用ldmda。原因就在於存放arch 和 proc info 的位址剛好相反。一個在lable3的上面。一個在的下方。設計上應該是可以做修改的。8 {& _7 a+ k  R: v( o
6 c3 r* h+ `: f' j* x
2. arch定義的方式是透過macro,可以先看 ./include/asm-arm/mach/arch.h。裡頭有個 MACHINE_START ,這邊會設好macro,到時候直接使用就可以把arch的info宣告好。 例如 ./arch/arm/mach-omap1/board-generic.c
  1. /* macro */9 c" U# l9 g/ L, W- K; R
  2.      50 #define MACHINE_START(_type,_name)                      \0 X& I) B8 h$ R0 _# O! e
  3.      51 static const struct machine_desc __mach_desc_##_type    \: `; f& D5 B3 G; w- a, k) q' `
  4.      52  __used                                                 \8 t8 p# i) A4 W8 l2 z
  5.      53  __attribute__((__section__(".arch.info.init"))) = {    \3 v: {, @* i! ^. \6 l
  6.      54         .nr             = MACH_TYPE_##_type,            \
    : D. v# `7 Z& Z8 o7 Y1 w; N: k! C
  7.      55         .name           = _name,( ?- F9 i7 b& A, @( I8 T6 M
  8.      56
    5 q. G; H! c9 A/ n
  9.      57 #define MACHINE_END                             \
    ' T! _  N+ t/ W; p
  10.      58 };
      U% E( M! {$ g
  11.      /* 用法 */
    # ~+ G# ~; l8 M& I- L/ C
  12.      93 MACHINE_START(OMAP_GENERIC, "Generic OMAP1510/1610/1710")$ O9 T5 z; U; v- X/ M  d* ~  F9 b
  13.      94         /* Maintainer: Tony Lindgren <tony@atomide.com> */
    8 Y' z- ~+ l! u  c; }
  14.      95         .phys_io        = 0xfff00000,
    * v3 J$ T' g2 @) }9 I- G( z
  15.      96         .io_pg_offst    = ((0xfef00000) >> 18) & 0xfffc,
    - r# m7 X: s# C2 K$ H
  16.      97         .boot_params    = 0x10000100,+ v& K0 q- p3 v! r: X
  17.      98         .map_io         = omap_generic_map_io,( K: U  W4 ^1 ]! Y" U% N: Q& s( w
  18.      99         .init_irq       = omap_generic_init_irq,+ Y1 Q- Z% z5 j+ B9 F+ [) B
  19.     100         .init_machine   = omap_generic_init,9 A" K; F6 o8 r  y: M$ ]4 P2 K; R
  20.     101         .timer          = &omap_timer,$ X8 |0 F. O& P- a9 V7 R0 y* a2 H
  21.     102 MACHINE_END: o7 i( o# @9 S& B& D5 T
  22. - }& L$ v/ r% ]  u1 U
  23.     /* func */
    3 ~& J% E, P" x
  24.     204         .type   __lookup_machine_type, %function
    ' s2 d; [* f! s/ s" F
  25.     205 __lookup_machine_type:, G* ~; z/ x- c! n
  26.     206         adr     r3, 3b& ^/ ~$ m* \) T
  27.     207         ldmia   r3, {r4, r5, r6}, c$ i. p9 H9 w2 i2 r
  28.     208         sub     r3, r3, r4                      @ get offset between virt&phys' n; u8 p* N% Y) i
  29.     209         add     r5, r5, r3                      @ convert virt addresses to
    & b7 F4 v- G3 U2 m
  30.     210         add     r6, r6, r3                      @ physical address space
    3 b$ w  u6 R+ A
  31.     211 1:      ldr     r3, [r5, #MACHINFO_TYPE]        @ get machine type
    $ u! p7 O5 o8 [) t4 f- c0 o5 F
  32.     212         teq     r3, r1                          @ matches loader number?% h% s# e+ ?9 e7 ~* A: h
  33.     213         beq     2f                              @ found
    1 F- K1 {4 P. F
  34.     214         add     r5, r5, #SIZEOF_MACHINE_DESC    @ next machine_desc! x3 k1 b' h. p4 W5 \; s. j+ X
  35.     215         cmp     r5, r6
    ( @! V  b8 X4 k5 t7 z  Q% ?; g
  36.     216         blo     1b9 I, W  @7 {* @
  37.     217         mov     r5, #0                          @ unknown machine% d' o" ]1 S. e  E# {; ~
  38.     218 2:      mov     pc, lr
複製代碼
4#
 樓主| 發表於 2008-10-13 17:56:46 | 只看該作者
接著我們又返回到head.S,
9 n. T: k, N, o5 z0 \line 87~88也是做check動作。
) @1 y! f8 }1 |* z$ o) H6 Y0 jline 89跳到vet_atags。在head-common.S
  1.      87         movs    r8, r5                          @ invalid machine (r5=0)?5 R" M2 K% X6 w: t
  2.      88         beq     __error_a                       @ yes, error 'a'* J6 c' ~/ u. e- _: D' a
  3.      89         bl      __vet_atags8 i- ]* n; t7 i+ l1 m0 n6 V+ u: {
  4.      90         bl      __create_page_tables
複製代碼
line 245, tst會去做and動作。並且update flags。這邊的用意是判斷位址是不是aligned。
  n& J5 I3 W. }$ e* s8 ]. b, uline 246, 沒有aligned跳到label 1,就返回了。! w* s3 o/ d% p, s' p; |
line 248~250, 讀取atags所在的address裡頭的值到r5,看看是否不等於ATAG_CORE_SIZE,不等於的話也是返回。
9 m0 I$ k) {5 Lline 251~254, 判斷一下第一個達到的atag pointer是不是等於ATAG_CORE。如果正確的話,等一下要讀取atag的資料,才會正確。
8 V9 ~8 m) Q2 F$ ?1 Q(atag是由bootloader帶給linux kernel的東西,用來告知booting所需要知道的參數。例如螢幕寬度,記憶體大小等等)
  1.      14 #define ATAG_CORE 0x54410001
    3 n" f5 M7 Z7 `' j
  2.      15 #define ATAG_CORE_SIZE ((2*4 + 3*4) >> 2)
    8 u/ g4 `4 U. \. V7 ^9 R1 l
  3. , f% \8 V+ T- R/ K# R2 O" d: P
  4.     243         .type   __vet_atags, %function5 l, Q& V7 n5 X! s1 n5 m- M
  5.     244 __vet_atags:7 O3 G" h% @' t8 G+ P
  6.     245         tst     r2, #0x3                        @ aligned?- M# C+ N7 N/ w* E
  7.     246         bne     1f
    ) E2 r& u; n! t" V# s
  8.     247& D$ c2 J6 N; ?8 ^& i' d) W7 O
  9.     248         ldr     r5, [r2, #0]                    @ is first tag ATAG_CORE?
    5 e3 O- R& P5 q# K; \# o
  10.     249         subs    r5, r5, #ATAG_CORE_SIZE
    6 A, h* y0 _" W! y  `) m
  11.     250         bne     1f, y% T1 \: ^0 o7 C' S
  12.     251         ldr     r5, [r2, #4]
    * P% U( z( [% l2 G
  13.     252         ldr     r6, =ATAG_CORE$ J" N7 ~9 b1 |- g( ]) I- c4 R+ s( o
  14.     253         cmp     r5, r6. ~, X7 k5 w6 K2 _. w
  15.     254         bne     1f
    % X5 H9 d3 G4 [& k* k
  16.     255
    ; H/ J4 |# G3 N! N( l1 N5 l2 ?
  17.     256         mov     pc, lr                          @ atag pointer is ok
    ! l. v9 ~. x' g/ r
  18.     257; z' Q9 `  a* a  `5 h
  19.     258 1:      mov     r2, #0, a1 u' }5 @' \+ m3 ^# k+ I  a
  20.     259         mov     pc, lr
複製代碼
接著我們又跳回去head.S。  ( E: C. d" h1 A; K
line 90,又跳到 __create_page_tables。   (很累人....應該會死不少腦細胞)
3 V1 u9 z8 `/ a7 Q8 Y* f) v$ A哇!page table?!!如雷貫耳的東西,不知道會怎麼做。剛剛偷看了一下,@@還蠻長了,先到這邊好了,下次在繼續寫。
5#
 樓主| 發表於 2008-10-14 12:13:51 | 只看該作者
由於code看起來似乎越來越難解釋,會需要在檔案之間跳來跳去。建議一些基礎的東西可以再多複習幾次(其實是在說我自己 ),閱讀source code上收穫會比較多。例如:
7 f& t# N$ v9 M. X2 _) @
. o( [5 i- D5 G, Q1. arm instruction set - 這個最好每個指令的意思都大略看過一次,行有餘力多看幾個版本,armv4, v5 or v6。! \4 c/ T2 A/ g8 b! _) }5 F. g
2. compiler & assembler & linker - toolchain工具做的事情和功能,大致的流程和功能要有概念。% z  _6 E; f& }. Z
3. Makefile & link script - 這兩個功能和撰寫的方式要有簡單的概念。
: X! o9 l2 E9 C  w5 T8 v0 ?+ V) l. x6 s9 V* t5 J
以上1是非常重要的重點,2&3只要有大致上的概念就可以,因為trace code的時候,有時需要跳到Makeflie&Link script去看最後object code編排的位址。: n% J$ O$ ?% d: H6 ^  M9 v

5 _5 ^, a. I( z6 t; t" M由於我們trace到了page table這個關鍵字,在開始之前,稍微簡短的解釋,為了幫助了解,儘量用易懂的概念講,有些用詞可能會和一些真實狀況不同,但是懂了之後,應該會有能力分辨,至於正式而學術上解說,很多書上應該都有,或是google一下就很多啦∼
6#
 樓主| 發表於 2008-10-14 12:14:47 | 只看該作者
page table本身是很抽象的東西,尤其是對所謂的 user-mode application programmer 來說,大部分的狀況它是不需要被考慮到的部份,但是他的產生卻對os和driver帶來了很多影響。我們從一個小小的疑問出發。0 E& X4 O; k7 q* T; [

* y$ {- X( X3 g; \) }9 F) m『產生page table到底是要給誰用的?』2 T" l# Z: @- o) k+ P
. m5 C- S+ [7 A  N0 n
其實真正作用在它上面的H/W是MMU,一旦CPU啟用了MMU,當cpu嘗試去記憶體讀取一個operand的時候,cpu內部打出去的位址訊號都會先送到mmu,mmu會拿著這個位址去對照page table,看看這個位址是不是被轉換到另外一個位置(還會確認讀寫權力),最後才會到真正的位址去讀寫。. Q) e9 ^$ |3 m0 j1 u+ o. _
: g1 q& O0 y) f& y1 x$ d
這樣來看CPU其實一開始打出去的位址訊號其實不是最後可以拿到資料的位址,我們稱為virtual address(va),mmu打出來的位址才能真正拿到資料,所以我們把mmu打出去的位址稱為physical address(pa)。那用來查詢這個位址對照關係的表格,就是page table。1 y$ k6 Z: k9 k- N
7 Y# j1 p+ u  G. {! @
到這邊我們有一個簡單的概念。來想像一個小問題,一個普通的周邊(例如你的顯示卡),因為他並不具有MMU功能,hw只看得懂pa,但是CPU卻是作用在va,假如我想去對hw的控制暫存器做讀寫,到底要用va還是pa?
7#
 樓主| 發表於 2008-10-14 12:15:51 | 只看該作者
這時,寫driver的人就必須要小心,設定給硬體看的位址必須要先轉成pa(像是設定DMA),給CPU執行的程式碼要使用va,這對一開始嘗試寫driver但是又不瞭解va pa之間的差別的人,常常產生很多疑問。; w% C0 O& x2 C1 P6 ?

6 f" n3 c' [1 b& k) k1 W' ?+ N, |# l9 Z現在我們回頭想想OS,既然我們跑到create page table,可以預期的是他想要將MMU打開,因此希望預先建立起一個page table,讓MMU知道目前os想規劃的位址對應和讀取權力是如何被安排的。所以os必須考慮到現在的系統究竟是長怎樣?應該要如何被安排?是不是有那些要被保護?好吧∼因為我們完全對os不了解,也不知道該安排什麼,只能祈禱在trace code完後,可以找到這些問題的答案,或者發現一些沒想到的問題。4 Z' w4 F) ~0 B+ l7 z

5 A2 F+ m/ x( \知道了page table的大致上的功能,下篇就可以專心的研究這個table的長相,和它想規劃出的系統模樣。0 `# ]/ o8 _2 W& }

& A3 e8 @0 C! z3 F1 A, tp.s. 字數限制好像變短了。   (看來很難寫)
8#
 樓主| 發表於 2008-10-14 13:56:17 | 只看該作者
由於字數限制挑整,用詞儘量精簡,以便可以貼必要source。另外,以後寫到一段落,考慮收集成一篇完整的文章,這樣應就不會因為用回覆的方式,造成閱讀斷斷續續的問題。希望有人對文章呈現方式有想法的話,可以跟我講。
9#
 樓主| 發表於 2008-10-14 13:57:39 | 只看該作者
現在,讓我們跳入create_page_tables吧∼
1 W+ W* w3 t- e! |: u4 M: X2 Dline 216,會跳到pgtbl的macro去執行,其實只是載入一個位址。: a  n5 k' z( \  A; \
  l8 e! @0 C/ M7 `% B1 ?
只是這個位址因為你硬體規劃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 */
    0 s9 E6 d+ ~' F' Q
  2.      95 textofs-y       := 0x00008000
    6 e/ G" p, }1 `, d: N9 n7 J8 _
  3.     152 TEXT_OFFSET := $(textofs-y)
複製代碼
  1. /* include/asm-arm/arch-omap/memory.h */
    $ S% J; h% L( S! I/ {+ J2 b
  2.      40 #define PHYS_OFFSET             UL(0x10000000)
    ( i+ P' {% [: M& {4 p

  3. 2 k$ I! y, A% }
  4.      /* arch/arm/kernel/head.S */- u- ~- j! E+ n  p8 x3 l
  5.      29 #define KERNEL_RAM_VADDR        (PAGE_OFFSET + TEXT_OFFSET)
    7 c9 ~6 }/ t' L) s% M
  6.      30 #define KERNEL_RAM_PADDR        (PHYS_OFFSET + TEXT_OFFSET)6 A# k- P  I1 i: C1 ?  ~
  7. & B7 n7 E) s) |. Y
  8.      47         .macro  pgtbl, rd" t5 W7 f. L( z
  9.      48         ldr     \rd, =(KERNEL_RAM_PADDR - 0x4000)- x( Z' F- K1 D
  10.      49         .endm  N' q: I% Y( b3 B; U6 f

  11. 9 Z) W9 O: w2 o/ Y# }
  12.     216         pgtbl   r4                              @ page table address
複製代碼
10#
 樓主| 發表於 2008-10-14 14:16:53 | 只看該作者
得到pg table的開始位置之後,當然就是初始化囉。+ D& |" F. V7 M) k$ h/ o" y
line 221, 將pg table的base addr放到r0.
. a8 c& r( F6 D2 aline 223, 將pg table的end addr放到r6.+ J/ Y" C" W- ^7 _# J* |
line 224~228, 反覆地將0x0寫到pg table的區段裡頭,每個loop寫16bytes. (4x4),直到碰到end,結果就是把它全部clear成0x0.
  1.     221         mov     r0, r4
    * \, Y# D8 e7 s* }: L0 G
  2.     222         mov     r3, #0
    8 R3 v' c$ r* B, X* }) M& S0 r
  3.     223         add     r6, r0, #0x4000
    & D' g2 }' `2 M' ^/ L
  4.     224 1:      str     r3, [r0], #48 V# ?7 v" j. l' T& R: V
  5.     225         str     r3, [r0], #4
    ( O( Y# S% m4 h4 U
  6.     226         str     r3, [r0], #4
    0 X. C! j+ f2 D& ?1 A* n9 c
  7.     227         str     r3, [r0], #4( g7 Z6 U- `' ^/ T+ O
  8.     228         teq     r0, r6
    / Z4 W- C- N, W. o6 v
  9.     229         bne     1b
複製代碼
line 231, 將位址等於 r10+PROCINFO_MM_MMUFLAGS 裡頭的值放到r7。r10是proc_info的位址。proc的info data structure被定義在『./include/asm-arm/procinfo.h』,offset取得的方式用compiler的功能,以便以後新增structure的欄位的時候不需要更動程式碼。這邊的動作合起來就是讀預設要設給mmu flags的值。
  1.    231         ldr     r7, [r10, #PROCINFO_MM_MMUFLAGS] @ mm_mmuflags
複製代碼
11#
 樓主| 發表於 2008-10-14 15:11:48 | 只看該作者
問題怎麼填值??$ g& x5 F" Q. `( |% J! G8 o# [3 v
拿出ARM的手冊,翻到MMU章節。一看發現page有很多種,還得分first level和second level。 0 M3 b2 U+ B2 m- b1 r* q2 V

8 P% ^* f" {0 T7 |念書時的印象,是從1st level在去查2nd level,先看怎麼設定1st level (不知以前老師有沒有亂教), p9 Y1 G( V6 o  R7 b
1. [31:20]存著section base addr
+ u* v9 g( ~$ f/ Z4 A2. [19:2]存著mmu flags% ?1 O- x! a+ \* x8 d
3. [1:0]用來辨別這是存放哪種page, 有四種:
2 ^' r; v3 h  x- Q) \' Q   a. fault (00) b. coarse page (01) c. section (1st level) (10) d. fine page (11)
; |* |4 r3 I0 g" D  H1 N  V0 U4. pg tabel資料要存放到 [31:14] = translation base, [13:2] = table index , [1:0] = 0b00 的位址
: c' U6 ]7 g6 l4 i
# q3 r4 c  r& R* \) C& m% V! F, r5 b/ g, G來看code是怎麼設定。; P2 {( r6 G0 C; m, B3 g! j
) T. f  F! q! X, @0 W; l
line 239, 將pc的值往右shift 20次放到r6.這是取得前20高位元的資料,有點像是 1.。1 b7 B, b3 ?( s
line 240, 將r6往左shift 20次之後,和r7做or的動作,剛好也是 2.提到的mmu flags和1.處理好的資料做整理。
+ s' y, e6 q7 V所以前面兩個做完,就完成了bit[31:2]。- N  Q2 l5 y2 ?+ |( w. x- r
line 241, 將r3的資料寫到r4+(r6<<0x2)的地方去,剛好是4.提到的位址。(lucky)
  1.     239         mov     r6, pc, lsr #20
    ! b8 K: d+ |  f. B& {* p% Y
  2.     240         orr     r3, r7, r6, lsl #20& i% n  |; Q8 d) c7 ]: N4 X& @4 f" D
  3.     241         str     r3, [r4, r6, lsl #2]
複製代碼
p.s. 用pc值剛好可以算出當前的page。
12#
 樓主| 發表於 2008-10-22 19:47:03 | 只看該作者
最近又被釘上了,開始忙碌,大概沒太多時間貼文∼
9 R% p$ V2 W* c0 v, z7 n, o
& V& `% q; J1 m5 _4 {0 u" F上篇已經將pc所屬的page table entry(pte)設定好,接著繼續看+ R2 X; Z  {) j% `3 O
line 247, 248, 將KERNEL_START的位址往右shift 18算出pte的offset,(等於line239&241,239&241是先shift right 20然後shift left 2),並將剛剛r3的值設給pte。『!』的意思是會把address存到r0。
5 w1 V  {6 W1 e9 u
! X2 y; ]9 Y/ [8 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) >> 18; l$ P) p7 O, i, b' h! A
  2.     248         str     r3, [r0, #(KERNEL_START & 0x00f00000) >> 18]! % I; O$ N4 {5 M" A% s/ ^
  3.     249         ldr     r6, =(KERNEL_END - 1)
    - ~$ y2 u' i* {& U' v* ^3 P
  4.     250         add     r0, r0, #4
    ) L- T; x' F) q5 E9 o; }
  5.     251         add     r6, r4, r6, lsr #182 `. j% E% B5 X
  6.     252 1:      cmp     r0, r6' u) X7 B# F6 x- N9 `1 C
  7.     253         add     r3, r3, #1 << 201 I4 F7 s9 }, k
  8.     254         strls   r3, [r0], #4
    ' ?4 o8 F% H  n+ r8 n( `
  9.     255         bls     1b
複製代碼
13#
 樓主| 發表於 2008-10-22 20:24:58 | 只看該作者
line279,PAGE_OFFSET是規範RAM mapping完後的virtual address,我們將這個位址所屬的pte算出來放到r0。1 C; Y* q. z4 @4 o7 y
line 280~283,將要 map 的physical address的方式算出來放到r6。# D9 P/ w; t0 t
line 284,最後將結果存到r0所指到的pte。& Z6 E3 N7 J0 e+ @: Q' z( C

, _7 h. Q- L/ ]( b以上三個動作,就是做好 ram 起頭的位址一開始map,還沒做完整塊map。由於我們目前的設定方式每塊是1MB,所以這邊是將RAM開始的1MB做map。) ^- J: r1 g3 a* I* k

/ Y5 T- i2 `! ]) L  J4 yline 327,返回,結束一開始的create page table的動作,我們可以看出其實並沒有做完整的初始化page table,所以之後應該會有其他page table的細節。
  1.     279         add     r0, r4, #PAGE_OFFSET >> 184 G) {% T% W; b
  2.     280         orr     r6, r7, #(PHYS_OFFSET & 0xff000000)) ^( [7 t# \) D5 R2 I/ V1 ?. E
  3.     281         .if     (PHYS_OFFSET & 0x00f00000)6 b( c# V+ n" g1 N- H
  4.     282         orr     r6, r6, #(PHYS_OFFSET & 0x00f00000)0 V* y9 M5 {( L
  5.     283         .endif) k; A# v8 h3 B; M
  6.     284         str     r6, [r0]) U9 f( v0 I& K8 a$ G% p
  7.     327         mov     pc, lr
複製代碼
附帶一提,我們這邊省略了一些用ifdef包起來的程式碼,像是一開始會印一些output message的程式碼(line286~326)。
14#
 樓主| 發表於 2008-10-22 20:37:08 | 只看該作者
自create page table返回後,我們偷偷看一下接下來的程式碼,  ?) m: K2 h3 n. k/ F; S
line 99, 將switch_data擺到r134 B0 H8 y; ]6 y0 q# n" `
line 101, 將enable_mmu擺到lr
/ M1 z2 _+ g: W) M* Pline 102, 將pc改跳到r10+PROCINFO_INITFUNC的地方去
. O4 @  l5 u* \6 L7 W0 P* f+ K' M$ C
其實這邊有點玄機,switch_data和enable_mmu都是function,結果位址都只是被存起來,並沒有直接跳過去執行。其實,雖然程式碼是順序switch_data->enable_mmu->proc init function,但其實執行的順序會是 procinfo 的init function-> enable_mmu -> switch_data 。至於,為什麼要這樣寫的原因?就不清楚了,也還沒仔細推敲過。
5 `0 |& T2 \. A: a% z" Z0 t  u1 T& q& K3 q
switch_data最後就會跳到大家都很熟悉的start_kernel(). 詳細要賣個關子,最近會忙一下,得要過一陣子才能貼文∼  
  1.      99         ldr     r13, __switch_data              @ address to jump to after; V% X1 S3 k; d9 r
  2.     100                                                 @ mmu has been enabled' n  J% F6 P7 l5 @
  3.     101         adr     lr, __enable_mmu                @ return (PIC) address
    4 L7 G9 r0 [1 r% O
  4.     102         add     pc, r10, #PROCINFO_INITFUNC
複製代碼
15#
 樓主| 發表於 2009-7-4 01:09:36 | 只看該作者
老店重新開張~
8 j: P$ ^' U" f. I& d4 s# F2 ^6 N8 T9 j
花了一些時間把舊的貼文整理到一個blog
- U3 i7 a) W( ~$ l, \: E有把一些敘述修改過6 {7 L  Z* r! Z$ `( t- i. G& C* R
希望會比較容易集中閱讀
" [8 ^: y2 T; ?) `7 H目前因為某些敘述不容易+ t" _) \4 u+ h+ Q: P/ R7 K* l
還是比較偏向筆記式而且用字不夠精確
9 D: o/ W' W: E8 e6 g1 {0 J希望之後能夠慢慢有系統地整理) |7 A2 c7 V7 J( N
大家有興趣的話
1 [) _* p1 v8 N' {可以來看看和討論
2 Q9 U5 b7 p- l" Xhttp://gogojesseco.blogspot.com/
* I! Z! h- U/ T5 ^0 y& f$ e  O+ @" q6 N, @
以後可能會採取  先在chip123貼新文章
" h6 G- d6 }  ^$ N) \5 A慢慢整理到blog上的方式
8 i0 U+ J$ E7 e% v  g3 }因為chip123比較方便討論 =)
" W" F$ i& q. z1 Yblog編輯修改起來比較方便
/ z8 O% g# l% X3 m閱讀也比較集中   大家可以在這邊看到討論
+ B( A8 l2 f% x4 A" R/ {' d, N然後在blog看到完整的文章 (類似BBS精華區的感覺)

評分

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

查看全部評分

16#
 樓主| 發表於 2009-7-15 17:07:03 | 只看該作者
隔了很長一段時間沒update
2 j- r, `9 B% r( h$ D7 J之前程式碼走到 ./arch/arm/kernel/head.S 的 line 99 附近
  1.      99         ldr     r13, __switch_data              @ address to jump to after
      Y2 {& d  j3 K% C* K- o( f0 z
  2.     100                                                 @ mmu has been enabled) n: n$ c2 h* q3 Z5 G! C& ^" d5 l
  3.     101         adr     lr, __enable_mmu                @ return (PIC) address
    3 q1 B& X. _6 B% q# D2 C% L* V
  4.     102         add     pc, r10, #PROCINFO_INITFUNC
複製代碼
line 99, 將__switch_data放到r13。(留作之後用)
8 L+ i6 u$ U) Oline 101, 將__enable_mmu的addr放到lr。(留作之後用)* G7 F9 _1 ?1 `
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
    7 S4 }% t( {8 E1 `/ n, f+ q) K
  2. 374 __arm926_setup:
    1 ?8 A1 A- ?/ ]- |* `* L
  3. 375         mov     r0, #0
    0 U. G) p+ N! A4 I8 M& ?
  4. 376         mcr     p15, 0, r0, c7, c7              @ invalidate I,D caches on v4  z9 I4 e9 i  ?3 n5 L2 `5 a) h4 H
  5. 377         mcr     p15, 0, r0, c7, c10, 4          @ drain write buffer on v4
    # E7 H2 @1 ~' r5 l/ h
  6. 378 #ifdef CONFIG_MMU6 m4 A9 J4 I+ N
  7. 379         mcr     p15, 0, r0, c8, c7              @ invalidate I,D TLBs on v4) }: f. M4 M6 I, y6 r
  8. 380 #endif
    ( P" Z7 i( }" X* e
  9. : [( B# E2 J! A( c0 `0 ~% O9 N
  10. 388         adr     r5, arm926_crval
    * X" \$ p( a9 A! ~. `( ]- X
  11. 389         ldmia   r5, {r5, r6}
    2 G% K, n/ p4 R1 i
  12. 390         mrc     p15, 0, r0, c1, c0              @ get control register v4' B: y+ P! S/ f! R% k
  13. 391         bic     r0, r0, r50 y0 l0 c6 U1 B2 b1 @! i
  14. 392         orr     r0, r0, r6
    + `! B8 \% y) c
  15. ) E2 }4 r) x- {4 [) h6 n
  16. 396         mov     pc, lr
    5 b! i% A' v) F0 c6 w& K8 B3 c
  17. 397         .size   __arm926_setup, . - __arm926_setup
複製代碼
這邊的程式碼就跟CPU有很大的相依性,2 x2 L! F- W, z1 \( k4 U7 L( R6 _
line 375~380, 主要就是invalidate CPU的I&D cache和清空write buffer。
! o) U0 e  H9 i9 Tline 388~392, 把cp15的設定讀出來,並且將一些預設狀態做好運算放到r0。(預設值從arm926_crval這邊可以取得。)" a. b" _5 c3 f& `: g4 n  o1 \
line 396, 直接把pc跳到lr,因為我們之前已經將lr = enable_mmu,所以直接跳過去。
17#
 樓主| 發表於 2009-7-15 17:29:45 | 只看該作者
  1. 155 __enable_mmu:3 O7 N/ V2 S; k( @" k
  2. 170         mov     r5, #(domain_val(DOMAIN_USER, DOMAIN_MANAGER) | \
    8 S" x( ]9 V9 Z% w
  3. 171                       domain_val(DOMAIN_KERNEL, DOMAIN_MANAGER) | \3 ^. N* F1 }3 T' a3 `% o
  4. 172                       domain_val(DOMAIN_TABLE, DOMAIN_MANAGER) | \8 i2 |% r. p9 W/ _2 w8 f
  5. 173                       domain_val(DOMAIN_IO, DOMAIN_CLIENT))
    * k/ P7 B5 R3 Z; E$ W2 ^0 U1 v8 G
  6. 174         mcr     p15, 0, r5, c3, c0, 0           @ load domain access register
    . h  `* N) N" m+ f" \
  7. 175         mcr     p15, 0, r4, c2, c0, 0           @ load page table pointer# F) i# Y! U- h% S, m$ z! p
  8. 176         b       __turn_mmu_on* X6 J7 x4 P' R4 V1 ]  P+ B
  9. 177 ENDPROC(__enable_mmu)
複製代碼
line 170~174,設置好domain access。(domain access可以先當成設置access的權限,或許以後可以寫詳細的文章說明)
! x& b: G0 I! C4 ^8 n3 Uline 175~176,將create好的page table開頭丟給mmu。跳到turn_mmu_on
  1. 191 __turn_mmu_on:
    * a, g+ B: C+ G3 E' z  y
  2. 192         mov     r0, r0
    " W+ H+ k. h) y! ]
  3. 193         mcr     p15, 0, r0, c1, c0, 0           @ write control reg
    " O+ N- Y1 U. h8 S5 [4 l
  4. 194         mrc     p15, 0, r3, c0, c0, 0           @ read id reg) J# R4 n2 ^0 @* T( f1 ^
  5. 195         mov     r3, r3
    8 z1 h) q6 t% o7 s. e9 s
  6. 196         mov     r3, r3
    + R' Z' q$ W* E3 W; H
  7. 197         mov     pc, r137 L7 T. G/ a  X& P+ U& o
  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:* E; O4 l1 Y9 q. Q# j: d
  2. 19         .long   __mmap_switched
    : P4 I! H1 |# V$ [
  3. 20         .long   __data_loc                      @ r4
    " q+ H  c3 r6 J( t. f
  4. 21         .long   _data                           @ r53 h& ^) c* a" }
  5. 22         .long   __bss_start                     @ r6
    ) q$ x! P4 H+ q% m2 S! N- R* R
  6. 23         .long   _end                            @ r7) U' K0 r7 x* L& n' R2 P% X+ v, e5 G# _
  7. 24         .long   processor_id                    @ r4
    * W, O: Z% a6 r2 B: p+ N- u
  8. 25         .long   __machine_arch_type             @ r5
    ; @+ O8 ~% _% z; X
  9. 26         .long   __atags_pointer                 @ r6( _/ Z( w) [" `  s' b
  10. 27         .long   cr_alignment                    @ r7
    ; [' @+ d, T  _9 Y/ B& h- c/ |
  11. 28         .long   init_thread_union + THREAD_START_SP @ sp
    3 ]$ n8 I1 m5 r- W+ p
  12. 29
    # L9 S. d( [; }' A4 F
複製代碼
switch_data的第一行就是 __mmap_switched,所以我們直接看line 39。
18#
 樓主| 發表於 2009-7-15 17:30:00 | 只看該作者
  1. 39 __mmap_switched:
    % |; e4 l- v! K  M7 ^5 |
  2. 40         adr     r3, __switch_data + 47 p2 n& ~# U8 i  h# y
  3. 41- @+ s9 i# g1 Z5 I4 S9 F5 Z- t
  4. 42         ldmia   r3!, {r4, r5, r6, r7}
    & @4 A: k8 o- [& r, x/ S# J6 w
  5. 43         cmp     r4, r5                          @ Copy data segment if needed
    ' b$ t2 ~- x) y' b
  6. 44 1:      cmpne   r5, r65 r$ {7 J' K% k
  7. 45         ldrne   fp, [r4], #4) @3 B5 D7 m* I
  8. 46         strne   fp, [r5], #4, |, G& ^3 i, p# b) Z" k
  9. 47         bne     1b: w! r: U8 F% _3 f
  10. 48
    0 x7 B: f: F; q) d
  11. 49         mov     fp, #0                          @ Clear BSS (and zero fp)1 E2 O/ M4 S/ C" j0 U, j
  12. 50 1:      cmp     r6, r7
    & P) H: y; s5 k
  13. 51         strcc   fp, [r6],#4
    6 f6 C+ u! T3 l7 x9 b
  14. 52         bcc     1b' l1 g% b2 T! j
  15. 53+ l) x; P- }  t) y, L9 x: @
  16. 54         ldmia   r3, {r4, r5, r6, r7, sp}
    3 S& X$ O; i% U( S# d8 r& [
  17. 55         str     r9, [r4]                        @ Save processor ID' o6 e% i" \' \3 Z8 I, h
  18. 56         str     r1, [r5]                        @ Save machine type
    * ~' u1 @- O2 e2 P- z
  19. 57         str     r2, [r6]                        @ Save atags pointer' l) U* _  B1 _$ k; {3 A% z
  20. 58         bic     r4, r0, #CR_A                   @ Clear 'A' bit
    , V9 i% Y. n% B. r( z+ [
  21. 59         stmia   r7, {r0, r4}                    @ Save control register values/ N6 t5 f- i2 S6 J6 N
  22. 60         b       start_kernel
    7 e# v: X) X7 x' k# C" P
  23. 61 ENDPROC(__mmap_switched)
複製代碼
switch_data的第一行就是 __mmap_switched,所以我們直接看line 39。; Q/ R% F0 B; f; z
line 39,將__data_loc的addr放到r3
, s8 G, o9 @6 V, J. v3 b* Eline 42,從r3的位址,連續讀取四筆資料到r4, r5, r6, r73 s8 U. F- R: m+ Q0 B
line 43~47,看看data segment是不是需要搬動。
* p" w1 {2 O, }line 49~52, clear BSS。+ {( O2 k' P  ?  x* N/ _$ A
' Z% I4 {% }0 ]
由於linux kernel在進入start_kernel前有一些前提必須要滿足:
9 g" P7 Y' B# |  \( Gr0  = cp#15 control register% p% v! j. Y$ X+ c2 N
r1  = machine ID" T9 x5 U# S/ @6 t* }( `) x9 |5 N
r2  = atags pointer8 n9 h$ d, S8 n6 }6 ]0 @
r9  = processor ID9 X* A! N. N% |/ w
5 o2 g+ C& p' o. J# @3 c
所以line 54~59就是在做這些準備。# @7 l/ \5 i& f) O. S
最後呢? 我們就跳到start_kernel了。(而且還是用b start_kernel,表示我們不會再回來了)
* q  f& D; a' H3 L6 L- [1 d! `  J9 R, g7 k- n7 N' u9 ]
看一下start_kernel()在./init/main.c,終於跳出architecture specific的目錄,表示- s$ U' p: o1 }  e. E% Y4 J& g4 u3 t
我們真正的開始linux kernel的初始化。
7 K3 _; F( L: o像是 shedule init, console init, memory init, irq init等等都在start_kernel裡頭。
* L- N, t) k7 @: f! j6 V) P. K. _到這邊之後,應該就可以深入linux kernel中,各項比較跟hardware不那麼相關的軟體部分。

評分

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

查看全部評分

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

本版積分規則

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

GMT+8, 2024-9-27 09:09 PM , Processed in 0.226013 second(s), 23 queries .

Powered by Discuz! X3.2

© 2001-2013 Comsenz Inc.

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