Chip123 科技應用創新平台

 找回密碼
 申請會員

QQ登錄

只需一步,快速開始

Login

用FB帳號登入

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

trace linux kernel source - ARM - 03

[複製鏈接]
跳轉到指定樓層
1#
發表於 2008-10-7 14:00:18 | 只看該作者 回帖獎勵 |倒序瀏覽 |閱讀模式
到目前為止,我們已經進展到kernel幫自己relocate完,並解將自己解壓縮到一個地方要準備開始執行,那疑問來了?到底是跳到哪裡去了,因為./compressed/head.S最後一行居然是
3 R% |3 n# R! H4 w$ l『mov pc, r4』0 u- F) p+ a' ]$ h, O$ m6 s' x) Z
r4只代表了解壓縮完後kernel的位址,那究竟整包kernel編譯的時候,哪個function哪個東西被放在最前面咧?!
1 O$ I$ R  N: _) Z* z
, R, G2 e" e, Y: ?所以我們又必須開始找於kernel link的時候是怎麼被安排的,有了前面的基礎,我們可以從Makefile知道程式碼如何被編譯。至於link上的細節,例如有那些section和section先後順序等等,可以從 lds 檔來規範。
& j- [$ k; Q! x) S. s
; [7 I3 Q$ {9 F, c% Y. U; 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)5 X: X3 B0 H& a2 ]
我們可以發現第一個section是『.text.head』,裡頭的_stext從目前的位置開始放。  W+ w! ^/ j6 n* t
於是我們曉得只要找到屬於.text.head這個section,並且是_stext這個symbol的程式碼,就是解壓縮完後的第一行程式碼。
  1.      26     .text.head : {
    % Q& i2 u& O. Z% {
  2.      27         _stext = .;- M- v' l1 f" M2 |' Z6 H. @0 b
  3.      28         _sinittext = .;
    2 n& Y- ?* T  l3 u
  4.      29         *(.text.head)+ k7 h6 U( M) V: x5 d/ n1 k, G- |/ d- c
  5.      30     }
複製代碼
用指令搜尋一下,發現 ./arch/arm/kernel/head.S 裡頭有關鍵字(如下),結果我們從./arch/arm/boot/compressed/head.S跳到了./arch/arm/kernel/head.S
  1.      77     .section ".text.head", "ax"
    & t* p8 X* t3 C/ \9 o/ w3 L6 i- C
  2.      78     .type   stext, %function
    ; e( J& r- d" ?2 ^+ M1 P
  3.      79 ENTRY(stext)
    ! G) T+ q2 ~5 `9 A' ~  z2 ]
  4.      80     msr cpsr_c, #PSR_F_BIT | PSR_I_BIT | SVC_MODE @ ensure svc mode
    " c  A! L$ F" o6 S* L
  5.      81                         @ and irqs disabled; k, j$ ~5 Q( h: I+ E
  6.      82     mrc p15, 0, r9, c0, c0      @ get processor id( A- K4 X& y( ?
  7.      83     bl  __lookup_processor_type     @ r5=procinfo r9=cpuid
    5 E  ^$ A. h9 R0 B6 ?
  8.      84     movs    r10, r5             @ invalid processor (r5=0)?8 t* _5 i4 {2 @! Y* Q
  9.      85     beq __error_p           @ yes, error 'p'
    , q3 d% n" c1 A
  10.      86     bl  __lookup_machine_type       @ r5=machinfo
    ' c! `! q. q6 W! H# @( t
  11.      87     movs    r8, r5              @ invalid machine (r5=0)?
    1 O1 ^) w) b* ^" E
  12.      88     beq __error_a           @ yes, error 'a'
    . H' c# {0 a; l) @& \
  13.      89     bl  __vet_atags
    5 l+ f& u$ J, e$ e: Y
  14.      90     bl  __create_page_tables
