Chip123 科技應用創新平台

 找回密碼
 申請會員

QQ登錄

只需一步,快速開始

Login

用FB帳號登入

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

trace linux kernel source - ARM - 03

[複製鏈接]
跳轉到指定樓層
1#
發表於 2008-10-7 14:00:18 | 只看該作者 回帖獎勵 |倒序瀏覽 |閱讀模式
到目前為止,我們已經進展到kernel幫自己relocate完,並解將自己解壓縮到一個地方要準備開始執行,那疑問來了?到底是跳到哪裡去了,因為./compressed/head.S最後一行居然是
) I& e% e3 A( U『mov pc, r4』
5 \3 }- Y$ K2 Z( d) F" g- C' D" c. P( ^; hr4只代表了解壓縮完後kernel的位址,那究竟整包kernel編譯的時候,哪個function哪個東西被放在最前面咧?!# c- y/ G0 @$ u' T. F
4 s# W; P: z; Z5 p$ v2 Q
所以我們又必須開始找於kernel link的時候是怎麼被安排的,有了前面的基礎,我們可以從Makefile知道程式碼如何被編譯。至於link上的細節,例如有那些section和section先後順序等等,可以從 lds 檔來規範。
) \0 [: G* n- F1 Y. a4 C/ U5 P# Y' M/ ]; w
有興趣的人可以看一下 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)
: a. Q' ]# ]+ W8 y$ |我們可以發現第一個section是『.text.head』,裡頭的_stext從目前的位置開始放。& }5 I: l4 f7 ?
於是我們曉得只要找到屬於.text.head這個section,並且是_stext這個symbol的程式碼,就是解壓縮完後的第一行程式碼。
  1.      26     .text.head : {
    $ {4 E0 e# L% F. U& S" X, w+ w
  2.      27         _stext = .;
    " m; o7 j* a8 D
  3.      28         _sinittext = .;( P# v- t; P' k$ c4 r7 D4 }
  4.      29         *(.text.head)
    # b7 {7 c; H+ P% l+ y. C9 {
  5.      30     }
複製代碼
用指令搜尋一下,發現 ./arch/arm/kernel/head.S 裡頭有關鍵字(如下),結果我們從./arch/arm/boot/compressed/head.S跳到了./arch/arm/kernel/head.S
  1.      77     .section ".text.head", "ax"
    - W1 A5 f$ a& R' c
  2.      78     .type   stext, %function, L- Q( V0 T( ~2 [9 z
  3.      79 ENTRY(stext). \2 @  _3 V0 }  f* K( F; A, b" X
  4.      80     msr cpsr_c, #PSR_F_BIT | PSR_I_BIT | SVC_MODE @ ensure svc mode
    9 ^  ~2 W6 t! k6 a1 Z/ H
  5.      81                         @ and irqs disabled
    1 @6 v4 t; n9 M/ ^9 L
  6.      82     mrc p15, 0, r9, c0, c0      @ get processor id
    2 J# F  H, l  u, v# E9 A4 \
  7.      83     bl  __lookup_processor_type     @ r5=procinfo r9=cpuid
    9 D4 J( q$ m$ Z7 ?/ z
  8.      84     movs    r10, r5             @ invalid processor (r5=0)?
    . [* G+ \5 ?. l# a9 ~. ]
  9.      85     beq __error_p           @ yes, error 'p'
    8 ]# }& h4 ^0 t# E- n
  10.      86     bl  __lookup_machine_type       @ r5=machinfo
    6 o" }+ _6 Y+ p. u
  11.      87     movs    r8, r5              @ invalid machine (r5=0)?
    " M( F2 a& k+ K6 r* k9 B/ o" J( z
  12.      88     beq __error_a           @ yes, error 'a'
    0 u' ?) x3 K% l1 o. n! b. J+ K1 ^
  13.      89     bl  __vet_atags
    % x9 _' v/ }. |8 D6 p. |$ R' L
  14.      90     bl  __create_page_tables
複製代碼
既然找到了檔案,我們又可以開始繼續。% W6 A9 M3 V/ p; Q, N
, _* y) a+ w) i5 _; P1 W& z
看了一下,程式碼多了很多bl的跳躍動作,看來會在各個function跳來跳去。
分享到:  QQ好友和群QQ好友和群 QQ空間QQ空間 騰訊微博騰訊微博 騰訊朋友騰訊朋友
收藏收藏 分享分享 頂 踩 分享分享
2#
 樓主| 發表於 2008-10-9 15:32:16 | 只看該作者
既然跳到真正的kernel開始跑,表示進入重頭戲,在進入kernel之前arm的平台有一些預設的狀況,也就是說arm kernel image會預設目前的cpu和系統的狀況是在某個狀態,這樣對一個剛要跑起來的OS比較決定目前要怎麼boot起來。
9 U, \2 s0 k8 D$ g( M) L; N& n7 P% u2 T# r' @, j0 @! f# t' h" q
可以看一下comment,有清楚的描述。這也幫助我們了解為什麼decompresser的程式跑完之後,還要把cache關掉。
  1.      59 /*- \9 C2 ~& {$ B% e
  2.      60  * Kernel startup entry point.# t( q8 X6 ^% J1 ]
  3.      61  * ---------------------------8 B- Q" S( H, o* |' D' L
  4.      62  *: Z& t) H$ c- F* M" D& b
  5.      63  * This is normally called from the decompressor code.  The requirements8 Z" J: ~7 f9 \% u
  6.      64  * are: MMU = off, D-cache = off, I-cache = dont care, r0 = 0,
      ]+ q5 M8 y" ~9 g
  7.      65  * r1 = machine nr, r2 = atags pointer.
複製代碼
基於以上的假設,當program counter指到kernel的程式的時候,就會從line 80開始跑。  W9 O7 A7 C! C: r# [
line 80, msr指令會把, operand的值搬到cpsr_c裡面。這是用來確保arm cpu目前是跑在svc mode, irq&fiq都disable。(設成0x1就會disable,definition在./include/asm-arm/ptrace.h)
/ {3 }) @. P" L4 Rline 82, 讀取CPU ID到r9
, r5 R1 b" ]) o4 bline 83, 跳到 __lookup_processor_type 執行(bl會記住返回位址)。
  1.      77     .section ".text.head", "ax"
    8 o' k* }0 s: @! G
  2.      78     .type   stext, %function* u; u$ b1 X, u7 a1 i! D2 L
  3.      79 ENTRY(stext)1 a" ~7 m/ T! j" \& W, A
  4.      80     msr cpsr_c, #PSR_F_BIT | PSR_I_BIT | SVC_MODE @ ensure svc mode( U- n  _" f4 e2 w3 I
  5.      81                         @ and irqs disabled& K( o& @& s# ^. H# p9 x  {
  6.      82     mrc p15, 0, r9, c0, c0      @ get processor id
    - e0 K# M) a2 c1 `0 K
  7.      83     bl  __lookup_processor_type     @ r5=procinfo r9=cpuid
複製代碼
接著會跳到head-common.S這個檔,
" q6 E* h) w3 C" T) y* Jline 158, (3f表示forware往前找叫做『3』label)將label 3的address放到r3。
! E7 [' C6 b# G$ yline 159, 將r3指到的位址的資料,依序放到r7, r6, r5.(ldmda的da是要每次都位址減一次)
# C! Z, s; M8 K- kline l60, 161, 利用真實得到位址r3減去取得資料的位址得到一個offset值,這樣可計算出r5, r6真正應該要指到的地方。  K: i, d! h0 e) }- _2 p+ A
line 163~169,這邊的程式應該不陌生,就是在各個CPU info裡面找尋對應的structure。找到的話就跳到line 171,返回head.S
0 j; J& h+ {2 jline 170, 找不到的話,r5的processor id就放0x0.表示unknown id。$ [0 s9 F9 u; ]
; S& i& @) J7 D- v( C
__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% _( ^0 e+ H* B# a3 f
  2.     157 __lookup_processor_type:
    1 \/ h- E2 o) |) s& s
  3.     158     adr r3, 3f
    9 |' U" f5 U6 E
  4.     159     ldmda   r3, {r5 - r7}  @( ^1 z* ?- c  p" I) c& q% H
  5.     160     sub r3, r3, r7          @ get offset between virt&phys1 a5 s# `. j8 _) R+ H
  6.     161     add r5, r5, r3          @ convert virt addresses to
    1 s) B2 O" R+ d' O
  7.     162     add r6, r6, r3          @ physical address space
    7 z/ q5 l" ^* U" X' v, w3 Q- ]5 Q% }  \
  8.     163 1:  ldmia   r5, {r3, r4}            @ value, mask
    " }, J& k. G! F& P
  9.     164     and r4, r4, r9          @ mask wanted bits$ q; [; d- C- t
  10.     165     teq r3, r4
    8 D& x) i; s1 U* i  I2 t
  11.     166     beq 2f
    $ @' O, S  c& \' A% J. h& t0 y
  12.     167     add r5, r5, #PROC_INFO_SZ       @ sizeof(proc_info_list)! W3 V& G7 G/ ^
  13.     168     cmp r5, r6
    ! b1 O& B. E6 F; {
  14.     169     blo 1b/ K( |$ P6 ^+ i2 Y$ a$ }
  15.     170     mov r5, #0              @ unknown processor
    & ~) \% @: R9 j# P
  16.     171 2:  mov pc, lr% v4 u7 e7 [. M3 U! X) ^
  17. - p+ A( W3 f8 s& z8 B9 b
  18.     187     .long   __proc_info_begin
    6 Z: A: n9 ~# H5 H* E4 y
  19.     188     .long   __proc_info_end
    2 F) O! S2 i( k+ {
  20.     189 3:  .long   .
    , s  m' q/ B: P6 X% r6 H
  21.     190     .long   __arch_info_begin
    * f' X- o/ ?4 r0 J
  22.     191     .long   __arch_info_end
複製代碼
跳了很多檔案,建議指令和object code如何link的概念要有,就會很清楚了。
3#
 樓主| 發表於 2008-10-13 17:20:35 | 只看該作者
我們從 head-common.S返回之後,接著繼續看。
2 Q& R  L4 {' X) |8 y% T$ g
4 ~8 i  M" {, y" Gline 84, movs的意思是說,做mov的動作,並且將指令的S bit設起來,最後的結果也會update CPSR。這個指令執行的過程當中,會根據你要搬動的值去把N and Z flag設好。這個是有助於下個指令做check的動作。
* E+ o& a, {1 B% }# Kline 85, 就是r5 = 0的話,就跳到__error_p去執行。
; P' q% P: \1 eline 86, 跳到__lookup_machine_type。原理跟剛剛找proc的資料一樣。
  1.      83         bl      __lookup_processor_type         @ r5=procinfo r9=cpuid  E9 v1 I2 g. Z! m8 B) u4 J0 R/ z
  2.      84         movs    r10, r5                         @ invalid processor (r5=0)?& ~7 L7 Y- q; K% n8 g% Y: h
  3.      85         beq     __error_p                       @ yes, error 'p'! [9 M: _$ ]: S( t9 i- T
  4.      86         bl      __lookup_machine_type           @ r5=machinfo
複製代碼
看得出來跟proc很像,有個兩個小地方是
  e- p- l. j' b8 X! i) }6 x: _9 `7 w
8 J* `* c3 W5 m% E9 P9 m- @1. line 207,用ldmia不是用ldmda。原因就在於存放arch 和 proc info 的位址剛好相反。一個在lable3的上面。一個在的下方。設計上應該是可以做修改的。
" a) b' R9 ?/ g9 H/ H% n7 c5 b8 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 _* z1 M0 ]' _
  2.      50 #define MACHINE_START(_type,_name)                      \: F4 R6 I7 p, i$ @! G; m+ R
  3.      51 static const struct machine_desc __mach_desc_##_type    \: v& W3 c0 P% l7 o# B+ ~. g. z* s
  4.      52  __used                                                 \
    1 V- s5 r4 T) z- O
  5.      53  __attribute__((__section__(".arch.info.init"))) = {    \, H$ L; i% o' K
  6.      54         .nr             = MACH_TYPE_##_type,            \% C4 _/ {3 M0 @
  7.      55         .name           = _name,8 \4 U$ ]& y' ~2 r$ F( ?8 k
  8.      56
    5 W  w* n$ n+ V
  9.      57 #define MACHINE_END                             \
    ) X) Y8 v7 X! L4 G1 V: T
  10.      58 };  ?  r/ C  |+ d$ O3 `, K* k  x
  11.      /* 用法 */4 p3 K' I3 p, e
  12.      93 MACHINE_START(OMAP_GENERIC, "Generic OMAP1510/1610/1710")
    5 ~! q7 o, N1 e5 p7 D
  13.      94         /* Maintainer: Tony Lindgren <tony@atomide.com> */% a! J2 \0 d, ~4 M
  14.      95         .phys_io        = 0xfff00000,
    ) s( ^# e/ C5 ?4 W
  15.      96         .io_pg_offst    = ((0xfef00000) >> 18) & 0xfffc,
    ) u6 y6 y( w9 S/ i8 `5 i
  16.      97         .boot_params    = 0x10000100,7 y. B# a* ~8 o8 o0 w+ W! l( r
  17.      98         .map_io         = omap_generic_map_io,
    2 N# T5 _# ?8 v7 u: M
  18.      99         .init_irq       = omap_generic_init_irq,
    : w4 S& s/ s) U9 K% k: a8 n  k
  19.     100         .init_machine   = omap_generic_init,
    9 ?( O& C# ]5 s2 Y" t2 R* k
  20.     101         .timer          = &omap_timer,
    ( C. P  h! F( w
  21.     102 MACHINE_END( Y: _, U  \. @& @+ _9 O
  22. 5 i# |* ]" ?, L: _9 H( y
  23.     /* func */
    $ H+ O( d+ f& k) W) q! A
  24.     204         .type   __lookup_machine_type, %function
    / u2 G# {6 b) W) u6 U8 T' S5 U
  25.     205 __lookup_machine_type:
    . D6 }. l* H, I0 n
  26.     206         adr     r3, 3b6 W/ C" P& R1 }( I+ R! U8 w. U& G. ^* t
  27.     207         ldmia   r3, {r4, r5, r6}3 u- s3 u# G2 Z& o& d7 m/ ~
  28.     208         sub     r3, r3, r4                      @ get offset between virt&phys
      R" c/ X" D3 z( [3 W: U' ^
  29.     209         add     r5, r5, r3                      @ convert virt addresses to
    " K/ r$ c3 B) Y; m9 ^) t# E
  30.     210         add     r6, r6, r3                      @ physical address space, t' W. D& L: ?* O* s# Z
  31.     211 1:      ldr     r3, [r5, #MACHINFO_TYPE]        @ get machine type! Q! m" D9 M  M( O0 ^1 J
  32.     212         teq     r3, r1                          @ matches loader number?, t2 `1 L7 v) \
  33.     213         beq     2f                              @ found
    ! B1 ^  |( d( H& w9 d5 Z+ J
  34.     214         add     r5, r5, #SIZEOF_MACHINE_DESC    @ next machine_desc# W% l* w8 p! ^+ N
  35.     215         cmp     r5, r6
    0 B' C% Q: Y$ j0 V% k
  36.     216         blo     1b4 i9 K5 p, S3 }+ c- J$ P1 c/ m0 J. ^
  37.     217         mov     r5, #0                          @ unknown machine
      u; c/ B( ^' C- \! G
  38.     218 2:      mov     pc, lr
複製代碼
4#
 樓主| 發表於 2008-10-13 17:56:46 | 只看該作者
接著我們又返回到head.S,
2 V! W4 h+ R7 uline 87~88也是做check動作。
3 s6 ?6 E% @: n, V$ E) \# uline 89跳到vet_atags。在head-common.S
  1.      87         movs    r8, r5                          @ invalid machine (r5=0)?
    . S5 ?" e: ?/ R. Y8 Y4 I: x
  2.      88         beq     __error_a                       @ yes, error 'a'
    5 `+ X! b, v4 P9 ?
  3.      89         bl      __vet_atags! q6 K: ]" J1 t1 c9 ?8 o, w
  4.      90         bl      __create_page_tables
複製代碼
line 245, tst會去做and動作。並且update flags。這邊的用意是判斷位址是不是aligned。
4 y. d) {# ?/ m* G! I6 O3 ]line 246, 沒有aligned跳到label 1,就返回了。
9 z. l+ U" R" b9 Bline 248~250, 讀取atags所在的address裡頭的值到r5,看看是否不等於ATAG_CORE_SIZE,不等於的話也是返回。
6 k- D$ B& f1 m, xline 251~254, 判斷一下第一個達到的atag pointer是不是等於ATAG_CORE。如果正確的話,等一下要讀取atag的資料,才會正確。
* ]2 E" M. _" P1 D0 e# t! v; p- |(atag是由bootloader帶給linux kernel的東西,用來告知booting所需要知道的參數。例如螢幕寬度,記憶體大小等等)
  1.      14 #define ATAG_CORE 0x544100016 s8 w, H2 S1 N# ?1 ?% a
  2.      15 #define ATAG_CORE_SIZE ((2*4 + 3*4) >> 2)
    * w5 o3 x* ^7 X" X) f8 a

  3.   y. ]* n' w2 M! Q# x$ n
  4.     243         .type   __vet_atags, %function* {4 t" F; ~0 y; M  Z
  5.     244 __vet_atags:# }( }4 z" B' }4 ^: ~3 t2 d
  6.     245         tst     r2, #0x3                        @ aligned?
    ( s' W& U" f/ d
  7.     246         bne     1f
    ( R; n! P+ Y- x4 x
  8.     247
    . r  ]! b* }7 ~* r
  9.     248         ldr     r5, [r2, #0]                    @ is first tag ATAG_CORE?
    ! V* P4 Q0 {+ W) ?
  10.     249         subs    r5, r5, #ATAG_CORE_SIZE7 e2 {1 y; ~3 z4 v- O" x
  11.     250         bne     1f
    " E1 |# T& b0 `5 X8 u8 f0 ?
  12.     251         ldr     r5, [r2, #4]
    $ R1 X% g' F2 \5 V5 p. Q, P8 ~( f% _
  13.     252         ldr     r6, =ATAG_CORE
    9 \" \) ?/ i6 m: @" g8 t
  14.     253         cmp     r5, r6. o3 o6 \: I7 Q" k+ y; e3 w; R% @
  15.     254         bne     1f
    6 V# a- J5 |2 \, N: r
  16.     2550 K6 g! W8 a! N& ]6 U* R
  17.     256         mov     pc, lr                          @ atag pointer is ok  ~; {1 |+ W3 j0 @9 q% Q
  18.     257
    % H. V( R4 Y. Q# z9 B# B" i: U) [
  19.     258 1:      mov     r2, #0
    , A' n' d" L% g' K, k
  20.     259         mov     pc, lr
複製代碼
接著我們又跳回去head.S。  
4 n8 y% H( w7 }& @: W( }( oline 90,又跳到 __create_page_tables。   (很累人....應該會死不少腦細胞); h1 ]! W. K+ y, B8 S
哇!page table?!!如雷貫耳的東西,不知道會怎麼做。剛剛偷看了一下,@@還蠻長了,先到這邊好了,下次在繼續寫。
5#
 樓主| 發表於 2008-10-14 12:13:51 | 只看該作者
由於code看起來似乎越來越難解釋,會需要在檔案之間跳來跳去。建議一些基礎的東西可以再多複習幾次(其實是在說我自己 ),閱讀source code上收穫會比較多。例如:/ V, b9 N% Z+ S# n/ v+ h9 v$ B

4 R+ ^/ `6 q3 M. i1. arm instruction set - 這個最好每個指令的意思都大略看過一次,行有餘力多看幾個版本,armv4, v5 or v6。0 x% q' I3 y7 y( w% g! z5 A: v% t
2. compiler & assembler & linker - toolchain工具做的事情和功能,大致的流程和功能要有概念。' y5 f  u: ~1 O" V; t" ?4 C
3. Makefile & link script - 這兩個功能和撰寫的方式要有簡單的概念。
5 o2 a, A  e0 I5 [% R/ ^- J* Q5 ~2 l  c9 Z2 h" ^& I" }( ~
以上1是非常重要的重點,2&3只要有大致上的概念就可以,因為trace code的時候,有時需要跳到Makeflie&Link script去看最後object code編排的位址。
% d" @0 k! P" A3 _. j
" {# u5 S# S' S9 w# ?+ e由於我們trace到了page table這個關鍵字,在開始之前,稍微簡短的解釋,為了幫助了解,儘量用易懂的概念講,有些用詞可能會和一些真實狀況不同,但是懂了之後,應該會有能力分辨,至於正式而學術上解說,很多書上應該都有,或是google一下就很多啦∼
6#
 樓主| 發表於 2008-10-14 12:14:47 | 只看該作者
page table本身是很抽象的東西,尤其是對所謂的 user-mode application programmer 來說,大部分的狀況它是不需要被考慮到的部份,但是他的產生卻對os和driver帶來了很多影響。我們從一個小小的疑問出發。
7 i' _. i- e/ L' Z, J
0 _7 h5 r& [* E『產生page table到底是要給誰用的?』! M4 a, ]! w! g% ^) O

0 o1 J7 V+ c, \) a其實真正作用在它上面的H/W是MMU,一旦CPU啟用了MMU,當cpu嘗試去記憶體讀取一個operand的時候,cpu內部打出去的位址訊號都會先送到mmu,mmu會拿著這個位址去對照page table,看看這個位址是不是被轉換到另外一個位置(還會確認讀寫權力),最後才會到真正的位址去讀寫。- s, @& w8 b8 I. L2 P$ W

- T) `6 \2 `9 n這樣來看CPU其實一開始打出去的位址訊號其實不是最後可以拿到資料的位址,我們稱為virtual address(va),mmu打出來的位址才能真正拿到資料,所以我們把mmu打出去的位址稱為physical address(pa)。那用來查詢這個位址對照關係的表格,就是page table。# d, R1 O6 h2 {+ Z( p6 i. h$ i

6 X/ ^5 l4 J$ u! g: J% L2 Q到這邊我們有一個簡單的概念。來想像一個小問題,一個普通的周邊(例如你的顯示卡),因為他並不具有MMU功能,hw只看得懂pa,但是CPU卻是作用在va,假如我想去對hw的控制暫存器做讀寫,到底要用va還是pa?
7#
 樓主| 發表於 2008-10-14 12:15:51 | 只看該作者
這時,寫driver的人就必須要小心,設定給硬體看的位址必須要先轉成pa(像是設定DMA),給CPU執行的程式碼要使用va,這對一開始嘗試寫driver但是又不瞭解va pa之間的差別的人,常常產生很多疑問。' Z4 {* ^1 O& y7 i  m
) m2 {: q: Y( r8 S) _) ~
現在我們回頭想想OS,既然我們跑到create page table,可以預期的是他想要將MMU打開,因此希望預先建立起一個page table,讓MMU知道目前os想規劃的位址對應和讀取權力是如何被安排的。所以os必須考慮到現在的系統究竟是長怎樣?應該要如何被安排?是不是有那些要被保護?好吧∼因為我們完全對os不了解,也不知道該安排什麼,只能祈禱在trace code完後,可以找到這些問題的答案,或者發現一些沒想到的問題。& v+ A) g! [( ~; p" [) v& x6 v

- ]! p* h! k: l! s6 x知道了page table的大致上的功能,下篇就可以專心的研究這個table的長相,和它想規劃出的系統模樣。; S: v4 r1 i) ~  C5 O: \/ F

" Y9 s1 H; a3 N' R2 h# m( T6 kp.s. 字數限制好像變短了。   (看來很難寫)
8#
 樓主| 發表於 2008-10-14 13:56:17 | 只看該作者
由於字數限制挑整,用詞儘量精簡,以便可以貼必要source。另外,以後寫到一段落,考慮收集成一篇完整的文章,這樣應就不會因為用回覆的方式,造成閱讀斷斷續續的問題。希望有人對文章呈現方式有想法的話,可以跟我講。
9#
 樓主| 發表於 2008-10-14 13:57:39 | 只看該作者
現在,讓我們跳入create_page_tables吧∼
2 M, }0 ?- P" F, m. pline 216,會跳到pgtbl的macro去執行,其實只是載入一個位址。
) S) D. B0 D/ K6 z* j- k7 _; ?! {2 ?0 r. o4 ]
只是這個位址因為你硬體規劃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 h5 {3 y8 Z; ]
  2.      95 textofs-y       := 0x00008000
    # o  ^, R. A3 a+ I5 U4 [
  3.     152 TEXT_OFFSET := $(textofs-y)
複製代碼
  1. /* include/asm-arm/arch-omap/memory.h */. d* [/ s8 [. V1 T4 O  ?
  2.      40 #define PHYS_OFFSET             UL(0x10000000). V2 x& [4 `' y6 M

  3. ( _; u9 @$ i8 o1 P$ @5 M5 p% I5 Z2 H
  4.      /* arch/arm/kernel/head.S */
    2 H, e- \8 {9 I1 d' l
  5.      29 #define KERNEL_RAM_VADDR        (PAGE_OFFSET + TEXT_OFFSET)/ l6 v8 T) b! C5 \
  6.      30 #define KERNEL_RAM_PADDR        (PHYS_OFFSET + TEXT_OFFSET)0 {4 D2 [# Y2 j! _3 a

  7. 1 ^4 D3 H4 L& J6 }0 k& z' |) C" }/ U
  8.      47         .macro  pgtbl, rd
    & Q, ?+ d, u' `$ \1 Z) I- v9 Z# J
  9.      48         ldr     \rd, =(KERNEL_RAM_PADDR - 0x4000)0 {% E. x$ O. N; |- x) E+ d5 z. j# K
  10.      49         .endm3 s7 @$ L0 G2 R9 c
  11. 9 s# {* r3 S% g1 O
  12.     216         pgtbl   r4                              @ page table address
複製代碼
10#
 樓主| 發表於 2008-10-14 14:16:53 | 只看該作者
得到pg table的開始位置之後,當然就是初始化囉。' t7 j# C. [( y
line 221, 將pg table的base addr放到r0.
# M0 e- U) v; D4 K  gline 223, 將pg table的end addr放到r6.
9 K; P3 d4 Q' `9 U- mline 224~228, 反覆地將0x0寫到pg table的區段裡頭,每個loop寫16bytes. (4x4),直到碰到end,結果就是把它全部clear成0x0.
  1.     221         mov     r0, r4
    + M, f3 d* Y/ V& ?8 b+ k
  2.     222         mov     r3, #0
    & W8 \, G" R2 ^2 D2 \, x
  3.     223         add     r6, r0, #0x4000/ P* U% X7 }  I& n& D% |
  4.     224 1:      str     r3, [r0], #4- e/ [. k  [( n. m7 Q
  5.     225         str     r3, [r0], #4/ A; w3 A9 D5 F# h
  6.     226         str     r3, [r0], #4
    % ]- |  W5 c% \0 f
  7.     227         str     r3, [r0], #4
    . ~8 D# @3 n5 W0 x4 p4 N
  8.     228         teq     r0, r6# Q% r8 f9 F5 y0 N6 d9 _
  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 | 只看該作者
問題怎麼填值??! T  z3 x( I' Q) {) t& |
拿出ARM的手冊,翻到MMU章節。一看發現page有很多種,還得分first level和second level。 " R' B8 t+ W* Z! }
3 S+ n: n, S& N6 T
念書時的印象,是從1st level在去查2nd level,先看怎麼設定1st level (不知以前老師有沒有亂教)0 _- F- r1 p3 T  s, z0 w
1. [31:20]存著section base addr
' f: g+ L2 I0 F% H9 X2. [19:2]存著mmu flags* o( r% \' s8 w( V7 _
3. [1:0]用來辨別這是存放哪種page, 有四種:
3 U. h( I% `" s$ y! A   a. fault (00) b. coarse page (01) c. section (1st level) (10) d. fine page (11)' H3 {  G3 [9 e& I1 j- q' }7 s9 }: Z
4. pg tabel資料要存放到 [31:14] = translation base, [13:2] = table index , [1:0] = 0b00 的位址9 D, T9 {5 }+ U- A8 ]0 _" T
7 I- Y3 R( F: {5 n
來看code是怎麼設定。4 U9 f& V* x/ [( `
9 E4 V$ ?9 T+ n
line 239, 將pc的值往右shift 20次放到r6.這是取得前20高位元的資料,有點像是 1.。
( o' X  T$ I" `, P' |  [line 240, 將r6往左shift 20次之後,和r7做or的動作,剛好也是 2.提到的mmu flags和1.處理好的資料做整理。
$ u" |) e! U5 ^+ ^所以前面兩個做完,就完成了bit[31:2]。
. F# V0 l+ c0 t; o" gline 241, 將r3的資料寫到r4+(r6<<0x2)的地方去,剛好是4.提到的位址。(lucky)
  1.     239         mov     r6, pc, lsr #20
    ) D9 R, z6 \4 ?3 K
  2.     240         orr     r3, r7, r6, lsl #20
    & N9 y: o1 C1 Y; `0 z
  3.     241         str     r3, [r4, r6, lsl #2]
複製代碼
p.s. 用pc值剛好可以算出當前的page。
12#
 樓主| 發表於 2008-10-22 19:47:03 | 只看該作者
最近又被釘上了,開始忙碌,大概沒太多時間貼文∼
+ s5 z% g( s1 J% E
, m. _. Y! D4 o* Z# p上篇已經將pc所屬的page table entry(pte)設定好,接著繼續看
7 R4 ~/ O% b% X7 k+ @( n9 d* B! V% }# Kline 247, 248, 將KERNEL_START的位址往右shift 18算出pte的offset,(等於line239&241,239&241是先shift right 20然後shift left 2),並將剛剛r3的值設給pte。『!』的意思是會把address存到r0。
  C6 |4 u3 u2 r! A" v! J$ J- W
7 v5 b6 S5 w6 ~+ B: Dline 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
    ! r- M7 K" J% F9 E
  2.     248         str     r3, [r0, #(KERNEL_START & 0x00f00000) >> 18]! / e) r& D- x! i+ G. L; b7 ]
  3.     249         ldr     r6, =(KERNEL_END - 1)0 E9 X8 @/ v- @! \) _
  4.     250         add     r0, r0, #4
    ( x8 r0 o6 j) S% i# w) _
  5.     251         add     r6, r4, r6, lsr #18  K* `$ [( A$ ?- T
  6.     252 1:      cmp     r0, r6
    ' W' I+ `5 n! K; R6 B
  7.     253         add     r3, r3, #1 << 20
      g2 P; O9 h  b. E% a' g
  8.     254         strls   r3, [r0], #4, g+ o. G* u# d- z- |+ U2 y' o( W
  9.     255         bls     1b
複製代碼
13#
 樓主| 發表於 2008-10-22 20:24:58 | 只看該作者
line279,PAGE_OFFSET是規範RAM mapping完後的virtual address,我們將這個位址所屬的pte算出來放到r0。
3 F8 c2 Q- S/ b- u1 o: p6 Oline 280~283,將要 map 的physical address的方式算出來放到r6。# R* _; [. D8 O+ g
line 284,最後將結果存到r0所指到的pte。9 x% J4 S3 r! p; L5 V2 @9 m6 n! M
; m7 K9 P( \1 i
以上三個動作,就是做好 ram 起頭的位址一開始map,還沒做完整塊map。由於我們目前的設定方式每塊是1MB,所以這邊是將RAM開始的1MB做map。& E  a% B# S' |: s8 g' ?+ Y" a

% s# D1 P, {% Q: |line 327,返回,結束一開始的create page table的動作,我們可以看出其實並沒有做完整的初始化page table,所以之後應該會有其他page table的細節。
  1.     279         add     r0, r4, #PAGE_OFFSET >> 18
    ; N5 x! q" M7 r$ y; E: K1 K1 H
  2.     280         orr     r6, r7, #(PHYS_OFFSET & 0xff000000)
    5 ]" y7 r; Z( ~8 {
  3.     281         .if     (PHYS_OFFSET & 0x00f00000), i9 x% z& s1 ~/ x' w: t/ c
  4.     282         orr     r6, r6, #(PHYS_OFFSET & 0x00f00000)) c3 P+ I8 [- J4 \$ l) l
  5.     283         .endif
    : F$ p! r, l- c# x  z$ Q
  6.     284         str     r6, [r0]% W7 N& W% p! V
  7.     327         mov     pc, lr
複製代碼
附帶一提,我們這邊省略了一些用ifdef包起來的程式碼,像是一開始會印一些output message的程式碼(line286~326)。
14#
 樓主| 發表於 2008-10-22 20:37:08 | 只看該作者
自create page table返回後,我們偷偷看一下接下來的程式碼,- h, z! Y$ r. c% W  n
line 99, 將switch_data擺到r13# K: E$ p: L) c& n  ^# b
line 101, 將enable_mmu擺到lr1 i1 }' C0 M; @# u
line 102, 將pc改跳到r10+PROCINFO_INITFUNC的地方去
- _' c- w( w/ {' u+ `
; V7 a) q. h" {1 {& d- p其實這邊有點玄機,switch_data和enable_mmu都是function,結果位址都只是被存起來,並沒有直接跳過去執行。其實,雖然程式碼是順序switch_data->enable_mmu->proc init function,但其實執行的順序會是 procinfo 的init function-> enable_mmu -> switch_data 。至於,為什麼要這樣寫的原因?就不清楚了,也還沒仔細推敲過。 % ?1 o# G1 }" y1 O+ g) g( C3 L
% |) R% F% x  r) n
switch_data最後就會跳到大家都很熟悉的start_kernel(). 詳細要賣個關子,最近會忙一下,得要過一陣子才能貼文∼  
  1.      99         ldr     r13, __switch_data              @ address to jump to after( U' J2 d. u8 F* r; D  D
  2.     100                                                 @ mmu has been enabled' J" f* h* e3 u9 t/ j. \$ U4 A# o
  3.     101         adr     lr, __enable_mmu                @ return (PIC) address
    * Z7 i! v' |$ u* v8 o$ V* b# f
  4.     102         add     pc, r10, #PROCINFO_INITFUNC
複製代碼
15#
 樓主| 發表於 2009-7-4 01:09:36 | 只看該作者
老店重新開張~
8 f" }& _$ R$ O1 _, a7 A+ `' p( w9 O) b3 s0 e5 q3 k. s  p
花了一些時間把舊的貼文整理到一個blog
6 c0 h# U+ w9 h* J有把一些敘述修改過/ H2 V) \  F- ]* g
希望會比較容易集中閱讀3 K. }0 C& H9 ^% `
目前因為某些敘述不容易
1 F( z7 f( q- r' V: i# @還是比較偏向筆記式而且用字不夠精確- O1 Z) D$ J5 Q" F" f" K
希望之後能夠慢慢有系統地整理( J  p% @4 w( L' [
大家有興趣的話: V+ q- g7 ]! F" h
可以來看看和討論
9 E$ t! @, ?( V3 Khttp://gogojesseco.blogspot.com// I2 W- h; k: J" Y1 E- c

2 k7 T4 `- K6 {以後可能會採取  先在chip123貼新文章
: C. G3 A3 z3 o; T! [* b1 s慢慢整理到blog上的方式  v8 `0 H/ q$ U& t- W2 q6 c2 X
因為chip123比較方便討論 =)1 |9 w6 v) W' U/ v
blog編輯修改起來比較方便
1 G/ j' |1 w7 ?4 ^9 U3 {# w( K) L閱讀也比較集中   大家可以在這邊看到討論8 p0 X# @) @! Y- I/ O2 `
然後在blog看到完整的文章 (類似BBS精華區的感覺)

評分

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

查看全部評分

16#
 樓主| 發表於 2009-7-15 17:07:03 | 只看該作者
隔了很長一段時間沒update
# b7 @8 q. G& |6 Q9 _之前程式碼走到 ./arch/arm/kernel/head.S 的 line 99 附近
  1.      99         ldr     r13, __switch_data              @ address to jump to after
    , S+ q7 S; x! ], V
  2.     100                                                 @ mmu has been enabled
    " Z7 f) Z, E4 R4 c) `& ^
  3.     101         adr     lr, __enable_mmu                @ return (PIC) address
    * Y9 s4 u0 M' B8 H* |
  4.     102         add     pc, r10, #PROCINFO_INITFUNC
複製代碼
line 99, 將__switch_data放到r13。(留作之後用)
! _' I0 _9 R1 g5 \- L4 fline 101, 將__enable_mmu的addr放到lr。(留作之後用)
2 m( t9 V) K; m" U6 E! Yline 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
    % r0 x% W8 t( w# X" W$ @! j+ N
  2. 374 __arm926_setup:
    * [% A0 j8 Z2 Y
  3. 375         mov     r0, #0# n( ]  S+ |3 J  {  I3 G
  4. 376         mcr     p15, 0, r0, c7, c7              @ invalidate I,D caches on v4
    ( R/ J5 c8 G9 a
  5. 377         mcr     p15, 0, r0, c7, c10, 4          @ drain write buffer on v4
    ( @; [2 p! S, F7 p9 J
  6. 378 #ifdef CONFIG_MMU# @$ ], c6 I  q- G7 K. s
  7. 379         mcr     p15, 0, r0, c8, c7              @ invalidate I,D TLBs on v4+ d# q% @$ I2 V, j7 u/ c' ]( F
  8. 380 #endif
    6 N% |* M# H9 O0 K& H
  9. 3 u! t! c% O: w8 Q4 n: Q
  10. 388         adr     r5, arm926_crval- w! o1 h+ o# i2 _7 o
  11. 389         ldmia   r5, {r5, r6}
    4 |  K% U9 Q/ t$ ]) W2 q. C
  12. 390         mrc     p15, 0, r0, c1, c0              @ get control register v4: j- w( y% P7 g0 ]; W3 U# R- y5 a5 \
  13. 391         bic     r0, r0, r5; k# [& @2 d  C* h/ D9 \0 z
  14. 392         orr     r0, r0, r6
    ! P; L2 [8 f0 j! Q

  15. 7 [+ ?* r5 O( L& a  g  A! L) O
  16. 396         mov     pc, lr
    5 N9 ]* ^# H/ P$ k- E
  17. 397         .size   __arm926_setup, . - __arm926_setup
複製代碼
這邊的程式碼就跟CPU有很大的相依性,4 i* |( @( k- v+ h( I
line 375~380, 主要就是invalidate CPU的I&D cache和清空write buffer。
4 T; F+ `4 t# ^# p& C- J- h% m* _line 388~392, 把cp15的設定讀出來,並且將一些預設狀態做好運算放到r0。(預設值從arm926_crval這邊可以取得。)
7 G; z" B' t% r" ?line 396, 直接把pc跳到lr,因為我們之前已經將lr = enable_mmu,所以直接跳過去。
17#
 樓主| 發表於 2009-7-15 17:29:45 | 只看該作者
  1. 155 __enable_mmu:
    , m  w+ ]0 r- _5 _! v7 a
  2. 170         mov     r5, #(domain_val(DOMAIN_USER, DOMAIN_MANAGER) | \
    ' w  L5 [2 x. @$ u
  3. 171                       domain_val(DOMAIN_KERNEL, DOMAIN_MANAGER) | \3 q/ O5 ~* A, y+ ^# ~' g& y. x% b
  4. 172                       domain_val(DOMAIN_TABLE, DOMAIN_MANAGER) | \
    4 D# L$ {/ H$ B8 a( @* P4 o
  5. 173                       domain_val(DOMAIN_IO, DOMAIN_CLIENT))
    / [4 t: }! p3 ~) \- V" N! N
  6. 174         mcr     p15, 0, r5, c3, c0, 0           @ load domain access register
    9 F2 }/ s3 z9 K, B
  7. 175         mcr     p15, 0, r4, c2, c0, 0           @ load page table pointer- G; v3 v8 y* n0 W! y6 q( h
  8. 176         b       __turn_mmu_on
    8 |: }" D( G* G/ Q
  9. 177 ENDPROC(__enable_mmu)
複製代碼
line 170~174,設置好domain access。(domain access可以先當成設置access的權限,或許以後可以寫詳細的文章說明)
+ i- n5 v2 ]! }line 175~176,將create好的page table開頭丟給mmu。跳到turn_mmu_on
  1. 191 __turn_mmu_on:
    - r# L+ o3 W% [* K- J
  2. 192         mov     r0, r0: D2 `$ z+ g, v! E& [0 {& b4 p
  3. 193         mcr     p15, 0, r0, c1, c0, 0           @ write control reg7 E& V# l1 s: }' }
  4. 194         mrc     p15, 0, r3, c0, c0, 0           @ read id reg
    ' {# Z1 H, t  N1 [0 Z
  5. 195         mov     r3, r3
    , }/ \2 A& ?1 p1 ?
  6. 196         mov     r3, r3; r7 i) q7 s  T/ h1 D
  7. 197         mov     pc, r13: D8 Y0 c, W( n& C9 a4 ~& G( B, a
  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 P& e; `6 i' {# y+ \# r) J
  2. 19         .long   __mmap_switched
    * |  D1 M6 ^& P+ r- f' A0 }
  3. 20         .long   __data_loc                      @ r4" I7 B6 A7 t& H- _, d
  4. 21         .long   _data                           @ r52 }; w( |0 i) U$ H8 V" ^
  5. 22         .long   __bss_start                     @ r6
    2 h, Q/ F* L/ u3 \& n* i3 }1 S
  6. 23         .long   _end                            @ r7
    " ^/ R9 ]& ^5 g) F
  7. 24         .long   processor_id                    @ r4: w& ~8 k2 z) H3 u
  8. 25         .long   __machine_arch_type             @ r5
    & a& `+ S5 T  D* x3 u) L5 H
  9. 26         .long   __atags_pointer                 @ r67 u5 `7 L  f0 E
  10. 27         .long   cr_alignment                    @ r76 `# r. \+ b7 Q$ y' l: _, T
  11. 28         .long   init_thread_union + THREAD_START_SP @ sp
    # g8 c' I) J& g6 k
  12. 29
    / Z2 m- w( V& m* v6 n+ B
複製代碼
switch_data的第一行就是 __mmap_switched,所以我們直接看line 39。
18#
 樓主| 發表於 2009-7-15 17:30:00 | 只看該作者
  1. 39 __mmap_switched:
    + }$ f" P3 }; G7 t& x' |9 W, F+ N
  2. 40         adr     r3, __switch_data + 4; T! q9 s. d) x0 b9 `5 W9 }
  3. 41
    " o; U% E' {1 E' ^5 Y* Z/ o
  4. 42         ldmia   r3!, {r4, r5, r6, r7}
    ; y# ]( U- S5 }* h
  5. 43         cmp     r4, r5                          @ Copy data segment if needed
    ) W2 `, g* z3 y, V6 T
  6. 44 1:      cmpne   r5, r6
    8 D, a9 T) r" Y  L; L2 \$ R# F& F- X
  7. 45         ldrne   fp, [r4], #4
    3 l/ }* w& l6 m8 O) _
  8. 46         strne   fp, [r5], #4
      w2 I. E6 M7 g) F
  9. 47         bne     1b6 m, ~2 w9 u* P& ^: S
  10. 48" j* Y' Q* E) x# G) R& Z
  11. 49         mov     fp, #0                          @ Clear BSS (and zero fp)% d* ]( V/ j$ d9 w& ~* d% J- B
  12. 50 1:      cmp     r6, r7
    4 |0 V; N0 {  h4 F5 R
  13. 51         strcc   fp, [r6],#4
    , G4 s$ a* K- ^" i  h( a3 @3 D
  14. 52         bcc     1b, e1 V* c( H' ~( ]! ^2 Y6 g* t
  15. 53, B) r" o/ c% a( `8 G/ `
  16. 54         ldmia   r3, {r4, r5, r6, r7, sp}
    $ G) c+ A( M2 {/ ?7 U
  17. 55         str     r9, [r4]                        @ Save processor ID& Y. ~, r3 J* x8 y" @/ C# s) X; ^" F* o
  18. 56         str     r1, [r5]                        @ Save machine type
    ' Y; \1 B1 K+ V( v8 o* M. v
  19. 57         str     r2, [r6]                        @ Save atags pointer8 b& I" @  I( d) y" ~( A, ]% d* Y, d
  20. 58         bic     r4, r0, #CR_A                   @ Clear 'A' bit; p  t  W4 y0 ~2 M4 g. ]' C
  21. 59         stmia   r7, {r0, r4}                    @ Save control register values* U- U: j6 L' L, h+ K
  22. 60         b       start_kernel
    ) l) u7 s! @) g# E. V# H) B0 Y: i
  23. 61 ENDPROC(__mmap_switched)
複製代碼
switch_data的第一行就是 __mmap_switched,所以我們直接看line 39。
! v5 |* ~" P# ~line 39,將__data_loc的addr放到r3" E* u0 k4 R5 \! R
line 42,從r3的位址,連續讀取四筆資料到r4, r5, r6, r7
& w* p6 b( B$ Rline 43~47,看看data segment是不是需要搬動。/ H# Q; ]( W8 g
line 49~52, clear BSS。* l  E' M8 N6 g" |+ \  E6 V

- ]- X" i2 x, @$ f由於linux kernel在進入start_kernel前有一些前提必須要滿足:
7 O" W8 ?/ b7 R' r+ Kr0  = cp#15 control register: s' F6 B  @, j* E) J
r1  = machine ID" r, v8 ~0 T! L: }1 }, g
r2  = atags pointer( h, Q) d6 `6 }$ w2 e; P5 H
r9  = processor ID0 h' {' b- x9 ^7 l! F
* g+ |5 B  R8 x' M
所以line 54~59就是在做這些準備。4 I; g; F9 S, i, A
最後呢? 我們就跳到start_kernel了。(而且還是用b start_kernel,表示我們不會再回來了)
( ~$ A! m/ J. M2 t) }
3 I  d+ n5 p. \' s; Y2 U( s$ J看一下start_kernel()在./init/main.c,終於跳出architecture specific的目錄,表示- b$ ^) A, b9 M2 _8 Y+ ^% f2 J
我們真正的開始linux kernel的初始化。
# L4 x$ w2 e& w+ P1 p; y4 q! J像是 shedule init, console init, memory init, irq init等等都在start_kernel裡頭。
: j3 @: f* a9 E' O% }/ M7 J到這邊之後,應該就可以深入linux kernel中,各項比較跟hardware不那麼相關的軟體部分。

評分

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

查看全部評分

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

本版積分規則

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

GMT+8, 2024-6-14 12:07 PM , Processed in 0.142018 second(s), 18 queries .

Powered by Discuz! X3.2

© 2001-2013 Comsenz Inc.

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