Chip123 科技應用創新平台

 找回密碼
 申請會員

QQ登錄

只需一步,快速開始

Login

用FB帳號登入

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

trace linux kernel source - ARM - 03

[複製鏈接]
跳轉到指定樓層
1#
發表於 2008-10-7 14:00:18 | 只看該作者 回帖獎勵 |倒序瀏覽 |閱讀模式
到目前為止,我們已經進展到kernel幫自己relocate完,並解將自己解壓縮到一個地方要準備開始執行,那疑問來了?到底是跳到哪裡去了,因為./compressed/head.S最後一行居然是5 z) i  Z; X7 z: K2 M5 E: t! U7 o
『mov pc, r4』
& I! ^! n" m4 ^  Fr4只代表了解壓縮完後kernel的位址,那究竟整包kernel編譯的時候,哪個function哪個東西被放在最前面咧?!
* M2 _4 s! L5 Q0 X
+ s+ K* C% E" }& I7 o+ c所以我們又必須開始找於kernel link的時候是怎麼被安排的,有了前面的基礎,我們可以從Makefile知道程式碼如何被編譯。至於link上的細節,例如有那些section和section先後順序等等,可以從 lds 檔來規範。7 H' s. Z$ s, l5 c( {: U# w

9 ]2 H4 m; Y0 Q) z8 G4 e有興趣的人可以看一下 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)
: v4 W, _/ }$ U0 i! q我們可以發現第一個section是『.text.head』,裡頭的_stext從目前的位置開始放。. O: D! }- D+ T
於是我們曉得只要找到屬於.text.head這個section,並且是_stext這個symbol的程式碼,就是解壓縮完後的第一行程式碼。
  1.      26     .text.head : {1 e( R2 w( U' D. K/ ?" l' j6 o7 w$ J. U
  2.      27         _stext = .;
    4 M  r0 @" m' n% Q& j" i
  3.      28         _sinittext = .;$ K$ s& C2 Y% M- [7 T6 _
  4.      29         *(.text.head)
    & t; O3 u! O# J( o# L
  5.      30     }
複製代碼
用指令搜尋一下,發現 ./arch/arm/kernel/head.S 裡頭有關鍵字(如下),結果我們從./arch/arm/boot/compressed/head.S跳到了./arch/arm/kernel/head.S
  1.      77     .section ".text.head", "ax"
    - l  u: f0 G# M. J, d  \, q
  2.      78     .type   stext, %function
    " P5 G7 L3 l6 m: Z' e+ x
  3.      79 ENTRY(stext)3 D8 }; m6 N8 h$ S
  4.      80     msr cpsr_c, #PSR_F_BIT | PSR_I_BIT | SVC_MODE @ ensure svc mode
    8 T7 _9 _9 O" g! a1 Z
  5.      81                         @ and irqs disabled% P+ D' [, C, f$ p. Q& Q
  6.      82     mrc p15, 0, r9, c0, c0      @ get processor id
    9 Q# V  v$ N+ B4 E$ E* L
  7.      83     bl  __lookup_processor_type     @ r5=procinfo r9=cpuid; J* t7 W3 J$ ]- G1 ]' O' K; }) V% R
  8.      84     movs    r10, r5             @ invalid processor (r5=0)?
    / ~2 @) ~% B  y6 I& H0 U+ A$ j, c
  9.      85     beq __error_p           @ yes, error 'p'
    6 [* j) I4 }8 H+ v, J
  10.      86     bl  __lookup_machine_type       @ r5=machinfo7 h# v1 ]! R4 X+ D! Z  S, j
  11.      87     movs    r8, r5              @ invalid machine (r5=0)?& i- g8 O( c2 B& I' c
  12.      88     beq __error_a           @ yes, error 'a'( Q8 |% [7 E1 ^! y1 f) e$ _# r
  13.      89     bl  __vet_atags. G/ L% S6 Q+ ^1 L# |
  14.      90     bl  __create_page_tables
複製代碼
既然找到了檔案,我們又可以開始繼續。
! V" X7 g% L" N. @0 l9 T$ q4 M
- A1 L2 R- X. g# {$ B看了一下,程式碼多了很多bl的跳躍動作,看來會在各個function跳來跳去。
分享到:  QQ好友和群QQ好友和群 QQ空間QQ空間 騰訊微博騰訊微博 騰訊朋友騰訊朋友
收藏收藏 分享分享 頂 踩 分享分享
2#
 樓主| 發表於 2008-10-9 15:32:16 | 只看該作者
既然跳到真正的kernel開始跑,表示進入重頭戲,在進入kernel之前arm的平台有一些預設的狀況,也就是說arm kernel image會預設目前的cpu和系統的狀況是在某個狀態,這樣對一個剛要跑起來的OS比較決定目前要怎麼boot起來。
8 h$ I  N& o5 C- E: k; i& X9 i4 U1 j' R& s% d
可以看一下comment,有清楚的描述。這也幫助我們了解為什麼decompresser的程式跑完之後,還要把cache關掉。
  1.      59 /*2 I  C! F! Z# R" l$ H- ~! v
  2.      60  * Kernel startup entry point.
    % b& i0 R: Q, [
  3.      61  * ---------------------------1 X7 O0 X$ E6 t* W# }! P
  4.      62  *
    ; ^9 B# o  p+ h0 P! b) x. d
  5.      63  * This is normally called from the decompressor code.  The requirements5 ]  W3 ~: D6 y
  6.      64  * are: MMU = off, D-cache = off, I-cache = dont care, r0 = 0,1 @5 ~- C: C5 t
  7.      65  * r1 = machine nr, r2 = atags pointer.
複製代碼
基於以上的假設,當program counter指到kernel的程式的時候,就會從line 80開始跑。
. y. o0 T5 h. r2 c9 |, w) Yline 80, msr指令會把, operand的值搬到cpsr_c裡面。這是用來確保arm cpu目前是跑在svc mode, irq&fiq都disable。(設成0x1就會disable,definition在./include/asm-arm/ptrace.h)1 V( E  ^' V+ i% N
line 82, 讀取CPU ID到r9* C4 H6 g6 V4 ?8 e: g2 O
line 83, 跳到 __lookup_processor_type 執行(bl會記住返回位址)。
  1.      77     .section ".text.head", "ax"/ K+ J/ x) H7 a; Z- {' v
  2.      78     .type   stext, %function
    ! |% Y' H  i- A2 _$ w
  3.      79 ENTRY(stext)
    . w  N$ \8 r2 \1 Q
  4.      80     msr cpsr_c, #PSR_F_BIT | PSR_I_BIT | SVC_MODE @ ensure svc mode
    ) z! U3 O) _# V4 @& }" J0 d+ Q% q
  5.      81                         @ and irqs disabled
    * g: h- O4 {9 _2 q0 R7 l4 L
  6.      82     mrc p15, 0, r9, c0, c0      @ get processor id8 E1 O8 P& p% t9 Q2 m8 r& s
  7.      83     bl  __lookup_processor_type     @ r5=procinfo r9=cpuid
複製代碼
接著會跳到head-common.S這個檔,
" W6 N& A8 C( E9 x8 l( {line 158, (3f表示forware往前找叫做『3』label)將label 3的address放到r3。
* i1 L" J8 I, L* T/ ~line 159, 將r3指到的位址的資料,依序放到r7, r6, r5.(ldmda的da是要每次都位址減一次)
/ S1 g5 P0 i) lline l60, 161, 利用真實得到位址r3減去取得資料的位址得到一個offset值,這樣可計算出r5, r6真正應該要指到的地方。( \: e( Z( h4 V
line 163~169,這邊的程式應該不陌生,就是在各個CPU info裡面找尋對應的structure。找到的話就跳到line 171,返回head.S- ~- Q# E$ Q$ W6 ?4 q& ~
line 170, 找不到的話,r5的processor id就放0x0.表示unknown id。5 k$ `2 y; b$ |- R+ l1 X  [9 A
6 W/ ?  p6 V7 V8 j
__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, %function5 \1 Z9 m3 ~6 c3 P' z
  2.     157 __lookup_processor_type:
    + {4 V* {+ Y: `2 x4 W
  3.     158     adr r3, 3f2 B# k! J/ z. u- o# B5 B( J$ h  ^
  4.     159     ldmda   r3, {r5 - r7}+ R3 R8 ^2 p9 a# ?8 ~3 s9 y$ a2 x! I
  5.     160     sub r3, r3, r7          @ get offset between virt&phys' J8 R9 c/ o1 n0 t7 l) V$ [
  6.     161     add r5, r5, r3          @ convert virt addresses to
    9 ]8 o6 @' i4 F3 {0 ]: w
  7.     162     add r6, r6, r3          @ physical address space
    * ~$ M! h( R! }+ c) f
  8.     163 1:  ldmia   r5, {r3, r4}            @ value, mask4 {8 p  |6 z% ^
  9.     164     and r4, r4, r9          @ mask wanted bits% ~" |) l: ~, _
  10.     165     teq r3, r47 N) E! Q- u6 V4 x# ~' O8 g( P# G
  11.     166     beq 2f6 J5 N+ X' d# |8 J; u. v  b
  12.     167     add r5, r5, #PROC_INFO_SZ       @ sizeof(proc_info_list)
    ' h' E$ O; D( ~5 g
  13.     168     cmp r5, r6( A$ ]1 ?9 M+ @- `
  14.     169     blo 1b/ j  v3 }* L7 g5 ]/ X
  15.     170     mov r5, #0              @ unknown processor/ `7 @) ]) Q  r! `
  16.     171 2:  mov pc, lr
    5 S5 J2 T) T( Q+ g% q

  17. * u1 v% x! l/ A$ ]9 Q# x
  18.     187     .long   __proc_info_begin
    $ X/ m8 u$ K, K' m% g. V5 a
  19.     188     .long   __proc_info_end
    9 s" O" f. y5 ~1 N  r, J
  20.     189 3:  .long   .: U% j6 I6 K* P$ {$ j
  21.     190     .long   __arch_info_begin
    & Q8 {* u6 W2 V5 M4 f/ H$ x8 W
  22.     191     .long   __arch_info_end
複製代碼
跳了很多檔案,建議指令和object code如何link的概念要有,就會很清楚了。
3#
 樓主| 發表於 2008-10-13 17:20:35 | 只看該作者
我們從 head-common.S返回之後,接著繼續看。9 _$ t' _7 V* i% l& c: T
  w: r$ [1 `# W. ?) o/ L8 X5 b
line 84, movs的意思是說,做mov的動作,並且將指令的S bit設起來,最後的結果也會update CPSR。這個指令執行的過程當中,會根據你要搬動的值去把N and Z flag設好。這個是有助於下個指令做check的動作。
, [% w6 u6 j" c+ ^line 85, 就是r5 = 0的話,就跳到__error_p去執行。
5 Y$ f2 D% W5 R  k' m" X6 wline 86, 跳到__lookup_machine_type。原理跟剛剛找proc的資料一樣。
  1.      83         bl      __lookup_processor_type         @ r5=procinfo r9=cpuid
    2 P9 T  u+ a* S4 b; `
  2.      84         movs    r10, r5                         @ invalid processor (r5=0)?, ?" E1 ~, g4 G& V! q0 ^+ x" p8 l
  3.      85         beq     __error_p                       @ yes, error 'p'
    : m! ~1 @: k) Y. p8 v2 l
  4.      86         bl      __lookup_machine_type           @ r5=machinfo
複製代碼
看得出來跟proc很像,有個兩個小地方是% s+ J4 Q/ {5 `. V
. |/ }; F$ G8 `, \4 p7 d8 B2 o
1. line 207,用ldmia不是用ldmda。原因就在於存放arch 和 proc info 的位址剛好相反。一個在lable3的上面。一個在的下方。設計上應該是可以做修改的。
5 i  V4 F2 D+ a* E
+ q2 G* |; o. Q/ T2. arch定義的方式是透過macro,可以先看 ./include/asm-arm/mach/arch.h。裡頭有個 MACHINE_START ,這邊會設好macro,到時候直接使用就可以把arch的info宣告好。 例如 ./arch/arm/mach-omap1/board-generic.c
  1. /* macro */' d6 t+ T$ t3 v
  2.      50 #define MACHINE_START(_type,_name)                      \
    0 m, x. Y+ Q) K- D7 O
  3.      51 static const struct machine_desc __mach_desc_##_type    \
    % f; V, Z8 h* {! j/ D& d
  4.      52  __used                                                 \
    . X4 h6 m, \9 _# D2 R9 {! K
  5.      53  __attribute__((__section__(".arch.info.init"))) = {    \
    + i# k6 |# Z6 l
  6.      54         .nr             = MACH_TYPE_##_type,            \
    6 o* M- v: D% c( O
  7.      55         .name           = _name,
    8 i( t3 W  K' K+ [
  8.      56) K$ R% z' @: e# |4 V1 d
  9.      57 #define MACHINE_END                             \/ G, s+ R% B! z' N
  10.      58 };" ~. C$ Y# O  w# K9 B; t6 o# I
  11.      /* 用法 */
      D+ q- T5 d) P6 [* ]; `
  12.      93 MACHINE_START(OMAP_GENERIC, "Generic OMAP1510/1610/1710")
    : N+ t: @/ c+ i% N; ?6 h5 l# P
  13.      94         /* Maintainer: Tony Lindgren <tony@atomide.com> */
    4 m( e" o* d' ~! O8 U4 {& Y
  14.      95         .phys_io        = 0xfff00000,
    ( G7 g# l+ G! N( m% a$ M
  15.      96         .io_pg_offst    = ((0xfef00000) >> 18) & 0xfffc,
    # I5 g9 u4 K2 c- Q; Y. ]
  16.      97         .boot_params    = 0x10000100,
    3 I. R8 ?* a) u, T
  17.      98         .map_io         = omap_generic_map_io,
      A$ N) V9 T, G( j  r1 f2 k" B8 l
  18.      99         .init_irq       = omap_generic_init_irq,8 l2 |! L- b' L- v
  19.     100         .init_machine   = omap_generic_init,
    7 L& k; o! U! M" K
  20.     101         .timer          = &omap_timer,
    . W- h4 _! }) A/ S$ m; u& M4 x
  21.     102 MACHINE_END
    6 M0 p8 ^$ F0 {- o! E) M2 Q6 Z
  22. ! {4 h  h6 {8 Q) b
  23.     /* func */
    $ Z/ }2 p. `6 ]  B3 E
  24.     204         .type   __lookup_machine_type, %function
    * d) C8 ]" d. t2 ]3 M
  25.     205 __lookup_machine_type:
    % ^6 {9 z/ h! u# t: ^) U$ n
  26.     206         adr     r3, 3b' O8 u/ Y- P4 u
  27.     207         ldmia   r3, {r4, r5, r6}
    0 ~! x% K. R6 W+ J+ I
  28.     208         sub     r3, r3, r4                      @ get offset between virt&phys" o* k5 Q/ |4 M$ X
  29.     209         add     r5, r5, r3                      @ convert virt addresses to
    4 Z0 i3 h& K# K" Q% F
  30.     210         add     r6, r6, r3                      @ physical address space
    " |, t5 O  J6 B' ?/ j
  31.     211 1:      ldr     r3, [r5, #MACHINFO_TYPE]        @ get machine type6 x. b9 Y5 a+ b% g' L0 I
  32.     212         teq     r3, r1                          @ matches loader number?6 E! B& N& _0 ~2 q; b
  33.     213         beq     2f                              @ found$ ~7 V5 u+ [0 Z/ v" Z# l
  34.     214         add     r5, r5, #SIZEOF_MACHINE_DESC    @ next machine_desc
    5 v' Y5 w/ q) \- {6 Q
  35.     215         cmp     r5, r6: M# ]7 p5 `8 \
  36.     216         blo     1b+ w; C* q# G% s4 K) ~
  37.     217         mov     r5, #0                          @ unknown machine1 z* ?: S6 Y- \+ m
  38.     218 2:      mov     pc, lr
複製代碼
4#
 樓主| 發表於 2008-10-13 17:56:46 | 只看該作者
接著我們又返回到head.S,
1 L$ Y+ l6 l, V( X) M9 Z( xline 87~88也是做check動作。- H8 _5 p/ q, N; Q0 l
line 89跳到vet_atags。在head-common.S
  1.      87         movs    r8, r5                          @ invalid machine (r5=0)?6 F5 s% f0 V9 d6 k) l% F7 }  K
  2.      88         beq     __error_a                       @ yes, error 'a'/ |# ?! m1 Y6 J+ o, K" ^) o+ E
  3.      89         bl      __vet_atags) h! w2 n4 h$ k! B8 x) h7 E( e. X5 G
  4.      90         bl      __create_page_tables
複製代碼
line 245, tst會去做and動作。並且update flags。這邊的用意是判斷位址是不是aligned。
! s9 ~/ C$ R/ o) T( t# A. ]8 Jline 246, 沒有aligned跳到label 1,就返回了。
9 I. c  N7 A, i- Yline 248~250, 讀取atags所在的address裡頭的值到r5,看看是否不等於ATAG_CORE_SIZE,不等於的話也是返回。
; m/ M/ D0 T+ O; @; W. u1 R1 Hline 251~254, 判斷一下第一個達到的atag pointer是不是等於ATAG_CORE。如果正確的話,等一下要讀取atag的資料,才會正確。+ b4 T0 ~8 }7 w$ n. n5 \
(atag是由bootloader帶給linux kernel的東西,用來告知booting所需要知道的參數。例如螢幕寬度,記憶體大小等等)
  1.      14 #define ATAG_CORE 0x54410001
    : ]3 L3 a( j, a* c
  2.      15 #define ATAG_CORE_SIZE ((2*4 + 3*4) >> 2)
    : i2 e9 _) \5 k  m8 N" e# d
  3. ' h, G$ Y! @! x& K
  4.     243         .type   __vet_atags, %function
    . @! a5 j  }. Y1 w
  5.     244 __vet_atags:* k2 y6 R* N  s% t. G
  6.     245         tst     r2, #0x3                        @ aligned?0 m1 D6 K" S9 T  s4 _( i
  7.     246         bne     1f" V& x4 v) {+ [. @
  8.     247
    6 M0 b$ }* A2 W  B9 O+ e& z* Q
  9.     248         ldr     r5, [r2, #0]                    @ is first tag ATAG_CORE?
    * }! l0 X  e/ g- V
  10.     249         subs    r5, r5, #ATAG_CORE_SIZE
    * L. k! J  N/ Z1 Y0 D0 m" `8 |0 X; X) v
  11.     250         bne     1f1 G& F& x9 }  N' g% d1 R
  12.     251         ldr     r5, [r2, #4]
    3 D6 B4 k  m/ U3 R$ y  T) w
  13.     252         ldr     r6, =ATAG_CORE6 x7 V* z: D0 O$ O8 i; X- Y
  14.     253         cmp     r5, r6+ o7 f* `& Y0 U% R/ b$ z/ M
  15.     254         bne     1f: N" ~4 T" w7 ?2 g0 f9 ]+ K
  16.     2557 T8 w3 l' Z$ [' \' i
  17.     256         mov     pc, lr                          @ atag pointer is ok, [% O4 K% h5 C( w
  18.     257- }2 z4 x# E8 x
  19.     258 1:      mov     r2, #0
      u6 U- e/ _8 `' R
  20.     259         mov     pc, lr
