Chip123 科技應用創新平台

 找回密碼
 申請會員

QQ登錄

只需一步,快速開始

Login

用FB帳號登入

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

trace linux kernel source - ARM - 03

[複製鏈接]
跳轉到指定樓層
1#
發表於 2008-10-7 14:00:18 | 只看該作者 回帖獎勵 |倒序瀏覽 |閱讀模式
到目前為止,我們已經進展到kernel幫自己relocate完,並解將自己解壓縮到一個地方要準備開始執行,那疑問來了?到底是跳到哪裡去了,因為./compressed/head.S最後一行居然是7 n8 ^" {9 Y/ r; [; ^: \" b9 Z' C
『mov pc, r4』. d* _/ X4 O9 }6 R
r4只代表了解壓縮完後kernel的位址,那究竟整包kernel編譯的時候,哪個function哪個東西被放在最前面咧?!
2 D. I9 d% W( s9 z' I) ]& E  B1 j+ r5 o! r# ^' _9 t
所以我們又必須開始找於kernel link的時候是怎麼被安排的,有了前面的基礎,我們可以從Makefile知道程式碼如何被編譯。至於link上的細節,例如有那些section和section先後順序等等,可以從 lds 檔來規範。* W, o1 z5 Y6 Y4 M7 N$ o

2 q( V: |) Q. U3 W+ l3 U: 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)8 g9 \) W7 T, T) W6 I6 I, h
我們可以發現第一個section是『.text.head』,裡頭的_stext從目前的位置開始放。
8 ^+ W! [* F$ n6 l* S7 N3 k於是我們曉得只要找到屬於.text.head這個section,並且是_stext這個symbol的程式碼,就是解壓縮完後的第一行程式碼。
  1.      26     .text.head : {
    8 }  M0 d& [) }3 `
  2.      27         _stext = .;4 R; G( ^9 n" V$ A6 Q6 h0 Q# u
  3.      28         _sinittext = .;; Y2 Y# c/ s9 L/ r0 R* J
  4.      29         *(.text.head)) u" Z- Z& V% x
  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+ C0 W* M$ P) b4 \0 w0 i
  2.      78     .type   stext, %function
    , [  C8 f- b  p& z# ]: P! Q
  3.      79 ENTRY(stext)
    5 h; t3 V% J% q  V3 J
  4.      80     msr cpsr_c, #PSR_F_BIT | PSR_I_BIT | SVC_MODE @ ensure svc mode: m' k3 @) h* ^2 q4 c
  5.      81                         @ and irqs disabled; T7 _) T# Y, c' A! p& a
  6.      82     mrc p15, 0, r9, c0, c0      @ get processor id
    # g! y' R' C: d
  7.      83     bl  __lookup_processor_type     @ r5=procinfo r9=cpuid
    " p2 j$ G" K: P: x3 i5 c5 c
  8.      84     movs    r10, r5             @ invalid processor (r5=0)?
      G+ E/ k9 Q( O( n  X
  9.      85     beq __error_p           @ yes, error 'p'- q8 J% V9 q, T) T# w! C2 R! Q
  10.      86     bl  __lookup_machine_type       @ r5=machinfo' c0 a8 C& t4 L; F; T! v& s
  11.      87     movs    r8, r5              @ invalid machine (r5=0)?7 T' I. P: S# A$ B8 y7 g' Y
  12.      88     beq __error_a           @ yes, error 'a'
    - B+ }' Q* e9 U7 B$ V
  13.      89     bl  __vet_atags) w6 b. {6 q# `. |8 ?
  14.      90     bl  __create_page_tables