複製代碼
既然找到了檔案,我們又可以開始繼續。1 H9 a* b* Y" t* p# r
& n2 O) ~7 P* {1 Z7 H+ M! l5 p
看了一下,程式碼多了很多bl的跳躍動作,看來會在各個function跳來跳去。
分享到:  QQ好友和群QQ好友和群 QQ空間QQ空間 騰訊微博騰訊微博 騰訊朋友騰訊朋友
收藏收藏 分享分享 頂 踩 分享分享
2#
 樓主| 發表於 2008-10-9 15:32:16 | 只看該作者
既然跳到真正的kernel開始跑,表示進入重頭戲,在進入kernel之前arm的平台有一些預設的狀況,也就是說arm kernel image會預設目前的cpu和系統的狀況是在某個狀態,這樣對一個剛要跑起來的OS比較決定目前要怎麼boot起來。
8 g) n5 U7 ~: `# w) A: f$ ]% X+ z: s. V) n' @  R7 L9 R
可以看一下comment,有清楚的描述。這也幫助我們了解為什麼decompresser的程式跑完之後,還要把cache關掉。
  1.      59 /*
    / J9 J/ v4 p& p6 p
  2.      60  * Kernel startup entry point.
    8 i6 ]+ o4 g8 V8 C6 G
  3.      61  * ---------------------------! K! u/ J8 t* @4 ?: e6 C+ R2 `  l
  4.      62  *8 W* c2 O; M( c" n; @
  5.      63  * This is normally called from the decompressor code.  The requirements2 H( G$ p$ y. w* f& o. x" p0 {( F
  6.      64  * are: MMU = off, D-cache = off, I-cache = dont care, r0 = 0,0 n# x* d6 V( L* f9 l
  7.      65  * r1 = machine nr, r2 = atags pointer.
複製代碼
基於以上的假設,當program counter指到kernel的程式的時候,就會從line 80開始跑。
. v1 x  D, H; G! \8 c! P5 Y/ Wline 80, msr指令會把, operand的值搬到cpsr_c裡面。這是用來確保arm cpu目前是跑在svc mode, irq&fiq都disable。(設成0x1就會disable,definition在./include/asm-arm/ptrace.h)% h8 o# X- v  X( G9 ^& S* V) {9 ^
line 82, 讀取CPU ID到r9
4 b$ k; R5 M- P, w4 _, m. [7 [/ ~line 83, 跳到 __lookup_processor_type 執行(bl會記住返回位址)。
  1.      77     .section ".text.head", "ax"
    9 \( X4 W: Q$ S1 [0 f
  2.      78     .type   stext, %function* T/ b' a" k* R, H/ {. a1 e
  3.      79 ENTRY(stext)0 G% A9 `5 S( c5 l
  4.      80     msr cpsr_c, #PSR_F_BIT | PSR_I_BIT | SVC_MODE @ ensure svc mode
    2 b& e3 K$ L- d8 Q  X7 t
  5.      81                         @ and irqs disabled
    1 N5 a( V! U' l9 B
  6.      82     mrc p15, 0, r9, c0, c0      @ get processor id
    ; G* X2 l+ K& {4 c/ s
  7.      83     bl  __lookup_processor_type     @ r5=procinfo r9=cpuid
複製代碼
接著會跳到head-common.S這個檔,, J' j3 t5 j( Z& _) H
line 158, (3f表示forware往前找叫做『3』label)將label 3的address放到r3。. e9 _8 ?9 Z1 J9 s! }! e# N  f: V& T% A
line 159, 將r3指到的位址的資料,依序放到r7, r6, r5.(ldmda的da是要每次都位址減一次)
4 g+ z; n4 ?: V. rline l60, 161, 利用真實得到位址r3減去取得資料的位址得到一個offset值,這樣可計算出r5, r6真正應該要指到的地方。
  i# r8 [2 y' F; ~/ lline 163~169,這邊的程式應該不陌生,就是在各個CPU info裡面找尋對應的structure。找到的話就跳到line 171,返回head.S
2 d: m4 ~0 _! z( B% Tline 170, 找不到的話,r5的processor id就放0x0.表示unknown id。. k4 _9 \  Z8 s7 M+ W: b' ~
: H, d+ p$ \* d, {
__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& S1 X+ w# S3 ?. ?2 h" d
  2.     157 __lookup_processor_type:
    9 {; j0 k. z! m* R* K
  3.     158     adr r3, 3f
    / u7 p, N( ?! e& K5 U/ {
  4.     159     ldmda   r3, {r5 - r7}+ P/ H$ ]8 ~" e; f! D
  5.     160     sub r3, r3, r7          @ get offset between virt&phys, ^5 L. c$ H# ]5 q7 a5 u
  6.     161     add r5, r5, r3          @ convert virt addresses to
    ( O% b$ s5 x9 u0 C, x9 M; I
  7.     162     add r6, r6, r3          @ physical address space( u1 k3 {. T. N% z- I& g' l
  8.     163 1:  ldmia   r5, {r3, r4}            @ value, mask- H& g" l" ]- _( }
  9.     164     and r4, r4, r9          @ mask wanted bits  I* ~& p' t4 [; e" T5 J( p- I+ ^9 B
  10.     165     teq r3, r4
      u& u3 e+ g3 Q  {- e8 s
  11.     166     beq 2f
    ) S+ x+ {: `" |  R3 \0 v8 S
  12.     167     add r5, r5, #PROC_INFO_SZ       @ sizeof(proc_info_list)
    . g1 M  U0 [; g4 @
  13.     168     cmp r5, r68 w) H2 z% l3 Z/ s& y
  14.     169     blo 1b5 G; j( V! l- y, R: N: Q4 N2 P
  15.     170     mov r5, #0              @ unknown processor7 [, R) E9 L1 \' `: Q% I7 ]
  16.     171 2:  mov pc, lr
    0 ~7 y! R" V: l7 j8 B. f5 f# }7 q7 R

  17. / ]0 T9 v  P; n, p" S. @' e$ o
  18.     187     .long   __proc_info_begin: e$ i5 s7 H' V
  19.     188     .long   __proc_info_end
    % t4 r4 A# g, K# c, @& @
  20.     189 3:  .long   .. Y& X% R/ d- ]1 s4 s8 T
  21.     190     .long   __arch_info_begin( k- d& s# ~) D% G7 s8 _
  22.     191     .long   __arch_info_end
複製代碼
跳了很多檔案,建議指令和object code如何link的概念要有,就會很清楚了。
3#
 樓主| 發表於 2008-10-13 17:20:35 | 只看該作者
我們從 head-common.S返回之後,接著繼續看。  v0 Q* q" L3 c% ]( r; F; E
! G4 e  F1 I" _4 n) }/ \
line 84, movs的意思是說,做mov的動作,並且將指令的S bit設起來,最後的結果也會update CPSR。這個指令執行的過程當中,會根據你要搬動的值去把N and Z flag設好。這個是有助於下個指令做check的動作。2 ?1 z' S6 U8 u2 o) T
line 85, 就是r5 = 0的話,就跳到__error_p去執行。) F3 `/ N! X" P/ T% C
line 86, 跳到__lookup_machine_type。原理跟剛剛找proc的資料一樣。
  1.      83         bl      __lookup_processor_type         @ r5=procinfo r9=cpuid
    1 A: r* O' Z' f* i' r5 F) ]
  2.      84         movs    r10, r5                         @ invalid processor (r5=0)?# e! [) d: d: K3 P
  3.      85         beq     __error_p                       @ yes, error 'p'% ]6 b& a) K7 E# p; }
  4.      86         bl      __lookup_machine_type           @ r5=machinfo
複製代碼
看得出來跟proc很像,有個兩個小地方是
5 _4 }7 e% i- M* r% G" Q: U5 f5 U9 `
1. line 207,用ldmia不是用ldmda。原因就在於存放arch 和 proc info 的位址剛好相反。一個在lable3的上面。一個在的下方。設計上應該是可以做修改的。
0 u6 |+ W* f2 F9 }; J0 P- K8 v6 Y6 S- @* C
2. arch定義的方式是透過macro,可以先看 ./include/asm-arm/mach/arch.h。裡頭有個 MACHINE_START ,這邊會設好macro,到時候直接使用就可以把arch的info宣告好。 例如 ./arch/arm/mach-omap1/board-generic.c
  1. /* macro */
    9 m- g9 ~/ C% H9 P8 U0 k# \1 K
  2.      50 #define MACHINE_START(_type,_name)                      \
    3 u+ ?9 V/ |0 m
  3.      51 static const struct machine_desc __mach_desc_##_type    \0 g& N; P$ X" e) Y8 t
  4.      52  __used                                                 \
    9 o6 m; V) j( K4 P7 u# x% K8 f4 e
  5.      53  __attribute__((__section__(".arch.info.init"))) = {    \- i( w; {- c; g) D/ P
  6.      54         .nr             = MACH_TYPE_##_type,            \
    1 G( K" I! U7 h( S; g
  7.      55         .name           = _name,4 M! V9 H) h5 F. r. |2 o; }
  8.      56
    % s! d% k, b9 t
  9.      57 #define MACHINE_END                             \- n! o, C2 o7 H6 @& ?- A
  10.      58 };
    + j/ z( F9 r" Y3 ^; \
  11.      /* 用法 */
    . s7 ]# o- T; Q: I
  12.      93 MACHINE_START(OMAP_GENERIC, "Generic OMAP1510/1610/1710")
    ' o6 _& A% \( S' `$ \% e
  13.      94         /* Maintainer: Tony Lindgren <tony@atomide.com> */
    8 A( ?% {  R( M; s; |2 P
  14.      95         .phys_io        = 0xfff00000,
    - z+ _. G6 ?2 }
  15.      96         .io_pg_offst    = ((0xfef00000) >> 18) & 0xfffc,' u3 U4 f( l6 h; d* i/ V2 I+ t
  16.      97         .boot_params    = 0x10000100,8 d- o1 z" |* M/ p
  17.      98         .map_io         = omap_generic_map_io,: N/ [0 q/ V6 G9 q& f" ^
  18.      99         .init_irq       = omap_generic_init_irq,- @) D; F) h4 r
  19.     100         .init_machine   = omap_generic_init,0 O% a/ H" }  n9 q  b( h( T
  20.     101         .timer          = &omap_timer,
    ) F  I+ A2 M4 d/ |
  21.     102 MACHINE_END3 p" F$ O2 ~5 Q! Y' a8 x

  22. 9 u) M- y+ a, R- I
  23.     /* func */, l" U  X7 z6 k  j5 P4 W
  24.     204         .type   __lookup_machine_type, %function7 @/ C# }9 J. R2 }& J0 v5 z
  25.     205 __lookup_machine_type:3 |' Z0 F8 B* {
  26.     206         adr     r3, 3b
    * v1 ^) p3 k+ e" y( Q4 b
  27.     207         ldmia   r3, {r4, r5, r6}1 ]& T7 z& N' t  B& o- e
  28.     208         sub     r3, r3, r4                      @ get offset between virt&phys
    1 J" }0 ]5 G1 H" _9 _
  29.     209         add     r5, r5, r3                      @ convert virt addresses to
    2 S9 o$ ~0 q. M+ D
  30.     210         add     r6, r6, r3                      @ physical address space2 r3 H5 N7 V' r( \
  31.     211 1:      ldr     r3, [r5, #MACHINFO_TYPE]        @ get machine type$ N1 F3 A8 b, {, i4 v
  32.     212         teq     r3, r1                          @ matches loader number?
    & T% M& Z' j7 I
  33.     213         beq     2f                              @ found
    4 s: F6 g* v$ C$ f5 |& P/ V* n
  34.     214         add     r5, r5, #SIZEOF_MACHINE_DESC    @ next machine_desc
    9 Y9 q- N: k+ L3 k$ m
  35.     215         cmp     r5, r6
    ) r7 D# w$ l! Z9 T* N8 Q
  36.     216         blo     1b% h& s  V$ U0 y
  37.     217         mov     r5, #0                          @ unknown machine
    2 H5 D( d5 w9 I9 @
  38.     218 2:      mov     pc, lr
複製代碼
4#
 樓主| 發表於 2008-10-13 17:56:46 | 只看該作者
接著我們又返回到head.S,1 C" f0 o8 X, J; S9 M  t
line 87~88也是做check動作。$ i: ?/ Z2 D) C; x7 |" i
line 89跳到vet_atags。在head-common.S
  1.      87         movs    r8, r5                          @ invalid machine (r5=0)?& R$ J4 Y9 q. x8 x6 J7 H
  2.      88         beq     __error_a                       @ yes, error 'a'$ K+ \5 _0 O' t$ H6 p' Y& Y, U
  3.      89         bl      __vet_atags
    2 \, _1 w  T' i+ V$ R! _
  4.      90         bl      __create_page_tables
複製代碼
line 245, tst會去做and動作。並且update flags。這邊的用意是判斷位址是不是aligned。, I6 S3 |* P. p3 y( H" g" t
line 246, 沒有aligned跳到label 1,就返回了。
. r5 E0 z8 @& G! @) ^" e. X: G4 I( nline 248~250, 讀取atags所在的address裡頭的值到r5,看看是否不等於ATAG_CORE_SIZE,不等於的話也是返回。- u8 i& Y) K( T3 N, C) E. @: S
line 251~254, 判斷一下第一個達到的atag pointer是不是等於ATAG_CORE。如果正確的話,等一下要讀取atag的資料,才會正確。
7 l2 a9 q9 ?6 a) E0 G. J) Y# _+ V(atag是由bootloader帶給linux kernel的東西,用來告知booting所需要知道的參數。例如螢幕寬度,記憶體大小等等)
  1.      14 #define ATAG_CORE 0x54410001
    * z* P* E) m  q& K
  2.      15 #define ATAG_CORE_SIZE ((2*4 + 3*4) >> 2)
    3 c3 x2 c$ Z' i
  3. . x- W' a5 a: {6 e# P' N% s0 L
  4.     243         .type   __vet_atags, %function
    7 }. W" W/ b& f1 [9 d8 z9 L
  5.     244 __vet_atags:  J8 f; e: ^& i
  6.     245         tst     r2, #0x3                        @ aligned?
    : J. s) X7 K. C6 f* `, J0 D
  7.     246         bne     1f5 K- y$ X- P: \# O* |
  8.     247. d9 p( ~8 E; q! r5 X
  9.     248         ldr     r5, [r2, #0]                    @ is first tag ATAG_CORE?4 r! l2 ]$ n% `* G8 T
  10.     249         subs    r5, r5, #ATAG_CORE_SIZE
    5 f$ {5 w; g& {! x2 I. f
  11.     250         bne     1f
    7 l- h9 r& h/ i, c0 G
  12.     251         ldr     r5, [r2, #4]" z! g% L9 L* A4 R: _
  13.     252         ldr     r6, =ATAG_CORE
    - _0 p  C7 B6 [$ B5 P0 [
  14.     253         cmp     r5, r6: U# M) I7 ~9 n8 |/ V
  15.     254         bne     1f
    . c" y% T$ K: J
  16.     2553 c& q( K! y( |% \. |7 }
  17.     256         mov     pc, lr                          @ atag pointer is ok5 B# q0 w' f% M  r* _
  18.     2576 n2 @+ Y& u$ ^1 o
  19.     258 1:      mov     r2, #0
      M: y4 x1 r) y. g8 O$ I2 A+ K
  20.     259         mov     pc, lr
複製代碼
接著我們又跳回去head.S。  
6 p3 u4 S; T* L" l# O" xline 90,又跳到 __create_page_tables。   (很累人....應該會死不少腦細胞)
2 [. G3 k' ]. f# o" d, h哇!page table?!!如雷貫耳的東西,不知道會怎麼做。剛剛偷看了一下,@@還蠻長了,先到這邊好了,下次在繼續寫。
5#
 樓主| 發表於 2008-10-14 12:13:51 | 只看該作者
由於code看起來似乎越來越難解釋,會需要在檔案之間跳來跳去。建議一些基礎的東西可以再多複習幾次(其實是在說我自己 ),閱讀source code上收穫會比較多。例如:# @7 ^0 L+ n0 }1 }: A5 P0 b

( x; x1 C* e7 M! m& ]7 ?5 I1. arm instruction set - 這個最好每個指令的意思都大略看過一次,行有餘力多看幾個版本,armv4, v5 or v6。, x' ]$ v0 Q2 t% m7 x: u
2. compiler & assembler & linker - toolchain工具做的事情和功能,大致的流程和功能要有概念。
! y; _: R1 L  _" ^+ ]6 u1 ^3. Makefile & link script - 這兩個功能和撰寫的方式要有簡單的概念。
" |8 q) a. h0 }0 h( W* M2 h
! c4 _) \( I$ N/ T8 @# t; t. B以上1是非常重要的重點,2&3只要有大致上的概念就可以,因為trace code的時候,有時需要跳到Makeflie&Link script去看最後object code編排的位址。
& a! s2 A6 c& @  `- ]) H8 K/ I; j3 z, l5 ?8 R% D
由於我們trace到了page table這個關鍵字,在開始之前,稍微簡短的解釋,為了幫助了解,儘量用易懂的概念講,有些用詞可能會和一些真實狀況不同,但是懂了之後,應該會有能力分辨,至於正式而學術上解說,很多書上應該都有,或是google一下就很多啦∼
6#
 樓主| 發表於 2008-10-14 12:14:47 | 只看該作者
page table本身是很抽象的東西,尤其是對所謂的 user-mode application programmer 來說,大部分的狀況它是不需要被考慮到的部份,但是他的產生卻對os和driver帶來了很多影響。我們從一個小小的疑問出發。7 m% {3 E( k1 I. D

- a$ m% A0 {: W, I『產生page table到底是要給誰用的?』9 Z8 v2 ?) n7 s7 ]
5 n; I) a$ t9 _; t- [: ~
其實真正作用在它上面的H/W是MMU,一旦CPU啟用了MMU,當cpu嘗試去記憶體讀取一個operand的時候,cpu內部打出去的位址訊號都會先送到mmu,mmu會拿著這個位址去對照page table,看看這個位址是不是被轉換到另外一個位置(還會確認讀寫權力),最後才會到真正的位址去讀寫。
# S3 F/ O/ U2 v, o; y; o- B  `" T4 W& B/ Z/ N7 x
這樣來看CPU其實一開始打出去的位址訊號其實不是最後可以拿到資料的位址,我們稱為virtual address(va),mmu打出來的位址才能真正拿到資料,所以我們把mmu打出去的位址稱為physical address(pa)。那用來查詢這個位址對照關係的表格,就是page table。8 u; K' `, r; P4 j" Y" o  f3 V
8 y5 h! I2 z6 u6 q8 i4 t" T* ~) W3 P
到這邊我們有一個簡單的概念。來想像一個小問題,一個普通的周邊(例如你的顯示卡),因為他並不具有MMU功能,hw只看得懂pa,但是CPU卻是作用在va,假如我想去對hw的控制暫存器做讀寫,到底要用va還是pa?
7#
 樓主| 發表於 2008-10-14 12:15:51 | 只看該作者
這時,寫driver的人就必須要小心,設定給硬體看的位址必須要先轉成pa(像是設定DMA),給CPU執行的程式碼要使用va,這對一開始嘗試寫driver但是又不瞭解va pa之間的差別的人,常常產生很多疑問。2 I; T3 k# y# \* a- {+ o$ `5 \
% t; |$ ]3 u! K8 k5 i, a
現在我們回頭想想OS,既然我們跑到create page table,可以預期的是他想要將MMU打開,因此希望預先建立起一個page table,讓MMU知道目前os想規劃的位址對應和讀取權力是如何被安排的。所以os必須考慮到現在的系統究竟是長怎樣?應該要如何被安排?是不是有那些要被保護?好吧∼因為我們完全對os不了解,也不知道該安排什麼,只能祈禱在trace code完後,可以找到這些問題的答案,或者發現一些沒想到的問題。
. A9 b/ J/ ?/ S9 Z* v
7 W5 n+ T, i- T6 a6 U; V& q* n/ F知道了page table的大致上的功能,下篇就可以專心的研究這個table的長相,和它想規劃出的系統模樣。
9 ?3 J( j# @3 H9 c5 W. u% ]8 A' T* l7 w0 [" [( X& i, I
p.s. 字數限制好像變短了。   (看來很難寫)
8#
 樓主| 發表於 2008-10-14 13:56:17 | 只看該作者
由於字數限制挑整,用詞儘量精簡,以便可以貼必要source。另外,以後寫到一段落,考慮收集成一篇完整的文章,這樣應就不會因為用回覆的方式,造成閱讀斷斷續續的問題。希望有人對文章呈現方式有想法的話,可以跟我講。
9#
 樓主| 發表於 2008-10-14 13:57:39 | 只看該作者
現在,讓我們跳入create_page_tables吧∼
- H" Q% g2 T3 A) Q' r% D% Nline 216,會跳到pgtbl的macro去執行,其實只是載入一個位址。, z; A* m# D" u

3 l7 z: h, a/ t% K' ~3 p3 x只是這個位址因為你硬體規劃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 */" [* v0 X  B+ j! @, C
  2.      95 textofs-y       := 0x000080009 k& Q: V: i4 I0 {7 I
  3.     152 TEXT_OFFSET := $(textofs-y)
複製代碼
  1. /* include/asm-arm/arch-omap/memory.h */
    2 G  ?) n. G1 e; W/ x
  2.      40 #define PHYS_OFFSET             UL(0x10000000)
    2 v( |/ o5 Q, p" B  @
  3. " [5 B; \$ l6 U. o) w
  4.      /* arch/arm/kernel/head.S */
      M( z% T3 C9 Z1 C- U
  5.      29 #define KERNEL_RAM_VADDR        (PAGE_OFFSET + TEXT_OFFSET)
    / H5 q. e1 j( s
  6.      30 #define KERNEL_RAM_PADDR        (PHYS_OFFSET + TEXT_OFFSET); a. }9 h; Z* X' z
  7. # J) E  z  q, `. L: E
  8.      47         .macro  pgtbl, rd
    * u. p, J% _  s0 n- Z
  9.      48         ldr     \rd, =(KERNEL_RAM_PADDR - 0x4000)
    # o& S2 q3 [1 }7 e; v
  10.      49         .endm
    : U" p3 u; ?9 q5 a0 R

  11. 3 e* r7 D( K# ]$ P" o
  12.     216         pgtbl   r4                              @ page table address
複製代碼
10#
 樓主| 發表於 2008-10-14 14:16:53 | 只看該作者
得到pg table的開始位置之後,當然就是初始化囉。
6 C- v& B1 G7 ?: }. bline 221, 將pg table的base addr放到r0.
- @5 ^/ q3 j% ?1 [9 @# ]line 223, 將pg table的end addr放到r6.
- N7 D$ C) L3 L" Y( g  J1 aline 224~228, 反覆地將0x0寫到pg table的區段裡頭,每個loop寫16bytes. (4x4),直到碰到end,結果就是把它全部clear成0x0.
  1.     221         mov     r0, r46 N% d( Y' k) @! f6 \
  2.     222         mov     r3, #0
    4 B3 G/ {1 [: ?3 H8 @
  3.     223         add     r6, r0, #0x4000
    7 c6 l  y* }- x6 u
  4.     224 1:      str     r3, [r0], #4: T0 K/ H7 }, }
  5.     225         str     r3, [r0], #4
    : J% o, \7 P. K+ p9 n: S0 [
  6.     226         str     r3, [r0], #4
    5 g2 J, R! q8 \% x9 Y
  7.     227         str     r3, [r0], #46 t0 ?0 i; y/ _+ \4 h  w1 }
  8.     228         teq     r0, r6
    3 H) B" s1 D7 r# 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 | 只看該作者
問題怎麼填值??
$ e1 m6 b0 d5 C0 g/ }9 j1 v/ C拿出ARM的手冊,翻到MMU章節。一看發現page有很多種,還得分first level和second level。
, e. E( \) z9 q  v; o6 l2 ?9 [% u
! j% f! s: D0 J2 ~- x念書時的印象,是從1st level在去查2nd level,先看怎麼設定1st level (不知以前老師有沒有亂教)
6 E- n, J- Q6 k1. [31:20]存著section base addr0 X/ R) Q3 O3 o8 `* I* |' c0 ?) R3 J
2. [19:2]存著mmu flags
1 W% l6 X: C0 x1 O/ ~6 T3. [1:0]用來辨別這是存放哪種page, 有四種:+ s- c* C5 j( t; ^  m/ g- Q! c
   a. fault (00) b. coarse page (01) c. section (1st level) (10) d. fine page (11)3 F( h8 ?# }, J  I, h
4. pg tabel資料要存放到 [31:14] = translation base, [13:2] = table index , [1:0] = 0b00 的位址! I$ z- U; z0 b

/ i7 Q9 C) }" a, c! P+ y9 P0 Q來看code是怎麼設定。8 I3 M. N: E, L* z* A' A" ]  Q

) c( A3 ~1 Z9 wline 239, 將pc的值往右shift 20次放到r6.這是取得前20高位元的資料,有點像是 1.。
1 L8 T) D: N" k* @9 Jline 240, 將r6往左shift 20次之後,和r7做or的動作,剛好也是 2.提到的mmu flags和1.處理好的資料做整理。
/ e5 @! |3 g3 M* T所以前面兩個做完,就完成了bit[31:2]。
; o3 C0 L/ d0 f) E5 l( ~9 lline 241, 將r3的資料寫到r4+(r6<<0x2)的地方去,剛好是4.提到的位址。(lucky)
  1.     239         mov     r6, pc, lsr #20
    ( D* a) G; ~3 {( [
  2.     240         orr     r3, r7, r6, lsl #20
    6 p, s, S$ Y' P; h: R7 d
  3.     241         str     r3, [r4, r6, lsl #2]
複製代碼
p.s. 用pc值剛好可以算出當前的page。
12#
 樓主| 發表於 2008-10-22 19:47:03 | 只看該作者
最近又被釘上了,開始忙碌,大概沒太多時間貼文∼
+ m* O) T, S) N$ R  a2 ^4 I5 o( U. f" t
上篇已經將pc所屬的page table entry(pte)設定好,接著繼續看3 }* v9 u& J9 f2 ?: |2 V1 Y0 m4 ~. b
line 247, 248, 將KERNEL_START的位址往右shift 18算出pte的offset,(等於line239&241,239&241是先shift right 20然後shift left 2),並將剛剛r3的值設給pte。『!』的意思是會把address存到r0。. h$ X: s& H( ]: S: a

7 ]+ ~; ?# I! y  Jline 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) >> 186 l  V8 E2 E  ]
  2.     248         str     r3, [r0, #(KERNEL_START & 0x00f00000) >> 18]!
    / X6 b, P/ d# L3 K6 L) r7 t  ?
  3.     249         ldr     r6, =(KERNEL_END - 1)
    / P8 |* v5 O# N, z+ c
  4.     250         add     r0, r0, #41 |/ N7 Q/ v8 j  u0 |. Z
  5.     251         add     r6, r4, r6, lsr #185 t) L6 `- g, d# g; n6 ?) v
  6.     252 1:      cmp     r0, r6# B. k8 L8 S3 F0 P) J% ?# y
  7.     253         add     r3, r3, #1 << 20! e6 b& }# ^6 C# u3 E5 d
  8.     254         strls   r3, [r0], #44 k  k" V, l3 e2 G
  9.     255         bls     1b
複製代碼
13#
 樓主| 發表於 2008-10-22 20:24:58 | 只看該作者
line279,PAGE_OFFSET是規範RAM mapping完後的virtual address,我們將這個位址所屬的pte算出來放到r0。" \( @! u" r- j0 Q) I( h0 l
line 280~283,將要 map 的physical address的方式算出來放到r6。
3 U6 D9 v/ V/ u" Nline 284,最後將結果存到r0所指到的pte。
/ u, {- X# M) v1 [# T. |0 p  \  i; }% W  }6 G/ L
以上三個動作,就是做好 ram 起頭的位址一開始map,還沒做完整塊map。由於我們目前的設定方式每塊是1MB,所以這邊是將RAM開始的1MB做map。! Y4 l* Y  e) o4 M# w/ M
" g  V7 v) ]! S9 k( e7 b
line 327,返回,結束一開始的create page table的動作,我們可以看出其實並沒有做完整的初始化page table,所以之後應該會有其他page table的細節。
  1.     279         add     r0, r4, #PAGE_OFFSET >> 18  _' V( e# f5 Y, X* q: A
  2.     280         orr     r6, r7, #(PHYS_OFFSET & 0xff000000)! T; @  U: P1 g( N
  3.     281         .if     (PHYS_OFFSET & 0x00f00000)% f' w3 A  ]2 A5 y' u5 R
  4.     282         orr     r6, r6, #(PHYS_OFFSET & 0x00f00000)
    5 l/ C, {( @1 V/ i2 z
  5.     283         .endif
    : y3 F% e2 F4 |# [" `4 z2 q
  6.     284         str     r6, [r0]6 j3 F5 Q3 X) E
  7.     327         mov     pc, lr
複製代碼
附帶一提,我們這邊省略了一些用ifdef包起來的程式碼,像是一開始會印一些output message的程式碼(line286~326)。
14#
 樓主| 發表於 2008-10-22 20:37:08 | 只看該作者
自create page table返回後,我們偷偷看一下接下來的程式碼,2 |; y, q7 p+ x$ s
line 99, 將switch_data擺到r13
* N1 c% b& b0 u+ E8 B& j; y5 @2 lline 101, 將enable_mmu擺到lr$ |8 Q/ m4 ^, d+ ?
line 102, 將pc改跳到r10+PROCINFO_INITFUNC的地方去
6 A3 ~/ S* R9 }! p
3 h: V9 V% @# j% x& h) \其實這邊有點玄機,switch_data和enable_mmu都是function,結果位址都只是被存起來,並沒有直接跳過去執行。其實,雖然程式碼是順序switch_data->enable_mmu->proc init function,但其實執行的順序會是 procinfo 的init function-> enable_mmu -> switch_data 。至於,為什麼要這樣寫的原因?就不清楚了,也還沒仔細推敲過。
4 \3 }4 m& m9 f" R7 G( \. R  O
" P! K0 b% C( Q& {* V4 vswitch_data最後就會跳到大家都很熟悉的start_kernel(). 詳細要賣個關子,最近會忙一下,得要過一陣子才能貼文∼  
  1.      99         ldr     r13, __switch_data              @ address to jump to after8 F! B2 l1 Q% \  M& R
  2.     100                                                 @ mmu has been enabled
    . {7 L: x. Y. f0 @0 K9 ?
  3.     101         adr     lr, __enable_mmu                @ return (PIC) address/ D5 I1 m& a+ ~7 }) J9 A( T+ b
  4.     102         add     pc, r10, #PROCINFO_INITFUNC
複製代碼
15#
 樓主| 發表於 2009-7-4 01:09:36 | 只看該作者
老店重新開張~
4 m) r& O7 s9 H: o) }
) x, b" a3 O3 O& P花了一些時間把舊的貼文整理到一個blog
8 y) M! _2 h9 Z' g. ~) n有把一些敘述修改過( L% u6 G4 A2 ^) d6 z
希望會比較容易集中閱讀) p/ o) F5 _4 j' g8 Q1 |& ?/ {
目前因為某些敘述不容易( k! H+ i" y; _: q9 A( {1 @
還是比較偏向筆記式而且用字不夠精確
8 |1 c0 Y/ i" i* Q, C9 G' ~  n希望之後能夠慢慢有系統地整理: n+ w/ W/ q6 z3 I
大家有興趣的話. b- v6 v& R* r9 B) J/ K  s! |. M) E
可以來看看和討論
( [5 |, k& S3 N( R) j/ u& uhttp://gogojesseco.blogspot.com/
# V  ], U: c# C/ Z- w- j' B& M+ X3 s# o# g, V9 Z" g1 P
以後可能會採取  先在chip123貼新文章
) _5 I  {/ J# q0 s- Z9 c) H* ]1 ?5 u慢慢整理到blog上的方式
( B+ a. E! L5 H; W因為chip123比較方便討論 =): b0 _$ n- F+ `9 A0 K
blog編輯修改起來比較方便
2 A; v; b7 |; \0 G) Z) O閱讀也比較集中   大家可以在這邊看到討論
' Q9 d( Q' a. _然後在blog看到完整的文章 (類似BBS精華區的感覺)

評分

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

查看全部評分

16#
 樓主| 發表於 2009-7-15 17:07:03 | 只看該作者
隔了很長一段時間沒update8 m' f* X) U; ^$ y, o
之前程式碼走到 ./arch/arm/kernel/head.S 的 line 99 附近
  1.      99         ldr     r13, __switch_data              @ address to jump to after
    + c2 s# J3 q5 J) U. K
  2.     100                                                 @ mmu has been enabled$ d- }, U7 d5 h6 b- d' z
  3.     101         adr     lr, __enable_mmu                @ return (PIC) address0 s# T( E: f6 \% S. B) v' B
  4.     102         add     pc, r10, #PROCINFO_INITFUNC
複製代碼
line 99, 將__switch_data放到r13。(留作之後用)
, r/ O* j7 Q8 K( M$ @line 101, 將__enable_mmu的addr放到lr。(留作之後用)2 G; J7 v" Z2 {9 L3 q$ \
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' ?3 S% D, I/ c% b8 m# }
  2. 374 __arm926_setup:4 v) _3 _" \3 O' o& x
  3. 375         mov     r0, #0
    ! T9 `- H# s5 ^% a" D
  4. 376         mcr     p15, 0, r0, c7, c7              @ invalidate I,D caches on v4
    2 t) p' `" \( k2 e
  5. 377         mcr     p15, 0, r0, c7, c10, 4          @ drain write buffer on v4
    / i  r7 |0 p$ [5 P% i
  6. 378 #ifdef CONFIG_MMU
    7 J8 N3 |2 S, T- `; Y
  7. 379         mcr     p15, 0, r0, c8, c7              @ invalidate I,D TLBs on v42 Z3 k3 U% w- J, ~
  8. 380 #endif* `0 P; J6 A  z1 Z- @
  9. ) B; j6 a1 w1 U- @
  10. 388         adr     r5, arm926_crval
    + x# Y& [- C9 b1 U( E8 k
  11. 389         ldmia   r5, {r5, r6}
    9 G4 [! `( p7 z
  12. 390         mrc     p15, 0, r0, c1, c0              @ get control register v4- l6 k! z  o5 m- P7 @8 S! F
  13. 391         bic     r0, r0, r52 {, J5 @2 H6 r- d' [  |, x; ^  l
  14. 392         orr     r0, r0, r6& Y+ T& Z4 Q* E, }+ N$ X# p

  15. / A1 [  t# G7 A
  16. 396         mov     pc, lr( F; S' G3 c, c! m7 Y) g
  17. 397         .size   __arm926_setup, . - __arm926_setup
複製代碼
這邊的程式碼就跟CPU有很大的相依性,6 |" A5 F  m" ^4 O' ]4 J8 @9 j+ p
line 375~380, 主要就是invalidate CPU的I&D cache和清空write buffer。( r& x5 e) V6 \3 r, B% n/ Z4 w/ u
line 388~392, 把cp15的設定讀出來,並且將一些預設狀態做好運算放到r0。(預設值從arm926_crval這邊可以取得。)
4 j' Z1 _+ E' a6 d5 G8 [* sline 396, 直接把pc跳到lr,因為我們之前已經將lr = enable_mmu,所以直接跳過去。
17#
 樓主| 發表於 2009-7-15 17:29:45 | 只看該作者
  1. 155 __enable_mmu:, K( U% k, v  z& z5 Q7 Q
  2. 170         mov     r5, #(domain_val(DOMAIN_USER, DOMAIN_MANAGER) | \/ N3 T  C: |  @" u" a$ P9 p
  3. 171                       domain_val(DOMAIN_KERNEL, DOMAIN_MANAGER) | \
    . b4 S5 ]" i6 Y- I1 L
  4. 172                       domain_val(DOMAIN_TABLE, DOMAIN_MANAGER) | \
    + g5 `) E, T1 F+ z, r5 O7 u
  5. 173                       domain_val(DOMAIN_IO, DOMAIN_CLIENT))
    ( _; @  Y. t6 r  v% n  b* U) X
  6. 174         mcr     p15, 0, r5, c3, c0, 0           @ load domain access register) ^, q" H2 r1 i% X
  7. 175         mcr     p15, 0, r4, c2, c0, 0           @ load page table pointer
    ( d4 p3 a0 r8 F) A; k/ B" W, `
  8. 176         b       __turn_mmu_on1 W3 C! I2 c5 y3 b
  9. 177 ENDPROC(__enable_mmu)
複製代碼
line 170~174,設置好domain access。(domain access可以先當成設置access的權限,或許以後可以寫詳細的文章說明)( X( _" p( C& f( g3 N" P# m
line 175~176,將create好的page table開頭丟給mmu。跳到turn_mmu_on
  1. 191 __turn_mmu_on:
    + y& Y) \3 a/ r' j4 ^! ^
  2. 192         mov     r0, r01 G$ Q# A: o2 {) u1 w, x
  3. 193         mcr     p15, 0, r0, c1, c0, 0           @ write control reg0 Q5 R0 }5 s" E
  4. 194         mrc     p15, 0, r3, c0, c0, 0           @ read id reg' X" C' v$ A2 O; Z
  5. 195         mov     r3, r3
    2 S" Y- E# W0 N. ]  ^/ D
  6. 196         mov     r3, r3
    4 V: G7 G% p/ \/ t
  7. 197         mov     pc, r13* `7 {$ r  l9 O) A) d* S
  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:
    . u% K4 |' d3 {- H9 I
  2. 19         .long   __mmap_switched! Z0 p$ _  B4 F% ?. b+ _
  3. 20         .long   __data_loc                      @ r4
    9 R& n" Y' d3 q( v
  4. 21         .long   _data                           @ r5
    ) h' B# i3 x6 L4 \' r3 l$ z, w* l
  5. 22         .long   __bss_start                     @ r6
      H$ o) ?: X( A- z$ o/ y: N
  6. 23         .long   _end                            @ r7
    ( @- H5 J/ p4 D  ]  ?8 a' E8 j  [
  7. 24         .long   processor_id                    @ r4
    . Z: K6 X7 }+ a/ Q
  8. 25         .long   __machine_arch_type             @ r56 }6 R' C; c& H7 ~8 r4 J
  9. 26         .long   __atags_pointer                 @ r6' j% R' [' I7 {3 U: L5 s. l: @+ H
  10. 27         .long   cr_alignment                    @ r7
    ! j, F) k4 O6 p$ g! O% M4 t
  11. 28         .long   init_thread_union + THREAD_START_SP @ sp
    ! P- `6 a0 @! ?( X! @6 N
  12. 29/ Q9 a0 ^( P/ z, ?' f
複製代碼
switch_data的第一行就是 __mmap_switched,所以我們直接看line 39。
18#
 樓主| 發表於 2009-7-15 17:30:00 | 只看該作者
  1. 39 __mmap_switched:
    $ a* p# n! B. l, ?
  2. 40         adr     r3, __switch_data + 4
    - }: `( N) n; A/ b+ I5 u; Z) _
  3. 41+ B  H0 B5 y+ h/ V' e5 }7 q
  4. 42         ldmia   r3!, {r4, r5, r6, r7}# y2 V7 E! l( x2 G% C+ |
  5. 43         cmp     r4, r5                          @ Copy data segment if needed% u! B9 _  d: S4 w$ C4 f5 y9 C$ G8 E
  6. 44 1:      cmpne   r5, r6* V" P. k& o" P; }
  7. 45         ldrne   fp, [r4], #4+ P  Y* Y1 x6 s. m
  8. 46         strne   fp, [r5], #4
    . _& [/ }$ Q% X$ O5 s
  9. 47         bne     1b
    4 S: r5 d9 i: Q3 R1 `4 Q$ f
  10. 48. r4 {3 m8 p5 H0 j. R
  11. 49         mov     fp, #0                          @ Clear BSS (and zero fp)
    . b1 ]0 c- e; A0 A- S
  12. 50 1:      cmp     r6, r7
    ( B9 _, P- K3 k8 J8 E
  13. 51         strcc   fp, [r6],#4( ~/ J2 H0 G( f
  14. 52         bcc     1b
    * Q$ E+ M  L- u3 u+ H, a
  15. 536 e* k& v7 }: x: J4 a  ^; v
  16. 54         ldmia   r3, {r4, r5, r6, r7, sp}
    / s6 @0 A: T+ n
  17. 55         str     r9, [r4]                        @ Save processor ID! b6 S5 q' [" N
  18. 56         str     r1, [r5]                        @ Save machine type
    & N9 n2 t( \1 ]8 Z
  19. 57         str     r2, [r6]                        @ Save atags pointer9 f8 `5 ]( w$ J4 F: m7 ~4 ?
  20. 58         bic     r4, r0, #CR_A                   @ Clear 'A' bit7 w7 D7 o$ q* o9 z5 x3 I  L
  21. 59         stmia   r7, {r0, r4}                    @ Save control register values
    / z# j. z( q% `+ `! B, |
  22. 60         b       start_kernel
    ; V6 F& T) x+ F: N, k
  23. 61 ENDPROC(__mmap_switched)
複製代碼
switch_data的第一行就是 __mmap_switched,所以我們直接看line 39。: h2 z- F+ _0 w
line 39,將__data_loc的addr放到r3  l1 v. i! E4 k
line 42,從r3的位址,連續讀取四筆資料到r4, r5, r6, r7
5 G1 C0 x2 q7 ~. Eline 43~47,看看data segment是不是需要搬動。, I" h9 ^$ @- a8 c; f& v  \. J
line 49~52, clear BSS。
% e7 \' E2 }8 ^0 F8 u0 T, Y* i( U& K' _+ R. Q
由於linux kernel在進入start_kernel前有一些前提必須要滿足:
  t) u* G4 _6 ?- B, Dr0  = cp#15 control register
/ X  g7 B5 O" a1 ^r1  = machine ID
1 T. C8 R: ?; Q5 K& [) br2  = atags pointer6 V' i1 g0 M% E& g+ k* F
r9  = processor ID
  M+ G3 _( d6 h) M# }" v0 T
( y/ j% g" z7 s- f4 {2 [所以line 54~59就是在做這些準備。6 B" j7 b9 q! Q+ z
最後呢? 我們就跳到start_kernel了。(而且還是用b start_kernel,表示我們不會再回來了)
- I5 q( ^6 `! Q' R* d% F5 i: J; |; z$ U$ l' [9 p$ I" A
看一下start_kernel()在./init/main.c,終於跳出architecture specific的目錄,表示2 @" k  V% J: m
我們真正的開始linux kernel的初始化。
. ~! v" _/ Y! m. k. [  b像是 shedule init, console init, memory init, irq init等等都在start_kernel裡頭。
7 z( N4 N3 d, i  S, Z% I到這邊之後,應該就可以深入linux kernel中,各項比較跟hardware不那麼相關的軟體部分。

評分

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

查看全部評分

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

本版積分規則

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

GMT+8, 2024-5-18 05:49 AM , Processed in 0.146019 second(s), 18 queries .

Powered by Discuz! X3.2

© 2001-2013 Comsenz Inc.

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