複製代碼
接著我們又跳回去head.S。  1 T5 S1 Q+ F* t; u- ~% O
line 90,又跳到 __create_page_tables。   (很累人....應該會死不少腦細胞)# k7 H3 j, L& ?* j
哇!page table?!!如雷貫耳的東西,不知道會怎麼做。剛剛偷看了一下,@@還蠻長了,先到這邊好了,下次在繼續寫。
5#
 樓主| 發表於 2008-10-14 12:13:51 | 只看該作者
由於code看起來似乎越來越難解釋,會需要在檔案之間跳來跳去。建議一些基礎的東西可以再多複習幾次(其實是在說我自己 ),閱讀source code上收穫會比較多。例如:/ ]. |! S/ a' S0 s

3 ?/ ^& \8 q: F' N. J  T' {: N1. arm instruction set - 這個最好每個指令的意思都大略看過一次,行有餘力多看幾個版本,armv4, v5 or v6。7 |' M; O* s" @( r  y4 C/ M
2. compiler & assembler & linker - toolchain工具做的事情和功能,大致的流程和功能要有概念。
5 B9 K0 z2 N7 a3 c1 |4 N8 o3. Makefile & link script - 這兩個功能和撰寫的方式要有簡單的概念。2 K5 ?0 r- @7 a! L- n( X: e

1 F  s) h$ p4 O8 e以上1是非常重要的重點,2&3只要有大致上的概念就可以,因為trace code的時候,有時需要跳到Makeflie&Link script去看最後object code編排的位址。" Q. j+ d$ p; q5 g1 i
) v$ @% i. _6 [: D  O& X8 K' W
由於我們trace到了page table這個關鍵字,在開始之前,稍微簡短的解釋,為了幫助了解,儘量用易懂的概念講,有些用詞可能會和一些真實狀況不同,但是懂了之後,應該會有能力分辨,至於正式而學術上解說,很多書上應該都有,或是google一下就很多啦∼
6#
 樓主| 發表於 2008-10-14 12:14:47 | 只看該作者
page table本身是很抽象的東西,尤其是對所謂的 user-mode application programmer 來說,大部分的狀況它是不需要被考慮到的部份,但是他的產生卻對os和driver帶來了很多影響。我們從一個小小的疑問出發。
0 f7 ~# ~  g4 F7 ]) U6 d
' A. T5 O9 ]0 p% |% J4 D% D『產生page table到底是要給誰用的?』' c9 h8 e5 ~) b