複製代碼
既然找到了檔案,我們又可以開始繼續。
9 C6 S7 `( V- W. E7 D+ y* I3 ^; E' m* o! Z3 n$ s/ R
看了一下,程式碼多了很多bl的跳躍動作,看來會在各個function跳來跳去。
分享到:  QQ好友和群QQ好友和群 QQ空間QQ空間 騰訊微博騰訊微博 騰訊朋友騰訊朋友
收藏收藏 分享分享 頂 踩 分享分享
2#
 樓主| 發表於 2008-10-9 15:32:16 | 只看該作者
既然跳到真正的kernel開始跑,表示進入重頭戲,在進入kernel之前arm的平台有一些預設的狀況,也就是說arm kernel image會預設目前的cpu和系統的狀況是在某個狀態,這樣對一個剛要跑起來的OS比較決定目前要怎麼boot起來。
! c4 |$ l" _- P/ Z8 x
; u8 j6 p2 `: f; ~# H0 s! |. T可以看一下comment,有清楚的描述。這也幫助我們了解為什麼decompresser的程式跑完之後,還要把cache關掉。
  1.      59 /*  e: I" r, K, |5 p+ _6 c
  2.      60  * Kernel startup entry point.
    % K6 [* n' P: N
  3.      61  * ---------------------------
    3 ^$ h4 ?* |4 o) X7 V7 G" x
  4.      62  *
    % q, ^. D$ u; L% v) r
  5.      63  * This is normally called from the decompressor code.  The requirements2 x+ J  m9 V7 o
  6.      64  * are: MMU = off, D-cache = off, I-cache = dont care, r0 = 0,6 c* {% R* }6 ]* P0 y# z5 J8 j
  7.      65  * r1 = machine nr, r2 = atags pointer.
複製代碼
基於以上的假設,當program counter指到kernel的程式的時候,就會從line 80開始跑。
% B+ t7 W, S: o. M0 Oline 80, msr指令會把, operand的值搬到cpsr_c裡面。這是用來確保arm cpu目前是跑在svc mode, irq&fiq都disable。(設成0x1就會disable,definition在./include/asm-arm/ptrace.h)+ ?/ F  T! F. S& l3 F: ^
line 82, 讀取CPU ID到r92 [0 ^4 `2 d; k
line 83, 跳到 __lookup_processor_type 執行(bl會記住返回位址)。
  1.      77     .section ".text.head", "ax"; k* \" y% l- N7 `# e
  2.      78     .type   stext, %function$ N* ~+ Z# {% N4 C: N+ P
  3.      79 ENTRY(stext)
    0 h) G* d" E, V9 s
  4.      80     msr cpsr_c, #PSR_F_BIT | PSR_I_BIT | SVC_MODE @ ensure svc mode
    6 ^+ s# |! L) m* P, i3 w
  5.      81                         @ and irqs disabled; }8 E& l! i; i2 J
  6.      82     mrc p15, 0, r9, c0, c0      @ get processor id
    0 y7 j+ n  r  V5 Q. Y/ x& V0 c
  7.      83     bl  __lookup_processor_type     @ r5=procinfo r9=cpuid
複製代碼
接著會跳到head-common.S這個檔,9 `3 }2 a' B4 @5 I& _7 i
line 158, (3f表示forware往前找叫做『3』label)將label 3的address放到r3。
3 P5 L3 c/ F) T2 F) c; vline 159, 將r3指到的位址的資料,依序放到r7, r6, r5.(ldmda的da是要每次都位址減一次)
) {+ j2 X+ {$ r8 J4 zline l60, 161, 利用真實得到位址r3減去取得資料的位址得到一個offset值,這樣可計算出r5, r6真正應該要指到的地方。
! r! i  z* M8 Rline 163~169,這邊的程式應該不陌生,就是在各個CPU info裡面找尋對應的structure。找到的話就跳到line 171,返回head.S. _4 F$ c- ]) ]$ ]  Y; X
line 170, 找不到的話,r5的processor id就放0x0.表示unknown id。
+ L) C; @4 a% C( y& [1 ]
' t$ o7 e$ x! N6 x: `& K0 O__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! k0 k0 A5 q7 [1 c* Y  q
  2.     157 __lookup_processor_type:2 f1 ^% e. x3 H2 y- y: e; I
  3.     158     adr r3, 3f
    ! l! I5 A5 J! E0 v
  4.     159     ldmda   r3, {r5 - r7}1 N$ ~. o$ l" H# X
  5.     160     sub r3, r3, r7          @ get offset between virt&phys* y' o# I  C( f. R: o3 }9 g/ z  s
  6.     161     add r5, r5, r3          @ convert virt addresses to
    ( I5 c, `$ @7 @& ~6 c# u
  7.     162     add r6, r6, r3          @ physical address space1 ~! p( s: T3 P
  8.     163 1:  ldmia   r5, {r3, r4}            @ value, mask
    + O" m' X; X, m2 q. c3 w9 \
  9.     164     and r4, r4, r9          @ mask wanted bits6 H( s5 E5 N1 G
  10.     165     teq r3, r40 e6 t2 Q* y, Y+ v) f
  11.     166     beq 2f% e* I* d6 m9 \) u4 n
  12.     167     add r5, r5, #PROC_INFO_SZ       @ sizeof(proc_info_list)
    6 r+ |" D; c( F. b1 J7 t) y6 |( C1 n' H
  13.     168     cmp r5, r6+ h1 X& M, z( l2 U
  14.     169     blo 1b
    ! |+ x4 z4 o& W5 E) q4 B
  15.     170     mov r5, #0              @ unknown processor5 t1 G2 s. }- s" T% h  q/ z
  16.     171 2:  mov pc, lr: X$ o' e  V% X( @% }
  17. 7 o; g- z1 ^# M' c
  18.     187     .long   __proc_info_begin; p/ b% M# D0 Y* }5 F
  19.     188     .long   __proc_info_end8 H  v/ J# U% z0 z8 F9 H
  20.     189 3:  .long   .
    * F, H( ~9 t$ n( t
  21.     190     .long   __arch_info_begin
    . l4 I+ @6 W6 b- ~) L
  22.     191     .long   __arch_info_end
複製代碼
跳了很多檔案,建議指令和object code如何link的概念要有,就會很清楚了。
3#
 樓主| 發表於 2008-10-13 17:20:35 | 只看該作者
我們從 head-common.S返回之後,接著繼續看。( o0 `5 T. C. \

2 D- _* G* T/ q& ?1 o: O2 Dline 84, movs的意思是說,做mov的動作,並且將指令的S bit設起來,最後的結果也會update CPSR。這個指令執行的過程當中,會根據你要搬動的值去把N and Z flag設好。這個是有助於下個指令做check的動作。5 N; a1 c* p6 b" n. T: m4 o6 ], v
line 85, 就是r5 = 0的話,就跳到__error_p去執行。' b# H8 L( h/ l5 O; [  h
line 86, 跳到__lookup_machine_type。原理跟剛剛找proc的資料一樣。
  1.      83         bl      __lookup_processor_type         @ r5=procinfo r9=cpuid
    ) |! ?/ ]: @' y& P: Z. Z5 F
  2.      84         movs    r10, r5                         @ invalid processor (r5=0)?
    7 ]( }1 F! a7 P+ L; I# q3 [
  3.      85         beq     __error_p                       @ yes, error 'p'
    " z6 N7 B9 ]" b# o
  4.      86         bl      __lookup_machine_type           @ r5=machinfo
複製代碼
看得出來跟proc很像,有個兩個小地方是
( F7 z; h$ x% f5 Z3 w0 Q  U* c* t4 F( i3 _
1. line 207,用ldmia不是用ldmda。原因就在於存放arch 和 proc info 的位址剛好相反。一個在lable3的上面。一個在的下方。設計上應該是可以做修改的。
$ ?7 u) {7 N. u& A) ^$ P' O  H' c. n  h- O1 c( f; c  o9 r
2. arch定義的方式是透過macro,可以先看 ./include/asm-arm/mach/arch.h。裡頭有個 MACHINE_START ,這邊會設好macro,到時候直接使用就可以把arch的info宣告好。 例如 ./arch/arm/mach-omap1/board-generic.c
  1. /* macro */" Q) v7 H5 X* V. L& G
  2.      50 #define MACHINE_START(_type,_name)                      \
    2 ~7 g& x' ]" r8 `. p
  3.      51 static const struct machine_desc __mach_desc_##_type    \' r: ~  B+ l1 T& g! E1 ~
  4.      52  __used                                                 \
    % u* Y2 H/ W2 d2 z' y- L6 X0 R9 q5 q
  5.      53  __attribute__((__section__(".arch.info.init"))) = {    \
    ) S6 B' j5 g5 ~
  6.      54         .nr             = MACH_TYPE_##_type,            \* s' Q+ {- n$ ?' Q' k2 n4 J
  7.      55         .name           = _name,
    . S9 r7 K$ g2 V6 R2 R2 I5 v: f
  8.      56
    , Y/ K9 ?2 t8 ^0 Z7 N$ G3 _, p! {$ S
  9.      57 #define MACHINE_END                             \
    # S0 v" C" T! \0 j+ _
  10.      58 };6 p& {; Q; q% @; K; }
  11.      /* 用法 */# L( |+ e, {' S6 Q; Q& b
  12.      93 MACHINE_START(OMAP_GENERIC, "Generic OMAP1510/1610/1710")
    2 x; P% v+ P6 b9 r8 ^) _: Q9 c
  13.      94         /* Maintainer: Tony Lindgren <tony@atomide.com> */
    8 M9 `3 N5 J: \& \
  14.      95         .phys_io        = 0xfff00000," j% H4 g- x7 I$ X9 _1 M
  15.      96         .io_pg_offst    = ((0xfef00000) >> 18) & 0xfffc,
    , v/ s4 T4 D! v: `: [
  16.      97         .boot_params    = 0x10000100,9 O( D3 h1 [% x9 A( O
  17.      98         .map_io         = omap_generic_map_io,5 H  h6 w, ?) _7 N( M
  18.      99         .init_irq       = omap_generic_init_irq,& Y) V6 O' R- r* ^  u! R
  19.     100         .init_machine   = omap_generic_init,
    9 P( @) @6 U; ~( o
  20.     101         .timer          = &omap_timer,
    : Y  e1 z5 F! R. e- k6 [, _+ X8 X2 X
  21.     102 MACHINE_END
    : X0 O! C* Q% \# `) ~7 Q- Z) X
  22. $ R" Z& C6 G8 {
  23.     /* func */8 T4 |9 g) ]: W/ \3 m: O; I& {
  24.     204         .type   __lookup_machine_type, %function( T, }2 w/ L0 Z( B5 X! e
  25.     205 __lookup_machine_type:. _, ?8 B& |1 j
  26.     206         adr     r3, 3b7 o. U% R; U% B, A$ b) D
  27.     207         ldmia   r3, {r4, r5, r6}; _6 ]. R( ]- t; j2 ~
  28.     208         sub     r3, r3, r4                      @ get offset between virt&phys6 q8 {# z6 g: ^: q
  29.     209         add     r5, r5, r3                      @ convert virt addresses to% p( g2 Q8 N+ p% o! U
  30.     210         add     r6, r6, r3                      @ physical address space1 k  T/ p6 Q: C; t& G
  31.     211 1:      ldr     r3, [r5, #MACHINFO_TYPE]        @ get machine type
    ) V# S; K% t5 `  u' [6 t; Q6 s
  32.     212         teq     r3, r1                          @ matches loader number?
    : N4 O) A* M5 E) b1 J
  33.     213         beq     2f                              @ found
    % {& z- w! u0 n) ]* C; E
  34.     214         add     r5, r5, #SIZEOF_MACHINE_DESC    @ next machine_desc! x: s. Q5 U& ?9 I" ?) p
  35.     215         cmp     r5, r6
    * G9 Z8 E2 y+ P  S
  36.     216         blo     1b1 l6 _9 L8 x3 |0 D; J
  37.     217         mov     r5, #0                          @ unknown machine
    % o" \8 j+ X' P" h
  38.     218 2:      mov     pc, lr
複製代碼
4#
 樓主| 發表於 2008-10-13 17:56:46 | 只看該作者
接著我們又返回到head.S,
6 E; h6 s2 u6 M/ d7 wline 87~88也是做check動作。/ r0 a# e4 K2 A  B
line 89跳到vet_atags。在head-common.S
  1.      87         movs    r8, r5                          @ invalid machine (r5=0)?
    ' ^: p' G; e4 ?9 \* U* |
  2.      88         beq     __error_a                       @ yes, error 'a'
    ' R& n" f  p" P. F8 c
  3.      89         bl      __vet_atags
    . ?/ N) \" W& I( }  }
  4.      90         bl      __create_page_tables
複製代碼
line 245, tst會去做and動作。並且update flags。這邊的用意是判斷位址是不是aligned。4 i  f9 ]8 }) N  W
line 246, 沒有aligned跳到label 1,就返回了。* C% \6 N9 B- |' }4 j, }+ i
line 248~250, 讀取atags所在的address裡頭的值到r5,看看是否不等於ATAG_CORE_SIZE,不等於的話也是返回。4 {  p- i- F1 z9 {2 G0 ?
line 251~254, 判斷一下第一個達到的atag pointer是不是等於ATAG_CORE。如果正確的話,等一下要讀取atag的資料,才會正確。
0 H, D+ p% m- K0 K* e- c(atag是由bootloader帶給linux kernel的東西,用來告知booting所需要知道的參數。例如螢幕寬度,記憶體大小等等)
  1.      14 #define ATAG_CORE 0x544100014 Z8 U6 l+ G# a; H, @
  2.      15 #define ATAG_CORE_SIZE ((2*4 + 3*4) >> 2)
    - E1 D: P9 i7 d2 _+ M5 o

  3. # X# L# s2 A1 g$ p& ]
  4.     243         .type   __vet_atags, %function3 f& ~! J0 a8 w3 j
  5.     244 __vet_atags:' J3 h4 O: C3 O
  6.     245         tst     r2, #0x3                        @ aligned?
    : `; }9 K& t! w- s
  7.     246         bne     1f
    - V! |8 Z- N. m6 u
  8.     247
    . d/ `$ [8 T  m2 J6 b4 i$ w4 R
  9.     248         ldr     r5, [r2, #0]                    @ is first tag ATAG_CORE?  J7 e9 M0 z! n" |
  10.     249         subs    r5, r5, #ATAG_CORE_SIZE
      v/ [; k1 |4 u/ F/ k
  11.     250         bne     1f
    ; M4 H; Y( B% U9 z
  12.     251         ldr     r5, [r2, #4]: Y% n. {* F  l4 K) [
  13.     252         ldr     r6, =ATAG_CORE
    ) H3 ]+ M2 ?/ k+ s5 ]0 ~+ }  c5 ^
  14.     253         cmp     r5, r6& `! v& `, \3 U. b
  15.     254         bne     1f
    - n5 l$ t$ v4 _1 p4 y: B
  16.     255
    & x( ~3 j, I* _( J+ X! h& W2 i& I
  17.     256         mov     pc, lr                          @ atag pointer is ok
    + V4 R) f- d! Z2 A- x
  18.     257. F* \+ c; Z6 u- R- p
  19.     258 1:      mov     r2, #0
    / E+ A7 U# n# o2 c5 Y' V0 M, J
  20.     259         mov     pc, lr
複製代碼
接著我們又跳回去head.S。  
/ t% R# E' R. \+ P3 ?line 90,又跳到 __create_page_tables。   (很累人....應該會死不少腦細胞)* l/ G* s: q8 f; a- d3 Y2 G
哇!page table?!!如雷貫耳的東西,不知道會怎麼做。剛剛偷看了一下,@@還蠻長了,先到這邊好了,下次在繼續寫。
5#
 樓主| 發表於 2008-10-14 12:13:51 | 只看該作者
由於code看起來似乎越來越難解釋,會需要在檔案之間跳來跳去。建議一些基礎的東西可以再多複習幾次(其實是在說我自己 ),閱讀source code上收穫會比較多。例如:* A$ h' j- f# Z; l2 Q# ~
+ T4 @0 V/ _4 U6 g
1. arm instruction set - 這個最好每個指令的意思都大略看過一次,行有餘力多看幾個版本,armv4, v5 or v6。
( z- P; \8 n, w# p: Q8 V2. compiler & assembler & linker - toolchain工具做的事情和功能,大致的流程和功能要有概念。
, g% W" P' g0 p. w7 q3. Makefile & link script - 這兩個功能和撰寫的方式要有簡單的概念。
9 ]' u  u5 i# ^  n4 _
+ V  }  w4 b. N% u/ ^以上1是非常重要的重點,2&3只要有大致上的概念就可以,因為trace code的時候,有時需要跳到Makeflie&Link script去看最後object code編排的位址。0 V: A% H+ z6 L+ D  H
. @* \; k; R! ~% R7 I3 [8 b
由於我們trace到了page table這個關鍵字,在開始之前,稍微簡短的解釋,為了幫助了解,儘量用易懂的概念講,有些用詞可能會和一些真實狀況不同,但是懂了之後,應該會有能力分辨,至於正式而學術上解說,很多書上應該都有,或是google一下就很多啦∼
6#
 樓主| 發表於 2008-10-14 12:14:47 | 只看該作者
page table本身是很抽象的東西,尤其是對所謂的 user-mode application programmer 來說,大部分的狀況它是不需要被考慮到的部份,但是他的產生卻對os和driver帶來了很多影響。我們從一個小小的疑問出發。
/ U+ A" a- p4 u2 a: c0 w2 z' m* _+ Q4 H, `& U$ T
『產生page table到底是要給誰用的?』
$ B( _" b9 `7 g- E( O8 j; k
  m  C# ]6 y( x1 o* T其實真正作用在它上面的H/W是MMU,一旦CPU啟用了MMU,當cpu嘗試去記憶體讀取一個operand的時候,cpu內部打出去的位址訊號都會先送到mmu,mmu會拿著這個位址去對照page table,看看這個位址是不是被轉換到另外一個位置(還會確認讀寫權力),最後才會到真正的位址去讀寫。
8 k, P. r+ j' p7 S( U" ~& y6 i7 ~" i5 v/ e# l
這樣來看CPU其實一開始打出去的位址訊號其實不是最後可以拿到資料的位址,我們稱為virtual address(va),mmu打出來的位址才能真正拿到資料,所以我們把mmu打出去的位址稱為physical address(pa)。那用來查詢這個位址對照關係的表格,就是page table。) j7 G$ B( h, }

/ S7 {8 ^' E' u0 ~' f7 {4 w到這邊我們有一個簡單的概念。來想像一個小問題,一個普通的周邊(例如你的顯示卡),因為他並不具有MMU功能,hw只看得懂pa,但是CPU卻是作用在va,假如我想去對hw的控制暫存器做讀寫,到底要用va還是pa?
7#
 樓主| 發表於 2008-10-14 12:15:51 | 只看該作者
這時,寫driver的人就必須要小心,設定給硬體看的位址必須要先轉成pa(像是設定DMA),給CPU執行的程式碼要使用va,這對一開始嘗試寫driver但是又不瞭解va pa之間的差別的人,常常產生很多疑問。
& n' d$ w7 t5 u. H4 S8 F% k1 h3 {) |% f3 R5 K8 N! S; s9 \" z
現在我們回頭想想OS,既然我們跑到create page table,可以預期的是他想要將MMU打開,因此希望預先建立起一個page table,讓MMU知道目前os想規劃的位址對應和讀取權力是如何被安排的。所以os必須考慮到現在的系統究竟是長怎樣?應該要如何被安排?是不是有那些要被保護?好吧∼因為我們完全對os不了解,也不知道該安排什麼,只能祈禱在trace code完後,可以找到這些問題的答案,或者發現一些沒想到的問題。
! Y3 g$ _2 t/ G! M
* i2 M+ X' b. q  C知道了page table的大致上的功能,下篇就可以專心的研究這個table的長相,和它想規劃出的系統模樣。2 h: g& q) Z/ s& a
! ^3 X, G( x: M& q  A
p.s. 字數限制好像變短了。   (看來很難寫)
8#
 樓主| 發表於 2008-10-14 13:56:17 | 只看該作者
由於字數限制挑整,用詞儘量精簡,以便可以貼必要source。另外,以後寫到一段落,考慮收集成一篇完整的文章,這樣應就不會因為用回覆的方式,造成閱讀斷斷續續的問題。希望有人對文章呈現方式有想法的話,可以跟我講。
9#
 樓主| 發表於 2008-10-14 13:57:39 | 只看該作者
現在,讓我們跳入create_page_tables吧∼5 w- @  D* ~& k" w" E* a! Y
line 216,會跳到pgtbl的macro去執行,其實只是載入一個位址。. {0 `# {( c1 G

% V8 D( @" T9 ?) E2 R只是這個位址因為你硬體規劃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 */
    ( b- i. {$ w$ o+ o6 z
  2.      95 textofs-y       := 0x00008000  z8 C2 K7 `% j3 p/ b1 P5 s
  3.     152 TEXT_OFFSET := $(textofs-y)
複製代碼
  1. /* include/asm-arm/arch-omap/memory.h */
    # K: |0 c; A$ |: k! C
  2.      40 #define PHYS_OFFSET             UL(0x10000000)% J5 S0 a0 g$ T/ E4 d0 s! u" d( f9 b
  3. , r, j4 N; g9 k
  4.      /* arch/arm/kernel/head.S */9 A' F9 h7 k7 b8 V7 a
  5.      29 #define KERNEL_RAM_VADDR        (PAGE_OFFSET + TEXT_OFFSET): ?* w  j; m& x2 a
  6.      30 #define KERNEL_RAM_PADDR        (PHYS_OFFSET + TEXT_OFFSET)0 q7 ~* C/ |1 B0 i" `/ K
  7. " |& r* E" E9 w' b7 d
  8.      47         .macro  pgtbl, rd7 j1 u7 o& x) n' _3 k; f1 p
  9.      48         ldr     \rd, =(KERNEL_RAM_PADDR - 0x4000)! l4 a8 I5 q/ t
  10.      49         .endm6 f# P/ x# u& S* }/ |
  11. % w. f% w: e: n, R$ z- n
  12.     216         pgtbl   r4                              @ page table address
複製代碼
10#
 樓主| 發表於 2008-10-14 14:16:53 | 只看該作者
得到pg table的開始位置之後,當然就是初始化囉。6 L6 E% n0 R% ~; y8 T  |
line 221, 將pg table的base addr放到r0.& H) }4 v7 `; B& C7 t
line 223, 將pg table的end addr放到r6.
# V  A7 }; b7 C1 [( B- ?line 224~228, 反覆地將0x0寫到pg table的區段裡頭,每個loop寫16bytes. (4x4),直到碰到end,結果就是把它全部clear成0x0.
  1.     221         mov     r0, r4* E& _2 G  h6 \1 M
  2.     222         mov     r3, #0
    ! Z: ]. `4 b9 \: T
  3.     223         add     r6, r0, #0x4000
    6 D% W1 L' |5 x, M. ?
  4.     224 1:      str     r3, [r0], #4
    1 E& l% R9 k# d
  5.     225         str     r3, [r0], #4
    4 }* ?& \" w& o- E' o. Y0 d2 z2 X
  6.     226         str     r3, [r0], #48 p% t" k9 n' ]
  7.     227         str     r3, [r0], #4: `( L6 A  y6 l; r6 n4 o6 R
  8.     228         teq     r0, r6
    % V$ C+ U, W4 a3 Q1 q' m- M
  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 | 只看該作者
問題怎麼填值??& B7 i" m8 V- @/ [& J$ ]7 y
拿出ARM的手冊,翻到MMU章節。一看發現page有很多種,還得分first level和second level。
; `4 Z8 M8 |% i
# H  ^5 H) u7 I- j0 w- w念書時的印象,是從1st level在去查2nd level,先看怎麼設定1st level (不知以前老師有沒有亂教)
" y5 o8 t4 |' y2 D- w1. [31:20]存著section base addr
  V8 U6 [1 X/ Y2. [19:2]存著mmu flags# ?) x  c$ ^6 O" Q/ |! |$ G2 H) e
3. [1:0]用來辨別這是存放哪種page, 有四種:
& M* B9 i# N1 D" Z   a. fault (00) b. coarse page (01) c. section (1st level) (10) d. fine page (11); R5 Q  c- s( a* |+ a1 y
4. pg tabel資料要存放到 [31:14] = translation base, [13:2] = table index , [1:0] = 0b00 的位址
3 y6 Y+ a- b' E8 o- Y/ ]# d7 V6 X) s" V- b7 Z. L) [7 Y
來看code是怎麼設定。; ^) C0 r# B3 n' h' O$ M! S7 a$ W- _

1 d# _  R6 ]) P5 u( e5 e2 uline 239, 將pc的值往右shift 20次放到r6.這是取得前20高位元的資料,有點像是 1.。
  k" P1 j2 _. _7 H( t: Y5 s% l2 Lline 240, 將r6往左shift 20次之後,和r7做or的動作,剛好也是 2.提到的mmu flags和1.處理好的資料做整理。6 ?4 v5 ~) S! r( N( y/ Z
所以前面兩個做完,就完成了bit[31:2]。6 K: o$ j- D: x! x: |- F9 t
line 241, 將r3的資料寫到r4+(r6<<0x2)的地方去,剛好是4.提到的位址。(lucky)
  1.     239         mov     r6, pc, lsr #20  @* @; b( z  V5 L+ E8 b9 b& b
  2.     240         orr     r3, r7, r6, lsl #20( j) P3 W) |3 H# @( B+ N' T0 l
  3.     241         str     r3, [r4, r6, lsl #2]
複製代碼
p.s. 用pc值剛好可以算出當前的page。
12#
 樓主| 發表於 2008-10-22 19:47:03 | 只看該作者
最近又被釘上了,開始忙碌,大概沒太多時間貼文∼( `6 l: v* N# M
2 l* z9 C7 Q& @  F( m0 I+ v( n# \
上篇已經將pc所屬的page table entry(pte)設定好,接著繼續看
+ E  h. h5 F! @) K- T6 lline 247, 248, 將KERNEL_START的位址往右shift 18算出pte的offset,(等於line239&241,239&241是先shift right 20然後shift left 2),並將剛剛r3的值設給pte。『!』的意思是會把address存到r0。7 P/ p3 K9 j, i1 X: A

; ?) b2 W1 f/ {* aline 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
    ; u; V# r7 R9 v- q  {
  2.     248         str     r3, [r0, #(KERNEL_START & 0x00f00000) >> 18]!
    9 d" r5 a" N4 p& a7 w
  3.     249         ldr     r6, =(KERNEL_END - 1)
    + M* {* }3 R2 k& @0 ~7 H
  4.     250         add     r0, r0, #4# a! G+ u) v! _
  5.     251         add     r6, r4, r6, lsr #18
    * o1 A/ d. a3 b. ~1 b9 x9 Y% g0 J1 F% p
  6.     252 1:      cmp     r0, r6* d) }" o* p5 a
  7.     253         add     r3, r3, #1 << 20
    6 j" R; m! \& N6 E
  8.     254         strls   r3, [r0], #44 M/ X7 A( z+ G4 x6 }0 n$ F
  9.     255         bls     1b
複製代碼
13#
 樓主| 發表於 2008-10-22 20:24:58 | 只看該作者
line279,PAGE_OFFSET是規範RAM mapping完後的virtual address,我們將這個位址所屬的pte算出來放到r0。) T. Y9 ?2 q" I% _# V4 E
line 280~283,將要 map 的physical address的方式算出來放到r6。9 ]0 f! I) [+ }+ T( p& U( p: \+ h
line 284,最後將結果存到r0所指到的pte。1 I, h4 Y5 I7 E+ ]- X2 Q

: S5 _6 y3 U% h4 X' D: S以上三個動作,就是做好 ram 起頭的位址一開始map,還沒做完整塊map。由於我們目前的設定方式每塊是1MB,所以這邊是將RAM開始的1MB做map。* {, y2 i- B# f! Q/ L7 S

1 M; h1 g  w* q8 G) n1 d# r6 gline 327,返回,結束一開始的create page table的動作,我們可以看出其實並沒有做完整的初始化page table,所以之後應該會有其他page table的細節。
  1.     279         add     r0, r4, #PAGE_OFFSET >> 18
    . ^, o! h" X" n7 p2 H& V2 H
  2.     280         orr     r6, r7, #(PHYS_OFFSET & 0xff000000)
    5 F' _  l5 b0 `/ Z' v7 V
  3.     281         .if     (PHYS_OFFSET & 0x00f00000)
    - O# a  G2 q5 `/ \* w* N% q% y
  4.     282         orr     r6, r6, #(PHYS_OFFSET & 0x00f00000)
    ! s1 h7 N% m# F# C
  5.     283         .endif
    0 I; p; w- q5 n# `* j) u% d
  6.     284         str     r6, [r0]
    7 r' r& E/ X" n- [4 J6 ^8 i; ]
  7.     327         mov     pc, lr
