Chip123 科技應用創新平台

 找回密碼
 申請會員

QQ登錄

只需一步,快速開始

Login

用FB帳號登入

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

trace linux kernel source - ARM - 03

[複製鏈接]
跳轉到指定樓層
1#
發表於 2008-10-7 14:00:18 | 只看該作者 回帖獎勵 |倒序瀏覽 |閱讀模式
到目前為止,我們已經進展到kernel幫自己relocate完,並解將自己解壓縮到一個地方要準備開始執行,那疑問來了?到底是跳到哪裡去了,因為./compressed/head.S最後一行居然是0 k6 K4 ?. s4 f  r9 i8 h5 Q3 _
『mov pc, r4』
9 Q, Z/ r, O1 V+ v% p" n( p/ `r4只代表了解壓縮完後kernel的位址,那究竟整包kernel編譯的時候,哪個function哪個東西被放在最前面咧?!. h0 l: \7 [  L  }( Q; m  M: T

+ E6 i6 S5 l. w8 C! l所以我們又必須開始找於kernel link的時候是怎麼被安排的,有了前面的基礎,我們可以從Makefile知道程式碼如何被編譯。至於link上的細節,例如有那些section和section先後順序等等,可以從 lds 檔來規範。6 [& z# I, t' m
9 }" s+ n2 y$ P+ u# J
有興趣的人可以看一下 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)- y# J4 G  E. f: c+ ?
我們可以發現第一個section是『.text.head』,裡頭的_stext從目前的位置開始放。, \, A$ F9 E& D& n
於是我們曉得只要找到屬於.text.head這個section,並且是_stext這個symbol的程式碼,就是解壓縮完後的第一行程式碼。
  1.      26     .text.head : {
    # n( S% n( Y4 X
  2.      27         _stext = .;
    ; k$ [2 F" a1 Y# r3 Q" R* Q
  3.      28         _sinittext = .;" {4 x# ?/ X4 e( b8 K- @* K
  4.      29         *(.text.head)
    % E9 A% w& l, G' W# @
  5.      30     }
複製代碼
用指令搜尋一下,發現 ./arch/arm/kernel/head.S 裡頭有關鍵字(如下),結果我們從./arch/arm/boot/compressed/head.S跳到了./arch/arm/kernel/head.S
  1.      77     .section ".text.head", "ax"/ u8 H' ]+ P1 G9 r4 g
  2.      78     .type   stext, %function" T' U- T2 P) k8 z+ v" p# B
  3.      79 ENTRY(stext)
    8 r4 H/ F& V$ H% a0 \: z; o2 n$ Z, J
  4.      80     msr cpsr_c, #PSR_F_BIT | PSR_I_BIT | SVC_MODE @ ensure svc mode. U8 X( e. m0 r, M' C
  5.      81                         @ and irqs disabled% V# n6 S4 p8 ~- K
  6.      82     mrc p15, 0, r9, c0, c0      @ get processor id
    " M2 d/ J* `2 b; a5 F4 j, A
  7.      83     bl  __lookup_processor_type     @ r5=procinfo r9=cpuid
    ! Z) ?( O. N: ?3 {
  8.      84     movs    r10, r5             @ invalid processor (r5=0)?; P0 c/ `0 L4 e0 W* l
  9.      85     beq __error_p           @ yes, error 'p'
    3 K, k7 K/ _' A# ^$ {! C2 h
  10.      86     bl  __lookup_machine_type       @ r5=machinfo
      W1 r# F' }# V2 R; D& ~8 D6 _
  11.      87     movs    r8, r5              @ invalid machine (r5=0)?4 |7 ?6 {9 l; K$ Z, q! ?
  12.      88     beq __error_a           @ yes, error 'a'
    , p' p  N3 ^& Z! ?* p
  13.      89     bl  __vet_atags
    % L; Q3 ?. z3 p8 Y! v
  14.      90     bl  __create_page_tables
複製代碼
既然找到了檔案,我們又可以開始繼續。$ {/ v2 d; k, a( K6 t; `1 Q
+ |) Q9 j7 {  K$ B
看了一下,程式碼多了很多bl的跳躍動作,看來會在各個function跳來跳去。
分享到:  QQ好友和群QQ好友和群 QQ空間QQ空間 騰訊微博騰訊微博 騰訊朋友騰訊朋友
收藏收藏 分享分享 頂 踩 分享分享
2#
 樓主| 發表於 2008-10-9 15:32:16 | 只看該作者
既然跳到真正的kernel開始跑,表示進入重頭戲,在進入kernel之前arm的平台有一些預設的狀況,也就是說arm kernel image會預設目前的cpu和系統的狀況是在某個狀態,這樣對一個剛要跑起來的OS比較決定目前要怎麼boot起來。
+ @! S0 L; U2 m' W$ B. D2 N  d% L. |; [) i
可以看一下comment,有清楚的描述。這也幫助我們了解為什麼decompresser的程式跑完之後,還要把cache關掉。
  1.      59 /*
    8 Q& F9 y- K& T+ D9 w8 O
  2.      60  * Kernel startup entry point.
    : u6 E; B1 z% `
  3.      61  * ---------------------------7 W! H2 o* X6 y4 P* c
  4.      62  *
    4 {% {1 g2 s# ?& ~
  5.      63  * This is normally called from the decompressor code.  The requirements
    4 r2 m# N+ h6 P8 F' }( W3 n: V
  6.      64  * are: MMU = off, D-cache = off, I-cache = dont care, r0 = 0,) i7 F% K2 R: F/ ~- s
  7.      65  * r1 = machine nr, r2 = atags pointer.
複製代碼
基於以上的假設,當program counter指到kernel的程式的時候,就會從line 80開始跑。3 @. M6 j4 I2 f
line 80, msr指令會把, operand的值搬到cpsr_c裡面。這是用來確保arm cpu目前是跑在svc mode, irq&fiq都disable。(設成0x1就會disable,definition在./include/asm-arm/ptrace.h)
8 A1 f. q  k( m6 q- }line 82, 讀取CPU ID到r9
% g1 ^4 b5 n! ]. c, z8 @$ a$ i  iline 83, 跳到 __lookup_processor_type 執行(bl會記住返回位址)。
  1.      77     .section ".text.head", "ax"
    ( O$ V$ i) \; D, z
  2.      78     .type   stext, %function! R- _/ Z% v% w1 t+ q2 B3 Q
  3.      79 ENTRY(stext)8 r3 U; R9 s2 h( h% D+ N
  4.      80     msr cpsr_c, #PSR_F_BIT | PSR_I_BIT | SVC_MODE @ ensure svc mode3 B9 `5 t- v7 f9 ^4 O2 }
  5.      81                         @ and irqs disabled
    " B9 M! Y" L/ k4 p$ ]! z) u
  6.      82     mrc p15, 0, r9, c0, c0      @ get processor id9 X( ~/ ^' a; Y
  7.      83     bl  __lookup_processor_type     @ r5=procinfo r9=cpuid
複製代碼
接著會跳到head-common.S這個檔,
3 j9 t6 T8 k) j) x5 Y2 T% L" c( Sline 158, (3f表示forware往前找叫做『3』label)將label 3的address放到r3。
$ @( k; A2 p. G4 E2 aline 159, 將r3指到的位址的資料,依序放到r7, r6, r5.(ldmda的da是要每次都位址減一次)
3 \2 G% U+ h" e, H5 M) Iline l60, 161, 利用真實得到位址r3減去取得資料的位址得到一個offset值,這樣可計算出r5, r6真正應該要指到的地方。$ ~5 q6 u% k- S; A
line 163~169,這邊的程式應該不陌生,就是在各個CPU info裡面找尋對應的structure。找到的話就跳到line 171,返回head.S
( s) k# ]+ u" uline 170, 找不到的話,r5的processor id就放0x0.表示unknown id。# m  B- F& g7 @: Y
& y2 K% o9 t, D' \" 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
    6 h5 f" L1 U0 |& Z
  2.     157 __lookup_processor_type:% c9 N6 d7 Z1 c/ g/ O1 d8 K
  3.     158     adr r3, 3f" ~) T' y6 N. Y& A. C
  4.     159     ldmda   r3, {r5 - r7}
    6 [0 ]& j! Q/ q  E
  5.     160     sub r3, r3, r7          @ get offset between virt&phys
    ( M! O9 n& D* R2 V+ N4 v$ E
  6.     161     add r5, r5, r3          @ convert virt addresses to, j- Z9 n' ?, N, X: A
  7.     162     add r6, r6, r3          @ physical address space
    : V: p) w4 |) `
  8.     163 1:  ldmia   r5, {r3, r4}            @ value, mask$ |8 T3 }: }6 d. r
  9.     164     and r4, r4, r9          @ mask wanted bits
    8 Y( `+ S$ P$ W. \2 I& M+ c
  10.     165     teq r3, r4
    6 |3 i8 E  c8 D6 N5 _; ^7 {' n
  11.     166     beq 2f
    2 j/ h7 @* Y: ?0 `& n0 I
  12.     167     add r5, r5, #PROC_INFO_SZ       @ sizeof(proc_info_list)' G5 D# I( w0 k; g8 c
  13.     168     cmp r5, r6
    ; n$ g+ S" P1 y4 d
  14.     169     blo 1b& s' z* Z* v) c- C/ `
  15.     170     mov r5, #0              @ unknown processor4 S9 b4 b: A& }
  16.     171 2:  mov pc, lr0 A! S! M3 c0 @+ ?- l+ \
  17. 6 R, w# U* h$ v2 ~3 u
  18.     187     .long   __proc_info_begin2 v: m! ~* m( r% L$ D3 Z
  19.     188     .long   __proc_info_end
    - O/ j& v+ E" G3 S2 q. ?1 H2 q
  20.     189 3:  .long   .
    ( r3 Q% Z+ C/ R
  21.     190     .long   __arch_info_begin1 n  ?) C! a' U9 h0 k  \
  22.     191     .long   __arch_info_end
複製代碼
跳了很多檔案,建議指令和object code如何link的概念要有,就會很清楚了。
3#
 樓主| 發表於 2008-10-13 17:20:35 | 只看該作者
我們從 head-common.S返回之後,接著繼續看。
- R# k6 j5 J: v1 y4 Z
* h* v3 D5 h7 H, A" [$ ^line 84, movs的意思是說,做mov的動作,並且將指令的S bit設起來,最後的結果也會update CPSR。這個指令執行的過程當中,會根據你要搬動的值去把N and Z flag設好。這個是有助於下個指令做check的動作。
3 D( A' C# s, }5 Q. ?line 85, 就是r5 = 0的話,就跳到__error_p去執行。
, M  e7 }2 k; g* \) e3 J# Gline 86, 跳到__lookup_machine_type。原理跟剛剛找proc的資料一樣。
  1.      83         bl      __lookup_processor_type         @ r5=procinfo r9=cpuid
    9 w: ~, [" V: U. {" W
  2.      84         movs    r10, r5                         @ invalid processor (r5=0)?
    . f5 L1 g2 D* _+ g' i- U
  3.      85         beq     __error_p                       @ yes, error 'p'- M: y. f% o- f# {  e: h5 W. f
  4.      86         bl      __lookup_machine_type           @ r5=machinfo
複製代碼
看得出來跟proc很像,有個兩個小地方是
1 O! o- G3 u+ ~/ F
2 t  k" o9 J, m% d6 n1. line 207,用ldmia不是用ldmda。原因就在於存放arch 和 proc info 的位址剛好相反。一個在lable3的上面。一個在的下方。設計上應該是可以做修改的。
# T( H% e& r) ~# d" P- y2 S9 G  r# e
2. arch定義的方式是透過macro,可以先看 ./include/asm-arm/mach/arch.h。裡頭有個 MACHINE_START ,這邊會設好macro,到時候直接使用就可以把arch的info宣告好。 例如 ./arch/arm/mach-omap1/board-generic.c
  1. /* macro */. `" z6 q" \4 g8 R/ G/ C
  2.      50 #define MACHINE_START(_type,_name)                      \5 r+ ^, u" q6 ]( q; R9 `
  3.      51 static const struct machine_desc __mach_desc_##_type    \9 i2 N) b# G( f4 p0 Z' \
  4.      52  __used                                                 \2 o( o1 w( V$ W9 Q
  5.      53  __attribute__((__section__(".arch.info.init"))) = {    \. E0 D0 G0 S2 i- z. k1 {7 }
  6.      54         .nr             = MACH_TYPE_##_type,            \
    / c' L3 d( ]: Q: u
  7.      55         .name           = _name,
    : g7 `+ r' n1 B6 b
  8.      56
    7 H% f/ u) S* w" H/ F5 V4 j8 h
  9.      57 #define MACHINE_END                             \
    : e% a7 t4 _# s" g( F
  10.      58 };
    " P% R; z2 b5 Y1 `" d8 i9 L
  11.      /* 用法 */5 P7 d# b' |+ V, g# t- k! R
  12.      93 MACHINE_START(OMAP_GENERIC, "Generic OMAP1510/1610/1710")
    . f# s5 ]+ H" E+ [$ `
  13.      94         /* Maintainer: Tony Lindgren <tony@atomide.com> */
    6 f: l3 f8 r* K$ v
  14.      95         .phys_io        = 0xfff00000,6 [. q9 |( Z: ^+ T& ^
  15.      96         .io_pg_offst    = ((0xfef00000) >> 18) & 0xfffc,8 b$ r8 \2 k# B8 A
  16.      97         .boot_params    = 0x10000100,
    $ s, d& |; I; f8 e( _) W
  17.      98         .map_io         = omap_generic_map_io,
    8 x( B" u$ `& v1 o; V( i( v
  18.      99         .init_irq       = omap_generic_init_irq,
    & W& g0 f! z& `# s
  19.     100         .init_machine   = omap_generic_init,( x0 @$ o& ?' @7 p2 q* v% Q& J0 q
  20.     101         .timer          = &omap_timer,* e& T& }& }4 H+ k9 E
  21.     102 MACHINE_END2 K2 g# s2 k6 f0 J5 {* ^6 A

  22. 4 h# K3 ]4 q/ q& i/ c5 P
  23.     /* func */1 s- L8 W0 n9 D# T& R0 x
  24.     204         .type   __lookup_machine_type, %function
    6 B- ?( @4 `& b/ D( r1 i
  25.     205 __lookup_machine_type:
    7 a5 [3 p$ G* J$ M' y
  26.     206         adr     r3, 3b
    ) R; |/ ]0 ]4 ^5 y0 X. Y7 Y) Z0 D( [
  27.     207         ldmia   r3, {r4, r5, r6}
    2 c% o! B  q* S* H
  28.     208         sub     r3, r3, r4                      @ get offset between virt&phys4 r  I  F2 u6 m" P0 y, S
  29.     209         add     r5, r5, r3                      @ convert virt addresses to+ L: N' y9 G3 R
  30.     210         add     r6, r6, r3                      @ physical address space
    9 `# v) h2 e0 l9 C; s
  31.     211 1:      ldr     r3, [r5, #MACHINFO_TYPE]        @ get machine type
    / X( D( Y7 E; S+ B( E, K
  32.     212         teq     r3, r1                          @ matches loader number?
    0 v; S# y& M! k/ _0 o+ J1 c
  33.     213         beq     2f                              @ found$ y6 x! W1 m  b9 j) z
  34.     214         add     r5, r5, #SIZEOF_MACHINE_DESC    @ next machine_desc
      D' |! X; q7 a
  35.     215         cmp     r5, r67 h2 j" \4 G; {, x) y
  36.     216         blo     1b
      Y! W. _7 C: e4 v6 u2 D
  37.     217         mov     r5, #0                          @ unknown machine' _$ v1 x2 y9 M, q6 z. c" {0 S
  38.     218 2:      mov     pc, lr
複製代碼
4#
 樓主| 發表於 2008-10-13 17:56:46 | 只看該作者
接著我們又返回到head.S,6 g- L) P& f& e2 T# ^
line 87~88也是做check動作。& b3 D9 p2 \2 s5 _3 p- U
line 89跳到vet_atags。在head-common.S
  1.      87         movs    r8, r5                          @ invalid machine (r5=0)?' A" O3 H7 ]2 R( V6 O+ s2 P
  2.      88         beq     __error_a                       @ yes, error 'a'
    0 B6 {  o; Q7 P
  3.      89         bl      __vet_atags
    # R0 I9 `. {/ C0 j# z
  4.      90         bl      __create_page_tables
複製代碼
line 245, tst會去做and動作。並且update flags。這邊的用意是判斷位址是不是aligned。
* z. z/ b% b/ b, z, u6 ]line 246, 沒有aligned跳到label 1,就返回了。
2 |' ]- [) v: N2 E& n) j, K: Yline 248~250, 讀取atags所在的address裡頭的值到r5,看看是否不等於ATAG_CORE_SIZE,不等於的話也是返回。) b! Z) N; h3 |" \8 h- K/ z
line 251~254, 判斷一下第一個達到的atag pointer是不是等於ATAG_CORE。如果正確的話,等一下要讀取atag的資料,才會正確。
6 E6 p0 K* }. {/ N( X& h& e1 l& }(atag是由bootloader帶給linux kernel的東西,用來告知booting所需要知道的參數。例如螢幕寬度,記憶體大小等等)
  1.      14 #define ATAG_CORE 0x54410001
    & M8 w; j8 ?( K9 w
  2.      15 #define ATAG_CORE_SIZE ((2*4 + 3*4) >> 2), e3 s; X) T( u& q9 g% `

  3. 9 A) p- b* p& V* b7 }3 n8 D9 ?/ k
  4.     243         .type   __vet_atags, %function
    ! I) V! ~' H* T: l- R7 o6 s
  5.     244 __vet_atags:
    ' }7 o* x0 S! ]0 P& f* @) w
  6.     245         tst     r2, #0x3                        @ aligned?( O3 r; d& m2 n: s' E1 Y0 E& d5 G% X
  7.     246         bne     1f) L$ f6 o: R3 t( W3 r2 R" J
  8.     2479 x, n0 r+ c7 K3 U
  9.     248         ldr     r5, [r2, #0]                    @ is first tag ATAG_CORE?9 p3 r% J7 T2 A- e! R6 |+ }3 n+ A
  10.     249         subs    r5, r5, #ATAG_CORE_SIZE
    5 y) }: T7 Q3 l& p: ?1 ?3 o
  11.     250         bne     1f1 p5 U* P* O2 Q. P; s
  12.     251         ldr     r5, [r2, #4]5 T8 P! k# b* @. x! ^$ X
  13.     252         ldr     r6, =ATAG_CORE* q& c- T3 q, b& M
  14.     253         cmp     r5, r6
    ) N! d7 E$ U. x, v; C
  15.     254         bne     1f
    1 Z: |# W- M: X4 C7 t' K
  16.     255, x3 R3 b; {, Q8 Y
  17.     256         mov     pc, lr                          @ atag pointer is ok
    " N8 W! Y8 ?, L9 u! w( L
  18.     257; `2 E5 y3 C4 S
  19.     258 1:      mov     r2, #0
    : e4 T0 h9 s# Y3 ], f. g8 @
  20.     259         mov     pc, lr
複製代碼
接著我們又跳回去head.S。  & f- l  s, q& i
line 90,又跳到 __create_page_tables。   (很累人....應該會死不少腦細胞)
2 r9 ]* E8 `6 u! a7 u$ A7 I哇!page table?!!如雷貫耳的東西,不知道會怎麼做。剛剛偷看了一下,@@還蠻長了,先到這邊好了,下次在繼續寫。
5#
 樓主| 發表於 2008-10-14 12:13:51 | 只看該作者
由於code看起來似乎越來越難解釋,會需要在檔案之間跳來跳去。建議一些基礎的東西可以再多複習幾次(其實是在說我自己 ),閱讀source code上收穫會比較多。例如:7 k) K: j( W* \4 D
& e' p- }5 d; f6 F8 }+ N
1. arm instruction set - 這個最好每個指令的意思都大略看過一次,行有餘力多看幾個版本,armv4, v5 or v6。
1 @% O; U9 G" ^  G2. compiler & assembler & linker - toolchain工具做的事情和功能,大致的流程和功能要有概念。2 ~' J0 U% Q0 ~3 b' ], t
3. Makefile & link script - 這兩個功能和撰寫的方式要有簡單的概念。8 ]5 s8 i2 p5 A! p. m
% P; f4 U. Q$ `6 |* f) _( q$ j
以上1是非常重要的重點,2&3只要有大致上的概念就可以,因為trace code的時候,有時需要跳到Makeflie&Link script去看最後object code編排的位址。
2 W$ \$ J" V" E7 i3 D3 I
, q8 J) w) l4 K5 k; h8 T9 j* e, M* w由於我們trace到了page table這個關鍵字,在開始之前,稍微簡短的解釋,為了幫助了解,儘量用易懂的概念講,有些用詞可能會和一些真實狀況不同,但是懂了之後,應該會有能力分辨,至於正式而學術上解說,很多書上應該都有,或是google一下就很多啦∼
6#
 樓主| 發表於 2008-10-14 12:14:47 | 只看該作者
page table本身是很抽象的東西,尤其是對所謂的 user-mode application programmer 來說,大部分的狀況它是不需要被考慮到的部份,但是他的產生卻對os和driver帶來了很多影響。我們從一個小小的疑問出發。6 e: {0 [) m8 a/ }

! O8 S. k7 t5 k" c/ _7 Y: ?/ [『產生page table到底是要給誰用的?』
: k1 x& o9 L" ~! ?% I4 u1 e0 d; I0 K3 M/ V2 L% f
其實真正作用在它上面的H/W是MMU,一旦CPU啟用了MMU,當cpu嘗試去記憶體讀取一個operand的時候,cpu內部打出去的位址訊號都會先送到mmu,mmu會拿著這個位址去對照page table,看看這個位址是不是被轉換到另外一個位置(還會確認讀寫權力),最後才會到真正的位址去讀寫。
' T& }. K+ T. |& y9 a! ]7 Z7 Z5 J% B* [9 j) @
這樣來看CPU其實一開始打出去的位址訊號其實不是最後可以拿到資料的位址,我們稱為virtual address(va),mmu打出來的位址才能真正拿到資料,所以我們把mmu打出去的位址稱為physical address(pa)。那用來查詢這個位址對照關係的表格,就是page table。7 E1 ]8 }. N1 M) K5 H# g# c2 a" |

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

2 v* D$ w/ K) S7 h, U0 Q知道了page table的大致上的功能,下篇就可以專心的研究這個table的長相,和它想規劃出的系統模樣。
, I# M& T) K7 x2 v8 B7 H' p+ d: p3 c
p.s. 字數限制好像變短了。   (看來很難寫)
8#
 樓主| 發表於 2008-10-14 13:56:17 | 只看該作者
由於字數限制挑整,用詞儘量精簡,以便可以貼必要source。另外,以後寫到一段落,考慮收集成一篇完整的文章,這樣應就不會因為用回覆的方式,造成閱讀斷斷續續的問題。希望有人對文章呈現方式有想法的話,可以跟我講。
9#
 樓主| 發表於 2008-10-14 13:57:39 | 只看該作者
現在,讓我們跳入create_page_tables吧∼
! H* Q2 ?% q0 j4 l+ Nline 216,會跳到pgtbl的macro去執行,其實只是載入一個位址。% r9 l5 E+ r( ^$ n( {  b& Y/ v

) L9 u% b) L5 q( H4 c只是這個位址因為你硬體規劃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 */, E! V& s+ d2 y. z+ F7 D' K* D) O! o
  2.      95 textofs-y       := 0x00008000/ k- M7 n8 a6 x  [% A  F7 e
  3.     152 TEXT_OFFSET := $(textofs-y)
複製代碼
  1. /* include/asm-arm/arch-omap/memory.h */
    5 r9 d, p) w/ E7 [
  2.      40 #define PHYS_OFFSET             UL(0x10000000)
    9 }& {& D# K  H  H6 r: d

  3. 9 y. x  k) D+ q7 N3 c* x
  4.      /* arch/arm/kernel/head.S */
    ' W# j1 Z7 J$ e$ a8 J- Y  s) @
  5.      29 #define KERNEL_RAM_VADDR        (PAGE_OFFSET + TEXT_OFFSET), z6 H0 K$ v/ H
  6.      30 #define KERNEL_RAM_PADDR        (PHYS_OFFSET + TEXT_OFFSET)
    8 t  \" T: u6 I' B
  7. 7 ^! H/ o7 W+ p- e
  8.      47         .macro  pgtbl, rd
    : K/ f3 f/ }2 b6 {2 @& B+ U
  9.      48         ldr     \rd, =(KERNEL_RAM_PADDR - 0x4000)2 G# X! Q6 V+ [3 r) A. d
  10.      49         .endm
    $ |; v, j5 c5 ?9 G& `2 A1 w/ a- y
  11. ( N7 G- h' y, t: g; g; K
  12.     216         pgtbl   r4                              @ page table address
複製代碼
10#
 樓主| 發表於 2008-10-14 14:16:53 | 只看該作者
得到pg table的開始位置之後,當然就是初始化囉。
0 X$ W- L8 c0 \line 221, 將pg table的base addr放到r0.
" U% p7 \. d5 x2 G# f6 ]' B6 L( Xline 223, 將pg table的end addr放到r6.
7 K+ k. V. i6 D& lline 224~228, 反覆地將0x0寫到pg table的區段裡頭,每個loop寫16bytes. (4x4),直到碰到end,結果就是把它全部clear成0x0.
  1.     221         mov     r0, r48 |! H' l$ y4 T; Z$ U' w
  2.     222         mov     r3, #0" h$ U8 n) l. t9 U8 W7 c( q) `
  3.     223         add     r6, r0, #0x4000
    1 P6 d, m6 h# a$ \6 i2 Z
  4.     224 1:      str     r3, [r0], #4
    & a" [; N' t& m8 i& i
  5.     225         str     r3, [r0], #43 V' `0 l+ \* z! D# |' t
  6.     226         str     r3, [r0], #4
    : Q3 n7 y- U* T7 v3 j* _
  7.     227         str     r3, [r0], #4
    " m; J: B! f2 D# J
  8.     228         teq     r0, r6; n* T& }2 p; V7 i
  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 | 只看該作者
問題怎麼填值??; w8 o6 P7 f9 n2 _
拿出ARM的手冊,翻到MMU章節。一看發現page有很多種,還得分first level和second level。
0 |. G- @  F. U& D
4 q- {/ C4 B' h4 V8 I  i/ x念書時的印象,是從1st level在去查2nd level,先看怎麼設定1st level (不知以前老師有沒有亂教)
; z! o. r: m- H! i- O; J1. [31:20]存著section base addr9 Q! n% ~2 J" I
2. [19:2]存著mmu flags
" Y/ W: {1 l2 I1 s6 O+ y8 ?* z3. [1:0]用來辨別這是存放哪種page, 有四種:
9 b' P5 V" z" `2 d   a. fault (00) b. coarse page (01) c. section (1st level) (10) d. fine page (11)
6 B" p5 m# d: T- A/ g& y4. pg tabel資料要存放到 [31:14] = translation base, [13:2] = table index , [1:0] = 0b00 的位址( [4 @3 x  l! N& L
1 C  D4 w7 u; h" e+ ^
來看code是怎麼設定。
' ~5 M: K) X: x- q2 E) t& I
2 y& I* m. w) b6 i: yline 239, 將pc的值往右shift 20次放到r6.這是取得前20高位元的資料,有點像是 1.。$ v6 M5 G9 ]" t! S
line 240, 將r6往左shift 20次之後,和r7做or的動作,剛好也是 2.提到的mmu flags和1.處理好的資料做整理。9 _$ b) U$ J* n- m( B: Q+ E0 T
所以前面兩個做完,就完成了bit[31:2]。6 [7 P; R0 W4 P& j$ p+ Z
line 241, 將r3的資料寫到r4+(r6<<0x2)的地方去,剛好是4.提到的位址。(lucky)
  1.     239         mov     r6, pc, lsr #207 Z- d# {+ |! c
  2.     240         orr     r3, r7, r6, lsl #20
    5 {( G/ P0 u+ M: R
  3.     241         str     r3, [r4, r6, lsl #2]
複製代碼
p.s. 用pc值剛好可以算出當前的page。
12#
 樓主| 發表於 2008-10-22 19:47:03 | 只看該作者
最近又被釘上了,開始忙碌,大概沒太多時間貼文∼
- ~5 a! M5 g# F! X$ L3 S" w
9 [3 J# J. [" d3 G  E上篇已經將pc所屬的page table entry(pte)設定好,接著繼續看' q( y! [& r( ~. t, y8 J6 Q! v
line 247, 248, 將KERNEL_START的位址往右shift 18算出pte的offset,(等於line239&241,239&241是先shift right 20然後shift left 2),並將剛剛r3的值設給pte。『!』的意思是會把address存到r0。% I& ~8 L0 G# @% ~( i$ g/ Y/ I, A7 t

+ k8 {) L& b2 O+ o6 qline 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/ \( g7 @! y  ~0 k" F% e* p
  2.     248         str     r3, [r0, #(KERNEL_START & 0x00f00000) >> 18]!
    # c, U7 c1 L8 h9 x& W8 E% o
  3.     249         ldr     r6, =(KERNEL_END - 1)5 S. C& @+ D. U5 \7 A! G! W2 @) g/ X
  4.     250         add     r0, r0, #4
    3 A0 b- I6 K. q: t
  5.     251         add     r6, r4, r6, lsr #18
    : o2 \. k1 E7 ?
  6.     252 1:      cmp     r0, r6. e* J9 a. Y: x9 s
  7.     253         add     r3, r3, #1 << 20
    7 @' j% W9 R$ p! j% U: F
  8.     254         strls   r3, [r0], #4$ ^2 |1 j) w& z9 X3 p
  9.     255         bls     1b
複製代碼
13#
 樓主| 發表於 2008-10-22 20:24:58 | 只看該作者
line279,PAGE_OFFSET是規範RAM mapping完後的virtual address,我們將這個位址所屬的pte算出來放到r0。
9 u+ ~5 d- {: m2 _4 gline 280~283,將要 map 的physical address的方式算出來放到r6。
% b3 b. a4 w# Y. @line 284,最後將結果存到r0所指到的pte。0 V  B. |- O+ _% L
$ s& S" J! D9 h4 y
以上三個動作,就是做好 ram 起頭的位址一開始map,還沒做完整塊map。由於我們目前的設定方式每塊是1MB,所以這邊是將RAM開始的1MB做map。
5 n  Y( x! B: J3 {; z. x
7 X" T* B1 ~; z0 x* nline 327,返回,結束一開始的create page table的動作,我們可以看出其實並沒有做完整的初始化page table,所以之後應該會有其他page table的細節。
  1.     279         add     r0, r4, #PAGE_OFFSET >> 189 U2 G/ _" Z3 @% d2 I9 o) C4 Q
  2.     280         orr     r6, r7, #(PHYS_OFFSET & 0xff000000). K+ T6 D3 r: r$ g/ L! \
  3.     281         .if     (PHYS_OFFSET & 0x00f00000)
    + F4 [. I. J- o$ |& Z& G
  4.     282         orr     r6, r6, #(PHYS_OFFSET & 0x00f00000)4 V" }' K* M8 {9 ?4 F( S9 V+ _
  5.     283         .endif
    8 B1 r: g; [& O6 ]: j3 ^# f! o
  6.     284         str     r6, [r0]
    0 y$ ^8 r, L/ U" u5 L
  7.     327         mov     pc, lr
複製代碼
附帶一提,我們這邊省略了一些用ifdef包起來的程式碼,像是一開始會印一些output message的程式碼(line286~326)。
14#
 樓主| 發表於 2008-10-22 20:37:08 | 只看該作者
自create page table返回後,我們偷偷看一下接下來的程式碼,3 q/ O5 n; _+ \2 B
line 99, 將switch_data擺到r135 z8 A7 f" w7 V* J$ R! I
line 101, 將enable_mmu擺到lr% W/ _. q9 u7 T0 T4 g& Y% X- m
line 102, 將pc改跳到r10+PROCINFO_INITFUNC的地方去
1 {! u4 N& {6 B, c" t6 g% Y6 U; k% d, d) c% c$ A
其實這邊有點玄機,switch_data和enable_mmu都是function,結果位址都只是被存起來,並沒有直接跳過去執行。其實,雖然程式碼是順序switch_data->enable_mmu->proc init function,但其實執行的順序會是 procinfo 的init function-> enable_mmu -> switch_data 。至於,為什麼要這樣寫的原因?就不清楚了,也還沒仔細推敲過。
, S) Y# h! d. p. l& }* K* j! \( X! O" p3 h# e
switch_data最後就會跳到大家都很熟悉的start_kernel(). 詳細要賣個關子,最近會忙一下,得要過一陣子才能貼文∼  
  1.      99         ldr     r13, __switch_data              @ address to jump to after0 q9 P" Y# c: d1 U$ A
  2.     100                                                 @ mmu has been enabled( z& l! T: n3 B! \" f
  3.     101         adr     lr, __enable_mmu                @ return (PIC) address9 L% {& \7 T. F
  4.     102         add     pc, r10, #PROCINFO_INITFUNC
複製代碼
15#
 樓主| 發表於 2009-7-4 01:09:36 | 只看該作者
老店重新開張~
+ E6 ^# X9 A7 |2 a5 T* t4 b0 d/ q6 `' f5 F
花了一些時間把舊的貼文整理到一個blog
7 f, n  ^- W  S/ \  Q: i5 e* I有把一些敘述修改過
# W9 N: l' Z" M  j, q* u7 A; g5 v希望會比較容易集中閱讀9 U& t8 |) e8 i' Y3 C% a3 {
目前因為某些敘述不容易
1 B( Q. ~1 G" B5 ~3 u$ c5 F還是比較偏向筆記式而且用字不夠精確6 v$ V; U$ A! \: s/ w3 m! J& W
希望之後能夠慢慢有系統地整理
0 n  I- b; @/ C) H大家有興趣的話! X! T" O* F( I" ^; _; c
可以來看看和討論
: I: b1 r# D4 D; U8 R3 \/ nhttp://gogojesseco.blogspot.com/4 o+ I. f8 c- ?" ]! D

- f! [( J$ f# W0 I" m) f6 U以後可能會採取  先在chip123貼新文章
, s1 ]6 n% P* B/ L$ D$ H& t慢慢整理到blog上的方式
( K8 x. P2 V8 I2 l$ `! `$ P因為chip123比較方便討論 =)
1 L( h) N0 h9 R- c. k6 ~; U6 ^/ {blog編輯修改起來比較方便& z& o9 E! n$ m$ m4 c
閱讀也比較集中   大家可以在這邊看到討論; F; \3 ]( h' f( O' Z: C& l% U+ Y) L
然後在blog看到完整的文章 (類似BBS精華區的感覺)

評分

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

查看全部評分

16#
 樓主| 發表於 2009-7-15 17:07:03 | 只看該作者
隔了很長一段時間沒update
& P5 P% w3 ]; O" b: o3 J4 z/ t之前程式碼走到 ./arch/arm/kernel/head.S 的 line 99 附近
  1.      99         ldr     r13, __switch_data              @ address to jump to after
    3 S5 F* v% H% c
  2.     100                                                 @ mmu has been enabled& a9 j# ~' }) g# |( ~  D9 c2 O  u: V
  3.     101         adr     lr, __enable_mmu                @ return (PIC) address
    0 a5 q% A8 ^4 k; r4 n
  4.     102         add     pc, r10, #PROCINFO_INITFUNC
複製代碼
line 99, 將__switch_data放到r13。(留作之後用)
5 n& g% i* C3 ~2 s% }. Jline 101, 將__enable_mmu的addr放到lr。(留作之後用). ]( [0 O9 x' I' F: f5 s7 a
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, #function6 q+ c% D) m# |( {  j8 g  x
  2. 374 __arm926_setup:
    + |3 l( Z8 i7 p
  3. 375         mov     r0, #0% q7 ~' [2 x. e  H
  4. 376         mcr     p15, 0, r0, c7, c7              @ invalidate I,D caches on v4
    # X2 K8 s- P2 I& h- H' Q" F* m
  5. 377         mcr     p15, 0, r0, c7, c10, 4          @ drain write buffer on v4
    . l5 G% H- J( U
  6. 378 #ifdef CONFIG_MMU
    - w3 G: o2 a$ D
  7. 379         mcr     p15, 0, r0, c8, c7              @ invalidate I,D TLBs on v47 b, P8 \% k) K. i
  8. 380 #endif
    2 V' g6 L$ u$ M) K2 f' b9 O! V
  9. 3 s, V, L: J. m- b6 J
  10. 388         adr     r5, arm926_crval
    $ {+ k5 ]! r% P) z/ `% {
  11. 389         ldmia   r5, {r5, r6}, x% [, u0 J- U, [( e5 i. R
  12. 390         mrc     p15, 0, r0, c1, c0              @ get control register v4) U% p7 }8 U& n& l' f/ ]
  13. 391         bic     r0, r0, r5; ?: Y+ I$ ?( f& c7 k! g: P
  14. 392         orr     r0, r0, r6
    - z! ^3 a: O$ B/ h

  15. . H. `2 k5 u; w7 b  h* e
  16. 396         mov     pc, lr
    $ ]- J9 [: A7 t- R" c3 a' F4 E8 B* H
  17. 397         .size   __arm926_setup, . - __arm926_setup
複製代碼
這邊的程式碼就跟CPU有很大的相依性,$ @% A' G6 m" v7 w# Y
line 375~380, 主要就是invalidate CPU的I&D cache和清空write buffer。
- ?  Q- N4 u2 I2 H# u+ G, ]line 388~392, 把cp15的設定讀出來,並且將一些預設狀態做好運算放到r0。(預設值從arm926_crval這邊可以取得。)  w  L" ]% Q  `& E( C/ e
line 396, 直接把pc跳到lr,因為我們之前已經將lr = enable_mmu,所以直接跳過去。
17#
 樓主| 發表於 2009-7-15 17:29:45 | 只看該作者
  1. 155 __enable_mmu:
    ; m8 E8 X$ {/ T+ b7 G$ P
  2. 170         mov     r5, #(domain_val(DOMAIN_USER, DOMAIN_MANAGER) | \
    * u1 F3 }6 s2 v) U7 r8 f) ?% A# P
  3. 171                       domain_val(DOMAIN_KERNEL, DOMAIN_MANAGER) | \
    ! y) K6 S- \9 ^. x
  4. 172                       domain_val(DOMAIN_TABLE, DOMAIN_MANAGER) | \" z! N) c. N$ G  }- ~$ s8 I% `+ Q
  5. 173                       domain_val(DOMAIN_IO, DOMAIN_CLIENT))1 j( x/ O5 g! _* Q) L/ p
  6. 174         mcr     p15, 0, r5, c3, c0, 0           @ load domain access register
    : ?/ v- h( U  @: J5 Y  \
  7. 175         mcr     p15, 0, r4, c2, c0, 0           @ load page table pointer
    - G8 v) s9 o8 d2 o4 S
  8. 176         b       __turn_mmu_on
    % q4 r% w. }$ B5 ~# g( x0 z& t
  9. 177 ENDPROC(__enable_mmu)
複製代碼
line 170~174,設置好domain access。(domain access可以先當成設置access的權限,或許以後可以寫詳細的文章說明)% e7 y! j; l4 q* o7 L4 R9 `
line 175~176,將create好的page table開頭丟給mmu。跳到turn_mmu_on
  1. 191 __turn_mmu_on:
    + B4 u6 F3 h% |( W  R) F
  2. 192         mov     r0, r0
    - z+ D* I2 Y1 l+ d: F
  3. 193         mcr     p15, 0, r0, c1, c0, 0           @ write control reg
    4 ^. Z% t6 \* p7 f4 \
  4. 194         mrc     p15, 0, r3, c0, c0, 0           @ read id reg7 u5 m3 O" n* ~/ \8 \; s* n4 k3 u
  5. 195         mov     r3, r3
    + _8 I' s! q6 s! X% V
  6. 196         mov     r3, r38 \4 X+ K3 i, J# o$ l* [! f
  7. 197         mov     pc, r13$ c, l9 R( K8 ~* X/ x: t
  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:4 S3 E9 L5 p2 y3 d" ^8 _
  2. 19         .long   __mmap_switched
    7 W9 c* _0 X. Y# y
  3. 20         .long   __data_loc                      @ r4
    # j  n- I8 N( W9 _
  4. 21         .long   _data                           @ r5* i+ \7 X$ @5 m, `
  5. 22         .long   __bss_start                     @ r6
    " a; }9 S" l% @7 R- E
  6. 23         .long   _end                            @ r7# |+ O! Q4 d2 d* Y: O# W" h. v
  7. 24         .long   processor_id                    @ r48 e0 }# _: B# T" f' I2 M
  8. 25         .long   __machine_arch_type             @ r5
    6 A/ w! S/ ^8 J0 P5 d5 O; S. C
  9. 26         .long   __atags_pointer                 @ r6% T3 b( A6 E, {9 |1 d
  10. 27         .long   cr_alignment                    @ r7! X  ^8 w: n% K- n$ h
  11. 28         .long   init_thread_union + THREAD_START_SP @ sp  d' ?7 t5 S, k# w9 p
  12. 29  }9 @) n5 c% a; _/ N
複製代碼
switch_data的第一行就是 __mmap_switched,所以我們直接看line 39。
18#
 樓主| 發表於 2009-7-15 17:30:00 | 只看該作者
  1. 39 __mmap_switched:7 c0 v* Y- d' C# H/ x/ z: A% n
  2. 40         adr     r3, __switch_data + 4
    ' c. G8 I+ z4 W3 r+ @0 R/ L  o
  3. 41' n6 r' a# I# J7 e% n* `# a
  4. 42         ldmia   r3!, {r4, r5, r6, r7}
    " F: c1 a9 B8 y8 {
  5. 43         cmp     r4, r5                          @ Copy data segment if needed
    " e2 b# R: s% G+ i$ c$ }6 Z
  6. 44 1:      cmpne   r5, r63 h# |5 m( d5 k, R/ v
  7. 45         ldrne   fp, [r4], #4
    $ N) f) s* P" H4 n
  8. 46         strne   fp, [r5], #4  [8 {) h" @; S% N! S' S* K( X& i
  9. 47         bne     1b9 d2 H) k! h3 C( A2 Y, E0 D5 {
  10. 480 V+ b' o9 U( e9 C# M8 b
  11. 49         mov     fp, #0                          @ Clear BSS (and zero fp)
      f7 l$ E4 k; q% G2 q8 |- Y
  12. 50 1:      cmp     r6, r75 c+ E- P3 {: [. f- H
  13. 51         strcc   fp, [r6],#4
    , i* R: [  f3 b9 z
  14. 52         bcc     1b
    ' w8 ^2 `% Y6 }8 c/ d+ X
  15. 53
    ' F/ M( p# C2 Q! I! v, E: e
  16. 54         ldmia   r3, {r4, r5, r6, r7, sp}
    % H, r. A. q2 C
  17. 55         str     r9, [r4]                        @ Save processor ID
    2 E$ T! L3 a& p# S1 _7 m" j
  18. 56         str     r1, [r5]                        @ Save machine type' G( l1 J) A" x$ c+ C! p
  19. 57         str     r2, [r6]                        @ Save atags pointer
    1 I, F# r7 a/ i( L
  20. 58         bic     r4, r0, #CR_A                   @ Clear 'A' bit
    / w1 i4 g/ A7 D
  21. 59         stmia   r7, {r0, r4}                    @ Save control register values
    % e! U3 W! Z0 _5 M
  22. 60         b       start_kernel0 E3 M2 Z  c# H& W
  23. 61 ENDPROC(__mmap_switched)
複製代碼
switch_data的第一行就是 __mmap_switched,所以我們直接看line 39。
# a- g1 M$ [2 t& u0 I0 q/ Xline 39,將__data_loc的addr放到r3- w+ a- b) I" [( e5 h
line 42,從r3的位址,連續讀取四筆資料到r4, r5, r6, r7' _3 o( @% p: H6 L/ Z: Z
line 43~47,看看data segment是不是需要搬動。
  @" x; h; F. D: ?line 49~52, clear BSS。% L3 f# u" b# f3 }  V
& t) s, p8 d: w2 k% _  Z: k
由於linux kernel在進入start_kernel前有一些前提必須要滿足:. v# S% y) `2 S  e# A
r0  = cp#15 control register  b4 B, M8 d7 g. E6 Y0 D
r1  = machine ID% H) `7 G, i, [8 O
r2  = atags pointer$ K+ _( I0 s& H" Z) F) m
r9  = processor ID
7 {5 R) g* g3 i+ B2 k% F( t  {; H  d% H
所以line 54~59就是在做這些準備。
) V( B: y+ L9 f9 [6 l) M' [: ~: k  ^最後呢? 我們就跳到start_kernel了。(而且還是用b start_kernel,表示我們不會再回來了)# ?4 T5 @% T$ ~$ _/ a1 O# t

; ?- P! \2 J# ]) G看一下start_kernel()在./init/main.c,終於跳出architecture specific的目錄,表示# x4 g& R- O# P! F% i8 D) T3 }
我們真正的開始linux kernel的初始化。
8 L: _0 x! z2 }6 @7 ~! m1 W像是 shedule init, console init, memory init, irq init等等都在start_kernel裡頭。. G9 r3 X$ T, Q+ s
到這邊之後,應該就可以深入linux kernel中,各項比較跟hardware不那麼相關的軟體部分。

評分

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

查看全部評分

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

本版積分規則

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

GMT+8, 2024-6-26 11:25 AM , Processed in 0.151019 second(s), 19 queries .

Powered by Discuz! X3.2

© 2001-2013 Comsenz Inc.

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