' S, M4 _' P/ }* n其實真正作用在它上面的H/W是MMU,一旦CPU啟用了MMU,當cpu嘗試去記憶體讀取一個operand的時候,cpu內部打出去的位址訊號都會先送到mmu,mmu會拿著這個位址去對照page table,看看這個位址是不是被轉換到另外一個位置(還會確認讀寫權力),最後才會到真正的位址去讀寫。& k2 J9 D* ?) u1 j4 o" d3 r7 Q

6 b: A6 T, S: r, x  o  e6 h這樣來看CPU其實一開始打出去的位址訊號其實不是最後可以拿到資料的位址,我們稱為virtual address(va),mmu打出來的位址才能真正拿到資料,所以我們把mmu打出去的位址稱為physical address(pa)。那用來查詢這個位址對照關係的表格,就是page table。2 ]+ h1 q/ l( ]1 f

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

5 J$ q! C. O  l知道了page table的大致上的功能,下篇就可以專心的研究這個table的長相,和它想規劃出的系統模樣。: a: L' h' O' n. b' s( e9 Y
  O' |6 U! e4 H
p.s. 字數限制好像變短了。   (看來很難寫)
8#
 樓主| 發表於 2008-10-14 13:56:17 | 只看該作者
由於字數限制挑整,用詞儘量精簡,以便可以貼必要source。另外,以後寫到一段落,考慮收集成一篇完整的文章,這樣應就不會因為用回覆的方式,造成閱讀斷斷續續的問題。希望有人對文章呈現方式有想法的話,可以跟我講。
9#
 樓主| 發表於 2008-10-14 13:57:39 | 只看該作者
現在,讓我們跳入create_page_tables吧∼6 H- ?) t. x2 h- S! e1 j
line 216,會跳到pgtbl的macro去執行,其實只是載入一個位址。& }7 Z8 Y$ q! a+ L