複製代碼
附帶一提,我們這邊省略了一些用ifdef包起來的程式碼,像是一開始會印一些output message的程式碼(line286~326)。
14#
 樓主| 發表於 2008-10-22 20:37:08 | 只看該作者
自create page table返回後,我們偷偷看一下接下來的程式碼,! z8 D( j# [) {$ g7 R  N2 g
line 99, 將switch_data擺到r139 ^) {: `: B9 {6 e; M* o6 M& G
line 101, 將enable_mmu擺到lr
5 _* h* h5 s* q9 L$ i, L1 wline 102, 將pc改跳到r10+PROCINFO_INITFUNC的地方去
: K+ K- @$ q! w6 H
5 L5 T$ o3 I. _! T其實這邊有點玄機,switch_data和enable_mmu都是function,結果位址都只是被存起來,並沒有直接跳過去執行。其實,雖然程式碼是順序switch_data->enable_mmu->proc init function,但其實執行的順序會是 procinfo 的init function-> enable_mmu -> switch_data 。至於,為什麼要這樣寫的原因?就不清楚了,也還沒仔細推敲過。 ( Z5 F& B3 l3 L# ^: W

; i# }/ P' Z( L# ~4 o/ kswitch_data最後就會跳到大家都很熟悉的start_kernel(). 詳細要賣個關子,最近會忙一下,得要過一陣子才能貼文∼  
  1.      99         ldr     r13, __switch_data              @ address to jump to after3 w1 g% `  o9 w0 v& S' G  `
  2.     100                                                 @ mmu has been enabled
    7 c2 y0 `* @* v$ h2 u( i- V
  3.     101         adr     lr, __enable_mmu                @ return (PIC) address5 h6 p) E, J& ~3 O4 {2 J* `3 A, }
  4.     102         add     pc, r10, #PROCINFO_INITFUNC