( V% q  e* o2 _# ]# d只是這個位址因為你硬體規劃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 D& y! ]0 f9 [7 S
  2.      95 textofs-y       := 0x00008000
    & C/ {+ I/ t5 `1 }
  3.     152 TEXT_OFFSET := $(textofs-y)
複製代碼
  1. /* include/asm-arm/arch-omap/memory.h */
    - ]  P% e  N+ A
  2.      40 #define PHYS_OFFSET             UL(0x10000000)& K/ T( o  ~6 f0 W

  3. 6 Y) b1 R; w) Q3 Y9 a
  4.      /* arch/arm/kernel/head.S */
    % C1 m! K1 y" O: d/ _  v+ w$ h
  5.      29 #define KERNEL_RAM_VADDR        (PAGE_OFFSET + TEXT_OFFSET)6 I% b9 p% o8 |3 h
  6.      30 #define KERNEL_RAM_PADDR        (PHYS_OFFSET + TEXT_OFFSET)2 W. v& S( H) a4 W5 t: m4 S
  7. ) h% }9 y3 `% |% K7 {
  8.      47         .macro  pgtbl, rd* F4 J' s1 ^) b9 C5 D
  9.      48         ldr     \rd, =(KERNEL_RAM_PADDR - 0x4000)( z; L1 v1 t) Y8 Z. h0 o
  10.      49         .endm* F4 ?1 j6 E5 V; w" V

  11. * h; g% g* X1 m$ v. {
  12.     216         pgtbl   r4                              @ page table address
複製代碼
10#
 樓主| 發表於 2008-10-14 14:16:53 | 只看該作者
得到pg table的開始位置之後,當然就是初始化囉。
: M, h/ m9 l; jline 221, 將pg table的base addr放到r0.8 q" J7 B( Z6 u% Q' c$ e$ ]
line 223, 將pg table的end addr放到r6.
% p& H7 t- v3 X+ E& ^line 224~228, 反覆地將0x0寫到pg table的區段裡頭,每個loop寫16bytes. (4x4),直到碰到end,結果就是把它全部clear成0x0.
  1.     221         mov     r0, r4
    8 |$ R5 a$ X; Q9 v# p/ J5 P
  2.     222         mov     r3, #0! m! H* F" F, a1 ^0 U
  3.     223         add     r6, r0, #0x4000
    2 O" D0 E& L+ H9 `2 D
  4.     224 1:      str     r3, [r0], #4; I! u! C+ P+ V! O7 x5 M0 ?
  5.     225         str     r3, [r0], #4
    5 s+ k: x4 `" P, {# S
  6.     226         str     r3, [r0], #4
    ; G7 d4 e; t$ i% u
  7.     227         str     r3, [r0], #4
    . _4 c4 a: k' e7 s
  8.     228         teq     r0, r6$ x6 x0 ^, k$ 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 | 只看該作者
問題怎麼填值??& p9 Z) x8 j- {2 e9 @! q
拿出ARM的手冊,翻到MMU章節。一看發現page有很多種,還得分first level和second level。 1 O7 o6 {, }9 X7 S# p+ I- S7 f
# ?* U1 _% T, W" h/ c( ]0 a
念書時的印象,是從1st level在去查2nd level,先看怎麼設定1st level (不知以前老師有沒有亂教)& K/ |2 K( N) c2 w9 _- T2 A
1. [31:20]存著section base addr: N# B& s4 k9 N
2. [19:2]存著mmu flags; e* ~5 ?% |+ \
3. [1:0]用來辨別這是存放哪種page, 有四種:& h0 L; ^" u7 |- a2 K
   a. fault (00) b. coarse page (01) c. section (1st level) (10) d. fine page (11)# T- Y- R- w9 ?( l+ W* K0 l
4. pg tabel資料要存放到 [31:14] = translation base, [13:2] = table index , [1:0] = 0b00 的位址
" x) q/ T4 @& ]: k/ M2 M# A  _
; b' M, i9 W! n+ L( o+ N來看code是怎麼設定。. x( e: ~& \& k
1 ~0 [9 j( ~# |+ z) {" s6 f; S/ N
line 239, 將pc的值往右shift 20次放到r6.這是取得前20高位元的資料,有點像是 1.。
& q5 A$ v. q7 ?! B( dline 240, 將r6往左shift 20次之後,和r7做or的動作,剛好也是 2.提到的mmu flags和1.處理好的資料做整理。
7 [8 V3 [6 w# p; x7 k+ s所以前面兩個做完,就完成了bit[31:2]。1 w# X+ a$ i* [2 g+ m) }1 b
line 241, 將r3的資料寫到r4+(r6<<0x2)的地方去,剛好是4.提到的位址。(lucky)
  1.     239         mov     r6, pc, lsr #20$ D, Y0 [( \  x( Q) y  c
  2.     240         orr     r3, r7, r6, lsl #20
    ; n- W3 `( W- p# e  |8 g8 D
  3.     241         str     r3, [r4, r6, lsl #2]
複製代碼
p.s. 用pc值剛好可以算出當前的page。
12#
 樓主| 發表於 2008-10-22 19:47:03 | 只看該作者
最近又被釘上了,開始忙碌,大概沒太多時間貼文∼5 V+ v$ J# k' w9 c

9 ]9 L( N2 V8 n# {* b+ f1 ]4 b上篇已經將pc所屬的page table entry(pte)設定好,接著繼續看( f) k: i2 I$ ?  G8 q
line 247, 248, 將KERNEL_START的位址往右shift 18算出pte的offset,(等於line239&241,239&241是先shift right 20然後shift left 2),並將剛剛r3的值設給pte。『!』的意思是會把address存到r0。
, r6 l% I0 H8 \, I
; {  r+ F/ t0 a1 Q" r$ 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) >> 188 k! W5 q9 e: ]3 b+ ]. \7 s" `
  2.     248         str     r3, [r0, #(KERNEL_START & 0x00f00000) >> 18]! 7 u9 _4 r& z4 I4 _/ j7 b$ y/ T
  3.     249         ldr     r6, =(KERNEL_END - 1)
    ' q( {( E, R( j0 r
  4.     250         add     r0, r0, #4
    $ f! x, z- G  Z$ F# ~$ [8 V
  5.     251         add     r6, r4, r6, lsr #18- w4 T: |0 w2 M( F( y
  6.     252 1:      cmp     r0, r6( e% X0 ]# @8 A/ H
  7.     253         add     r3, r3, #1 << 20) F) k* N+ B& u) w
  8.     254         strls   r3, [r0], #4
    * o! S% Z; [) M2 e8 P
  9.     255         bls     1b
複製代碼
13#
 樓主| 發表於 2008-10-22 20:24:58 | 只看該作者
line279,PAGE_OFFSET是規範RAM mapping完後的virtual address,我們將這個位址所屬的pte算出來放到r0。
5 u4 W' i* q3 ^' xline 280~283,將要 map 的physical address的方式算出來放到r6。
, H. r) Y0 g6 [9 M  t( }- ?0 B4 lline 284,最後將結果存到r0所指到的pte。
/ V: p4 C3 A0 Y, K8 v' e0 R7 f( D1 F, l
以上三個動作,就是做好 ram 起頭的位址一開始map,還沒做完整塊map。由於我們目前的設定方式每塊是1MB,所以這邊是將RAM開始的1MB做map。! Y& R8 g4 U4 b
1 `- u9 M% ^' a
line 327,返回,結束一開始的create page table的動作,我們可以看出其實並沒有做完整的初始化page table,所以之後應該會有其他page table的細節。
  1.     279         add     r0, r4, #PAGE_OFFSET >> 180 L2 Y8 d- T3 T' k, d. |- D
  2.     280         orr     r6, r7, #(PHYS_OFFSET & 0xff000000)
    # f7 o+ V( [  S
  3.     281         .if     (PHYS_OFFSET & 0x00f00000)" k1 e& P; h7 D' V$ N
  4.     282         orr     r6, r6, #(PHYS_OFFSET & 0x00f00000)
    . a7 m+ a% H# U5 @5 L) j
  5.     283         .endif; N: A% _) I8 ^4 o3 J/ }5 u' S. N& C
  6.     284         str     r6, [r0]2 d+ |( C% h: L$ \* M4 i/ S
  7.     327         mov     pc, lr
複製代碼
附帶一提,我們這邊省略了一些用ifdef包起來的程式碼,像是一開始會印一些output message的程式碼(line286~326)。
14#
 樓主| 發表於 2008-10-22 20:37:08 | 只看該作者
自create page table返回後,我們偷偷看一下接下來的程式碼,
* ~& T; Y  D- s: B8 `- ]line 99, 將switch_data擺到r137 q0 u0 B2 p7 W, ]" }
line 101, 將enable_mmu擺到lr
; X! n* D+ D; c* |+ @line 102, 將pc改跳到r10+PROCINFO_INITFUNC的地方去
( t, T; d6 g8 R  N+ C; g# B$ j# u" P+ y$ j# s1 q
其實這邊有點玄機,switch_data和enable_mmu都是function,結果位址都只是被存起來,並沒有直接跳過去執行。其實,雖然程式碼是順序switch_data->enable_mmu->proc init function,但其實執行的順序會是 procinfo 的init function-> enable_mmu -> switch_data 。至於,為什麼要這樣寫的原因?就不清楚了,也還沒仔細推敲過。 9 b1 s8 |; m8 G8 o% ^

) b3 q0 c, r' G% Hswitch_data最後就會跳到大家都很熟悉的start_kernel(). 詳細要賣個關子,最近會忙一下,得要過一陣子才能貼文∼  
  1.      99         ldr     r13, __switch_data              @ address to jump to after
    # k" u, |5 |* X7 @( h) f% \
  2.     100                                                 @ mmu has been enabled! T- l2 W6 D5 V1 \- r1 n
  3.     101         adr     lr, __enable_mmu                @ return (PIC) address
    0 E1 w% B9 L* _/ X" g6 Z6 Q2 R
  4.     102         add     pc, r10, #PROCINFO_INITFUNC
複製代碼
15#
 樓主| 發表於 2009-7-4 01:09:36 | 只看該作者
老店重新開張~
. |2 |- j8 p, {, d* V7 E: v
3 v5 K' S7 a9 }) {花了一些時間把舊的貼文整理到一個blog6 D6 j2 c6 ~! S  ]$ g
有把一些敘述修改過
  A5 H* o5 r4 R. C) T) }0 \; h& P希望會比較容易集中閱讀
% p  L5 b0 @* d3 R5 C3 [" R目前因為某些敘述不容易
- A1 Q% V# Y; k2 Q( }還是比較偏向筆記式而且用字不夠精確
0 ?& K- \$ [5 N1 Z0 M# L+ }希望之後能夠慢慢有系統地整理* p  ^. E/ v6 A2 F4 s& L
大家有興趣的話- ]# E7 C- o, w2 g  i
可以來看看和討論
! J8 x9 b  ^0 x& Phttp://gogojesseco.blogspot.com/# l; B3 O4 v% A) D" }. E, H* B5 p# j

) m# |" J3 z6 [+ U4 }以後可能會採取  先在chip123貼新文章
5 J5 S/ `; \+ x5 T) U4 W慢慢整理到blog上的方式
' B# Q$ x9 X+ x  j5 N  `1 x因為chip123比較方便討論 =)
6 N4 H! {/ T+ W. }blog編輯修改起來比較方便
4 e  h0 W. B2 P閱讀也比較集中   大家可以在這邊看到討論7 [" O- ]6 W8 c- d# {' L
然後在blog看到完整的文章 (類似BBS精華區的感覺)

評分

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

查看全部評分

16#
 樓主| 發表於 2009-7-15 17:07:03 | 只看該作者
隔了很長一段時間沒update
: N% k8 B, P1 l2 d2 [+ n; ]之前程式碼走到 ./arch/arm/kernel/head.S 的 line 99 附近
  1.      99         ldr     r13, __switch_data              @ address to jump to after
    + `: R+ s  ^2 Y5 V" t
  2.     100                                                 @ mmu has been enabled% \0 S. I) C" X$ O
  3.     101         adr     lr, __enable_mmu                @ return (PIC) address5 u2 |5 R# k( X. `% D+ _
  4.     102         add     pc, r10, #PROCINFO_INITFUNC
複製代碼
line 99, 將__switch_data放到r13。(留作之後用)
! ]# k8 s8 T- Z% \/ s4 jline 101, 將__enable_mmu的addr放到lr。(留作之後用)
* I3 W; j, E& N1 L: sline 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
    + S, \& R8 K1 c: x
  2. 374 __arm926_setup:2 U$ x+ m/ F* R& x' B
  3. 375         mov     r0, #0
    # b; I' u- r; o2 [" z( x/ Q' y
  4. 376         mcr     p15, 0, r0, c7, c7              @ invalidate I,D caches on v4
    1 |5 x9 J4 x( ~1 A6 H- h3 g& J) |
  5. 377         mcr     p15, 0, r0, c7, c10, 4          @ drain write buffer on v4/ n( b3 S0 z' Z5 q
  6. 378 #ifdef CONFIG_MMU; E, \* m; k7 f* w
  7. 379         mcr     p15, 0, r0, c8, c7              @ invalidate I,D TLBs on v4
    # t5 V6 J5 w3 m! Y
  8. 380 #endif9 Q; {; ]% C6 x& _
  9. * B- a. S: Q! A7 p& p
  10. 388         adr     r5, arm926_crval
    ( _4 g" w1 S2 [4 h
  11. 389         ldmia   r5, {r5, r6}
    0 M7 q- F* L" |5 a! k/ |1 R
  12. 390         mrc     p15, 0, r0, c1, c0              @ get control register v4' x0 Y# e# E8 q! A" C8 _4 i
  13. 391         bic     r0, r0, r5
    $ u1 i7 ?- X- N  t/ T" u( D
  14. 392         orr     r0, r0, r6
    - {7 o, E2 F" g  k3 M! K3 a% K9 }

  15. * H; ~+ o* o' L
  16. 396         mov     pc, lr
    & g. d% d) m; w" }4 e
  17. 397         .size   __arm926_setup, . - __arm926_setup
複製代碼
這邊的程式碼就跟CPU有很大的相依性,# |  U! P3 n) K% I7 k* J* @
line 375~380, 主要就是invalidate CPU的I&D cache和清空write buffer。4 x! r9 e* N, n; x
line 388~392, 把cp15的設定讀出來,並且將一些預設狀態做好運算放到r0。(預設值從arm926_crval這邊可以取得。)
' F5 o/ ?7 [8 G2 Z* [/ H* Vline 396, 直接把pc跳到lr,因為我們之前已經將lr = enable_mmu,所以直接跳過去。
17#
 樓主| 發表於 2009-7-15 17:29:45 | 只看該作者
  1. 155 __enable_mmu:. p' K4 S/ Z7 J- s' l/ \9 w% M
  2. 170         mov     r5, #(domain_val(DOMAIN_USER, DOMAIN_MANAGER) | \
    & v) Y+ F# n. M! `  J
  3. 171                       domain_val(DOMAIN_KERNEL, DOMAIN_MANAGER) | \
    ) D, ^" ?9 s) _: P3 M
  4. 172                       domain_val(DOMAIN_TABLE, DOMAIN_MANAGER) | \. a$ t" V2 w. U
  5. 173                       domain_val(DOMAIN_IO, DOMAIN_CLIENT))
    " K; Y7 ]) y/ R, W
  6. 174         mcr     p15, 0, r5, c3, c0, 0           @ load domain access register
    $ I3 u* H$ r1 H* ?
  7. 175         mcr     p15, 0, r4, c2, c0, 0           @ load page table pointer+ j) D7 T# [8 n8 E
  8. 176         b       __turn_mmu_on
    ! X/ C8 f2 n5 _0 s& K
  9. 177 ENDPROC(__enable_mmu)
複製代碼
line 170~174,設置好domain access。(domain access可以先當成設置access的權限,或許以後可以寫詳細的文章說明)
4 n6 l) @4 Q5 t& i% }line 175~176,將create好的page table開頭丟給mmu。跳到turn_mmu_on
  1. 191 __turn_mmu_on:7 [! x& X4 _, ^2 ]1 G
  2. 192         mov     r0, r07 n7 a* e2 [: r) \4 e
  3. 193         mcr     p15, 0, r0, c1, c0, 0           @ write control reg
    + m7 R' I, F  @( i  x
  4. 194         mrc     p15, 0, r3, c0, c0, 0           @ read id reg0 H2 {5 ~* _5 _+ Q4 [- l% Z8 ?
  5. 195         mov     r3, r3+ u6 P- q4 G9 C4 `- q$ ^+ k# u
  6. 196         mov     r3, r3
    $ P" d5 M$ a' Z9 P! K) w
  7. 197         mov     pc, r13. x; K0 I+ d3 O$ _
  8. 198 ENDPROC(__turn_mmu_on)
複製代碼
顧名思義就是把mmu打開,將我們準備好的r0設定交給mmu,並讀取id到r3,接著pc跳到r13,r13剛剛在head.S已經先擺好__switch_data。所以會跳到head-common.S。
  1. 18 __switch_data:: `$ f  a$ w1 e, B; @) k
  2. 19         .long   __mmap_switched
    : ~0 a' r$ Z6 W$ l6 ?6 |2 k
  3. 20         .long   __data_loc                      @ r4
    ! t$ j$ R% y( k
  4. 21         .long   _data                           @ r5& S  t6 h( ?  m( d
  5. 22         .long   __bss_start                     @ r64 e5 `! F8 X6 d, x% D: }
  6. 23         .long   _end                            @ r7
    : s  w, @  q, L  l
  7. 24         .long   processor_id                    @ r46 y: S( h  x* ?% B6 l6 x3 |4 \
  8. 25         .long   __machine_arch_type             @ r5
    . F; N# {8 R; ?% ~* x- p
  9. 26         .long   __atags_pointer                 @ r62 V+ u! t- z! R* q8 j! A1 s) R6 Q
  10. 27         .long   cr_alignment                    @ r7
    / }$ D& E2 P& e/ d. I2 u! g
  11. 28         .long   init_thread_union + THREAD_START_SP @ sp' u6 A# K, }: y+ }: ~5 w. {
  12. 296 `- D" C- M/ E2 Y) d5 W3 p
複製代碼
switch_data的第一行就是 __mmap_switched,所以我們直接看line 39。
18#
 樓主| 發表於 2009-7-15 17:30:00 | 只看該作者
  1. 39 __mmap_switched:& `! f6 f, K& v8 Y8 x' l' W
  2. 40         adr     r3, __switch_data + 4$ C! Q9 k8 s' ~3 G" p( j0 k$ b+ E: e
  3. 413 J. ~: X1 q0 L# ]  S
  4. 42         ldmia   r3!, {r4, r5, r6, r7}
    ( h4 c! X5 X9 Z9 j
  5. 43         cmp     r4, r5                          @ Copy data segment if needed9 J, G4 s8 n7 T3 K* D0 c6 v
  6. 44 1:      cmpne   r5, r6
    6 M1 M( M  C% \! j" b, Z2 A" r8 Z
  7. 45         ldrne   fp, [r4], #43 h9 S/ z* @  u  k, Q9 U! u  {
  8. 46         strne   fp, [r5], #42 z$ S( y4 U( H1 }; m9 w9 v
  9. 47         bne     1b6 x3 d' {( y6 l5 @+ F7 Q! {
  10. 48
    3 z5 Q* ?  k& Z3 c9 q
  11. 49         mov     fp, #0                          @ Clear BSS (and zero fp)
    " q) a! \7 m8 U4 {
  12. 50 1:      cmp     r6, r7
    * s, O5 ^) L0 ~( Z- D
  13. 51         strcc   fp, [r6],#4
    ' u! I+ `& ^+ n6 ]) ]/ c2 W9 w
  14. 52         bcc     1b. D9 s' ?1 x3 _- P+ x  ~5 ~# u
  15. 53
    0 R' I6 m9 S5 v! F$ a5 |, c
  16. 54         ldmia   r3, {r4, r5, r6, r7, sp}' k6 G0 ~. J7 v" u; R/ Q
  17. 55         str     r9, [r4]                        @ Save processor ID8 V: B  k7 t- G
  18. 56         str     r1, [r5]                        @ Save machine type: d# C, }& b4 I! j  e2 N$ L
  19. 57         str     r2, [r6]                        @ Save atags pointer4 X) H( ~) |, B3 f  Z5 P
  20. 58         bic     r4, r0, #CR_A                   @ Clear 'A' bit: g$ w1 C5 v# G  J7 }
  21. 59         stmia   r7, {r0, r4}                    @ Save control register values/ a: l! i' W$ [2 Y( i) I( _
  22. 60         b       start_kernel
    + [/ q9 a" o$ G
  23. 61 ENDPROC(__mmap_switched)
複製代碼
switch_data的第一行就是 __mmap_switched,所以我們直接看line 39。
, J. G, R" m% V$ pline 39,將__data_loc的addr放到r3
' {  L9 B3 J) x3 Y/ b, \line 42,從r3的位址,連續讀取四筆資料到r4, r5, r6, r70 F; E+ s3 K2 T2 O2 Y. Y
line 43~47,看看data segment是不是需要搬動。9 x1 y# _  t9 p9 W3 O# X
line 49~52, clear BSS。& x! A- t! k4 E( g$ U
) j, c  W% q) T" B7 s2 V
由於linux kernel在進入start_kernel前有一些前提必須要滿足:; A/ b' v9 T3 s
r0  = cp#15 control register
" E4 d% C1 f3 H& d( R" U! h+ \r1  = machine ID
+ u; O: Q3 d# d5 A( `r2  = atags pointer+ T" a% o# Y. a4 E. a  Q
r9  = processor ID
+ U5 C) a; ^6 H0 O* Z# i
  c4 }! ?" t7 `6 H$ f所以line 54~59就是在做這些準備。
; E; ^$ a" q& k最後呢? 我們就跳到start_kernel了。(而且還是用b start_kernel,表示我們不會再回來了)/ R  f( i( M) ?6 z3 \# R

# q9 M6 S8 c2 c, A/ F1 N/ Z看一下start_kernel()在./init/main.c,終於跳出architecture specific的目錄,表示2 D, l, b1 W  u# M
我們真正的開始linux kernel的初始化。; ^+ C% c4 P3 p0 z
像是 shedule init, console init, memory init, irq init等等都在start_kernel裡頭。
( Y) Z" P! G2 S3 ~1 F- n到這邊之後,應該就可以深入linux kernel中,各項比較跟hardware不那麼相關的軟體部分。

評分

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

查看全部評分

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

本版積分規則

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

GMT+8, 2024-6-9 08:44 AM , Processed in 0.177023 second(s), 23 queries .

Powered by Discuz! X3.2

© 2001-2013 Comsenz Inc.

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