複製代碼
15#
 樓主| 發表於 2009-7-4 01:09:36 | 只看該作者
老店重新開張~8 i  }- m# x! y# s. W1 n" P, I# b

5 v6 M  l) V& F  o( T- L' c) y花了一些時間把舊的貼文整理到一個blog1 T1 j0 ~  |- y; d) P# y. T2 P
有把一些敘述修改過
- k! d! j/ D% H  ?9 U7 l8 B希望會比較容易集中閱讀
( z& E' B" _- Z: x* G目前因為某些敘述不容易
% ^1 C3 }4 L. I還是比較偏向筆記式而且用字不夠精確
' K6 ?5 q+ R7 k$ b希望之後能夠慢慢有系統地整理
: H) `& S3 f/ N# h% N大家有興趣的話
: P1 _/ i1 K4 s5 h可以來看看和討論
9 J: N& r; b: U; W- D( M( a! zhttp://gogojesseco.blogspot.com/
: _3 S& j( f7 z$ E. }( i
# Z6 k# h/ G& Y以後可能會採取  先在chip123貼新文章
6 M; C& ^' J0 @/ u) l慢慢整理到blog上的方式; n+ J; c( T3 }/ y) j$ w) p" H
因為chip123比較方便討論 =)! g8 \- U" o' A
blog編輯修改起來比較方便% n4 h: Q" A+ u' f; Y- u( t# D
閱讀也比較集中   大家可以在這邊看到討論
$ F. x. ?& ~+ w$ ~) z3 k1 O然後在blog看到完整的文章 (類似BBS精華區的感覺)

評分

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

查看全部評分

16#
 樓主| 發表於 2009-7-15 17:07:03 | 只看該作者
隔了很長一段時間沒update6 G, q" D9 P3 a4 Y
之前程式碼走到 ./arch/arm/kernel/head.S 的 line 99 附近
  1.      99         ldr     r13, __switch_data              @ address to jump to after
    . R! F% J" U: p
  2.     100                                                 @ mmu has been enabled! c/ s9 c' H# j
  3.     101         adr     lr, __enable_mmu                @ return (PIC) address
    1 ~$ j6 ^/ V4 N  l0 ~$ P* o
  4.     102         add     pc, r10, #PROCINFO_INITFUNC
複製代碼
line 99, 將__switch_data放到r13。(留作之後用)
4 a4 p& g; T1 |7 i9 b" g# C1 ^7 gline 101, 將__enable_mmu的addr放到lr。(留作之後用)" I2 ^' i0 o: _; O4 r
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. `' z* P  s' V/ _' `
  2. 374 __arm926_setup:
    & K4 p7 P' Z$ Z  E/ O5 n8 P
  3. 375         mov     r0, #09 K+ U& e) E% J  R. V5 S
  4. 376         mcr     p15, 0, r0, c7, c7              @ invalidate I,D caches on v4
    , T$ ^2 g7 w) S7 ?
  5. 377         mcr     p15, 0, r0, c7, c10, 4          @ drain write buffer on v4
    - U4 [- y7 \) e1 z$ w5 S: N$ U. l
  6. 378 #ifdef CONFIG_MMU% @+ X( e0 }2 z5 a
  7. 379         mcr     p15, 0, r0, c8, c7              @ invalidate I,D TLBs on v48 O; q( B/ O2 m$ T4 }, Z( G' S6 m0 R
  8. 380 #endif
    0 E" v4 E0 W5 U# R
  9. 9 L4 a* J. ]3 L. R2 W$ `' @
  10. 388         adr     r5, arm926_crval
    8 l% [* R+ T- ^' _; l
  11. 389         ldmia   r5, {r5, r6}
    1 k+ h0 H. J# E4 b3 G
  12. 390         mrc     p15, 0, r0, c1, c0              @ get control register v4
    2 J3 w9 c  P% s( ^% [) U
  13. 391         bic     r0, r0, r5
    / I  U; ~% N7 R; m$ N
  14. 392         orr     r0, r0, r66 z! I; b/ p" e5 }/ L
  15. 8 ^& j/ N* |- B* F2 Q+ K: N
  16. 396         mov     pc, lr
    4 x8 u1 `2 [* d0 |$ F
  17. 397         .size   __arm926_setup, . - __arm926_setup
複製代碼
這邊的程式碼就跟CPU有很大的相依性,
5 \3 M5 D$ K8 \1 B$ lline 375~380, 主要就是invalidate CPU的I&D cache和清空write buffer。6 G. Y, o9 ~3 Q9 K9 R( |+ R
line 388~392, 把cp15的設定讀出來,並且將一些預設狀態做好運算放到r0。(預設值從arm926_crval這邊可以取得。)
4 {* i3 M& T& q5 Qline 396, 直接把pc跳到lr,因為我們之前已經將lr = enable_mmu,所以直接跳過去。
17#
 樓主| 發表於 2009-7-15 17:29:45 | 只看該作者
  1. 155 __enable_mmu:
    ) `, S5 j& ^2 B+ p/ Q
  2. 170         mov     r5, #(domain_val(DOMAIN_USER, DOMAIN_MANAGER) | \" H7 @4 x! C' K- U8 u0 l) A8 d
  3. 171                       domain_val(DOMAIN_KERNEL, DOMAIN_MANAGER) | \9 U3 a6 F6 n4 J+ l' m9 m# N% s& T
  4. 172                       domain_val(DOMAIN_TABLE, DOMAIN_MANAGER) | \8 a9 U# m" d2 [( z1 G/ E" r
  5. 173                       domain_val(DOMAIN_IO, DOMAIN_CLIENT))* A& z. j% J* B+ s2 i  S* h# r
  6. 174         mcr     p15, 0, r5, c3, c0, 0           @ load domain access register& G  a" D0 U# P5 Z3 W! g1 y5 a
  7. 175         mcr     p15, 0, r4, c2, c0, 0           @ load page table pointer9 t9 S' C: E/ I$ L9 Z
  8. 176         b       __turn_mmu_on6 `& c# \8 w' a4 c% h
  9. 177 ENDPROC(__enable_mmu)
複製代碼
line 170~174,設置好domain access。(domain access可以先當成設置access的權限,或許以後可以寫詳細的文章說明)- `1 h( D/ f: {  m" k# ~
line 175~176,將create好的page table開頭丟給mmu。跳到turn_mmu_on
  1. 191 __turn_mmu_on:- O' ?1 k7 L" k" q
  2. 192         mov     r0, r0
    , `6 ?  P( N- p: A
  3. 193         mcr     p15, 0, r0, c1, c0, 0           @ write control reg
    & v6 i: c% j1 D2 v7 c, U  S: y
  4. 194         mrc     p15, 0, r3, c0, c0, 0           @ read id reg
    8 }/ \2 o2 O3 s1 l! d
  5. 195         mov     r3, r3
    : j8 _  t- P/ Q; P
  6. 196         mov     r3, r3
    ; U8 ^* o" u1 C3 W1 Q
  7. 197         mov     pc, r13
    * I3 d" O- K  `8 I3 i! \
  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:
    0 }% [  g0 g: A, V
  2. 19         .long   __mmap_switched5 ~8 n3 W" O3 \. b4 U
  3. 20         .long   __data_loc                      @ r4
    $ ?/ H6 ?* d7 ?
  4. 21         .long   _data                           @ r5
    : ?; E6 H" w! y5 b
  5. 22         .long   __bss_start                     @ r6
    : w2 e; b& o4 |4 L8 Y
  6. 23         .long   _end                            @ r7
    3 w- p3 [' J+ T& |9 I! @" \
  7. 24         .long   processor_id                    @ r4" K& Q! L* d9 V0 f; J3 r
  8. 25         .long   __machine_arch_type             @ r5
    ) z8 O% C2 h7 \
  9. 26         .long   __atags_pointer                 @ r6
    " Y; F8 O' ~) C# ~
  10. 27         .long   cr_alignment                    @ r7
    0 w  Q1 s( [9 F; H
  11. 28         .long   init_thread_union + THREAD_START_SP @ sp. x' J9 Z' p1 Z" f
  12. 29* L% q+ a  a0 Y6 s# J' z% n; P
複製代碼
switch_data的第一行就是 __mmap_switched,所以我們直接看line 39。
18#
 樓主| 發表於 2009-7-15 17:30:00 | 只看該作者
  1. 39 __mmap_switched:
    , B; B5 Y7 D7 Y" O; l/ A
  2. 40         adr     r3, __switch_data + 4" w. _% C4 ?3 R4 `
  3. 41
    + V6 ^" \3 H1 r2 D, ?, s
  4. 42         ldmia   r3!, {r4, r5, r6, r7}
    ; _6 _; T1 E+ g7 M; y
  5. 43         cmp     r4, r5                          @ Copy data segment if needed0 Y' A2 q# j( d6 B/ P1 \3 F- z
  6. 44 1:      cmpne   r5, r6( Y. T$ s  {+ s- @& B& k
  7. 45         ldrne   fp, [r4], #43 ^; q/ y, u$ t5 }0 o8 s
  8. 46         strne   fp, [r5], #4
    . V) W* C4 f0 C) B# Q8 V3 E3 [
  9. 47         bne     1b" y: v  e* |6 C+ _, S
  10. 48
      J9 @% b3 Q7 p5 G" E. b
  11. 49         mov     fp, #0                          @ Clear BSS (and zero fp)
    . U5 ?7 f, h6 |' [7 S  W2 ~, a
  12. 50 1:      cmp     r6, r7
    - p' z" T9 a# I0 w7 g# A* d# S2 `
  13. 51         strcc   fp, [r6],#4
    * \) l" Q' L. I4 o" r( |; j$ C# m
  14. 52         bcc     1b" n- Z# ^- y4 }+ ]; m
  15. 53
    4 t2 J+ J. k( p% Y
  16. 54         ldmia   r3, {r4, r5, r6, r7, sp}& p0 w' j. K% @# K" t  z
  17. 55         str     r9, [r4]                        @ Save processor ID
    + i7 A4 r% `, u0 r# W- O
  18. 56         str     r1, [r5]                        @ Save machine type
    ' N" [- ^6 @: U+ y
  19. 57         str     r2, [r6]                        @ Save atags pointer
    8 M9 ?  R- k* E* T% X
  20. 58         bic     r4, r0, #CR_A                   @ Clear 'A' bit
    5 J! S5 e8 ^: [0 D! [% }- B% d* A
  21. 59         stmia   r7, {r0, r4}                    @ Save control register values3 |9 u' y) q! b7 y
  22. 60         b       start_kernel/ u& |5 L/ |* I5 m- y; g) S+ r) s
  23. 61 ENDPROC(__mmap_switched)
複製代碼
switch_data的第一行就是 __mmap_switched,所以我們直接看line 39。8 c+ f6 a2 H, ^/ P
line 39,將__data_loc的addr放到r3
5 n* @/ D- R! z! r' g( Lline 42,從r3的位址,連續讀取四筆資料到r4, r5, r6, r7
- Y& {" a- x2 g3 |; K* n* qline 43~47,看看data segment是不是需要搬動。& q) {) D6 r) M, [8 E
line 49~52, clear BSS。
, b9 t* B8 z! e" Y7 P: m' V: w' i  v+ m
由於linux kernel在進入start_kernel前有一些前提必須要滿足:. ~/ t8 A3 y# V- Y8 _  o  N- Q/ w
r0  = cp#15 control register: O% J; T3 n3 S' ]# b
r1  = machine ID! }% I. N/ V$ J$ }
r2  = atags pointer5 ?( f  S+ z5 p! U4 {
r9  = processor ID5 O. e: A6 _; N. w3 E$ n0 E+ j

  c$ J- c8 Q) S. o  q5 p所以line 54~59就是在做這些準備。
+ j$ |( {  }4 {3 `& R最後呢? 我們就跳到start_kernel了。(而且還是用b start_kernel,表示我們不會再回來了)
- p+ v1 U' f* g5 [" M, N, C
9 |( y# z# j. h- t& C* C看一下start_kernel()在./init/main.c,終於跳出architecture specific的目錄,表示
6 C- |! M( M! K' i. u我們真正的開始linux kernel的初始化。$ Z: G2 [, \) h6 S. R% z
像是 shedule init, console init, memory init, irq init等等都在start_kernel裡頭。
4 P. P0 O% Q. ~: ]到這邊之後,應該就可以深入linux kernel中,各項比較跟hardware不那麼相關的軟體部分。

評分

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

查看全部評分

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

本版積分規則

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

GMT+8, 2024-6-13 05:01 PM , Processed in 0.152520 second(s), 22 queries .

Powered by Discuz! X3.2

© 2001-2013 Comsenz Inc.

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