Chip123 科技應用創新平台

 找回密碼
 申請會員

QQ登錄

只需一步,快速開始

Login

用FB帳號登入

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

trace linux kernel source - ARM - 03

[複製鏈接]
跳轉到指定樓層
1#
發表於 2008-10-7 14:00:18 | 只看該作者 回帖獎勵 |倒序瀏覽 |閱讀模式
到目前為止,我們已經進展到kernel幫自己relocate完,並解將自己解壓縮到一個地方要準備開始執行,那疑問來了?到底是跳到哪裡去了,因為./compressed/head.S最後一行居然是" K1 A* [5 O4 L8 S# T
『mov pc, r4』% D: ^2 Z6 ]! d3 I# Y3 b7 k' k
r4只代表了解壓縮完後kernel的位址,那究竟整包kernel編譯的時候,哪個function哪個東西被放在最前面咧?!( h! J6 K6 h+ k
% W, @- W4 }- K1 H( P
所以我們又必須開始找於kernel link的時候是怎麼被安排的,有了前面的基礎,我們可以從Makefile知道程式碼如何被編譯。至於link上的細節,例如有那些section和section先後順序等等,可以從 lds 檔來規範。  s! ^7 |& q  \! s

9 [) q8 V# l% J$ c* F有興趣的人可以看一下 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)
% q: s( t* ?  ^9 I我們可以發現第一個section是『.text.head』,裡頭的_stext從目前的位置開始放。
9 X! G- B+ b& R* |8 G& r0 g於是我們曉得只要找到屬於.text.head這個section,並且是_stext這個symbol的程式碼,就是解壓縮完後的第一行程式碼。
  1.      26     .text.head : {, v+ H# Z# R5 {- e2 q) O! P/ C
  2.      27         _stext = .;
    - M, A7 y# E1 X3 s
  3.      28         _sinittext = .;- L5 a) ~- ?- }2 t! L
  4.      29         *(.text.head)0 s1 B! J) N8 Z4 t
  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* W9 x9 b" T, [- y; P
  2.      78     .type   stext, %function
    4 H0 C- ?5 ]8 u
  3.      79 ENTRY(stext)& {. {4 o# A/ M- v$ z( v$ W$ F& K. v
  4.      80     msr cpsr_c, #PSR_F_BIT | PSR_I_BIT | SVC_MODE @ ensure svc mode
    3 L8 x4 Z# f9 c
  5.      81                         @ and irqs disabled
    , @0 V' N; u- `# y
  6.      82     mrc p15, 0, r9, c0, c0      @ get processor id
    ) o2 ]' C1 C2 ^8 L) S" G
  7.      83     bl  __lookup_processor_type     @ r5=procinfo r9=cpuid5 K! {- o7 |/ I2 H
  8.      84     movs    r10, r5             @ invalid processor (r5=0)?
    + x( x- R  R/ J$ r  t' ~! i+ s
  9.      85     beq __error_p           @ yes, error 'p'/ h' w4 I5 _* E: w$ l8 }
  10.      86     bl  __lookup_machine_type       @ r5=machinfo
    ! M' v+ {8 p0 T$ X
  11.      87     movs    r8, r5              @ invalid machine (r5=0)?
    ) F: e2 e% b0 `( w2 O' V
  12.      88     beq __error_a           @ yes, error 'a'
    . y3 f( a0 W; ?- g0 j: k
  13.      89     bl  __vet_atags
    - ]( E  _" m0 ~% U: W0 Q1 X
  14.      90     bl  __create_page_tables
複製代碼
既然找到了檔案,我們又可以開始繼續。
% C" A# }) I# o7 [' ^) l5 \; Z. `5 K, L/ m% u5 [6 q8 P
看了一下,程式碼多了很多bl的跳躍動作,看來會在各個function跳來跳去。
分享到:  QQ好友和群QQ好友和群 QQ空間QQ空間 騰訊微博騰訊微博 騰訊朋友騰訊朋友
收藏收藏 分享分享 頂 踩 分享分享
2#
 樓主| 發表於 2008-10-9 15:32:16 | 只看該作者
既然跳到真正的kernel開始跑,表示進入重頭戲,在進入kernel之前arm的平台有一些預設的狀況,也就是說arm kernel image會預設目前的cpu和系統的狀況是在某個狀態,這樣對一個剛要跑起來的OS比較決定目前要怎麼boot起來。% G7 f, l: z- H6 [8 o5 ]; Q' O
. `  O" a: h* H+ q2 Y- \
可以看一下comment,有清楚的描述。這也幫助我們了解為什麼decompresser的程式跑完之後,還要把cache關掉。
  1.      59 /*& _8 Q3 z4 u3 s1 m+ ^7 I
  2.      60  * Kernel startup entry point.
    * O$ Q0 q  j) {5 d1 r+ n$ Q# e
  3.      61  * ---------------------------! Y- O, j2 {- }8 g. G6 O& H
  4.      62  *
      n& `4 [$ U) y0 f. @! w% X* \3 p
  5.      63  * This is normally called from the decompressor code.  The requirements! X) D* z4 m+ W2 k1 Q( R: z$ N9 D; D
  6.      64  * are: MMU = off, D-cache = off, I-cache = dont care, r0 = 0,3 p+ ~5 b1 ^5 D+ y, M/ G
  7.      65  * r1 = machine nr, r2 = atags pointer.
複製代碼
基於以上的假設,當program counter指到kernel的程式的時候,就會從line 80開始跑。
; V; ?, |* r2 h; Uline 80, msr指令會把, operand的值搬到cpsr_c裡面。這是用來確保arm cpu目前是跑在svc mode, irq&fiq都disable。(設成0x1就會disable,definition在./include/asm-arm/ptrace.h)0 e% A, E7 q. d- \& ^
line 82, 讀取CPU ID到r9
& ]9 I- r+ S* M# g  |* L' j' ~" bline 83, 跳到 __lookup_processor_type 執行(bl會記住返回位址)。
  1.      77     .section ".text.head", "ax"  [4 s& ]5 o% T# y% W9 Z
  2.      78     .type   stext, %function
    % I& G9 c. c$ K. h+ u
  3.      79 ENTRY(stext): B; t/ ~0 c7 H  [; x* a' s
  4.      80     msr cpsr_c, #PSR_F_BIT | PSR_I_BIT | SVC_MODE @ ensure svc mode
    # m4 E3 H; F8 J5 ~
  5.      81                         @ and irqs disabled9 E& Q. ?- w6 u1 X# |% B
  6.      82     mrc p15, 0, r9, c0, c0      @ get processor id
    5 h1 S/ v& X  S
  7.      83     bl  __lookup_processor_type     @ r5=procinfo r9=cpuid
複製代碼
接著會跳到head-common.S這個檔,
3 k/ I5 I; M8 [4 p; Z& C: sline 158, (3f表示forware往前找叫做『3』label)將label 3的address放到r3。
/ U" q( r5 \# o" Eline 159, 將r3指到的位址的資料,依序放到r7, r6, r5.(ldmda的da是要每次都位址減一次)( s: m- F1 D' t' r, H
line l60, 161, 利用真實得到位址r3減去取得資料的位址得到一個offset值,這樣可計算出r5, r6真正應該要指到的地方。4 i1 a4 a: g3 p' S9 i8 u/ G
line 163~169,這邊的程式應該不陌生,就是在各個CPU info裡面找尋對應的structure。找到的話就跳到line 171,返回head.S
7 T* R5 x5 q& X* [' Kline 170, 找不到的話,r5的processor id就放0x0.表示unknown id。7 L4 g; p6 r/ B! k* ^; G- e) b3 ^
4 I' f6 x2 T/ j9 L0 z
__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, %function8 R, q+ R* e3 d% G
  2.     157 __lookup_processor_type:/ X" |9 Z7 S- a# ^: e. g0 W; ]
  3.     158     adr r3, 3f3 A: W: c/ ?/ f9 m, X5 r. g
  4.     159     ldmda   r3, {r5 - r7}
    5 J: D- @  d' C
  5.     160     sub r3, r3, r7          @ get offset between virt&phys% U) M" U  q  t9 Z8 l/ A4 f
  6.     161     add r5, r5, r3          @ convert virt addresses to
    5 H" z6 u1 s/ ~
  7.     162     add r6, r6, r3          @ physical address space* k; X9 B8 g* U
  8.     163 1:  ldmia   r5, {r3, r4}            @ value, mask
    # e- X0 Z( x3 G
  9.     164     and r4, r4, r9          @ mask wanted bits
    & t3 Z* i* b5 X
  10.     165     teq r3, r4
    ) M- Y5 O9 Z. R+ n) |+ Z7 H5 l
  11.     166     beq 2f/ [) U8 G' d: ]! B0 _" o
  12.     167     add r5, r5, #PROC_INFO_SZ       @ sizeof(proc_info_list)
    ) q, V$ ^7 }0 F- m( J, @' P' s
  13.     168     cmp r5, r6
    % h/ ?. Z/ z( Q& n
  14.     169     blo 1b
    8 m: V7 S  k2 L$ Z
  15.     170     mov r5, #0              @ unknown processor
    . |: r: Q$ h7 x/ d: `; j. @! i+ \2 D
  16.     171 2:  mov pc, lr
    7 N9 _' `, F# i! L4 f

  17. % ?6 b4 \+ L9 |. Z; [  |
  18.     187     .long   __proc_info_begin
    " {- ]0 D2 Z: y* T1 R' k
  19.     188     .long   __proc_info_end
    9 b; i( w* a' {- t9 A
  20.     189 3:  .long   .% K3 L* D% P2 B% k" d/ ^7 h& X5 e  g3 Y
  21.     190     .long   __arch_info_begin
    1 W2 R9 r. {1 ^/ V6 `
  22.     191     .long   __arch_info_end
複製代碼
跳了很多檔案,建議指令和object code如何link的概念要有,就會很清楚了。
3#
 樓主| 發表於 2008-10-13 17:20:35 | 只看該作者
我們從 head-common.S返回之後,接著繼續看。' U3 C! v- F5 Z/ |# q6 D# \

- A% P2 ^6 x- b, z7 n3 a& }- [; i. eline 84, movs的意思是說,做mov的動作,並且將指令的S bit設起來,最後的結果也會update CPSR。這個指令執行的過程當中,會根據你要搬動的值去把N and Z flag設好。這個是有助於下個指令做check的動作。- Y% I7 A& ^6 Q3 L5 v- p
line 85, 就是r5 = 0的話,就跳到__error_p去執行。0 J* p3 Q  W2 C1 C$ y
line 86, 跳到__lookup_machine_type。原理跟剛剛找proc的資料一樣。
  1.      83         bl      __lookup_processor_type         @ r5=procinfo r9=cpuid
    , W, h8 A6 y: }( k
  2.      84         movs    r10, r5                         @ invalid processor (r5=0)?6 E! x  Q5 M' H/ y
  3.      85         beq     __error_p                       @ yes, error 'p'9 Y8 _( S3 B: ~6 t% M) {' y8 R
  4.      86         bl      __lookup_machine_type           @ r5=machinfo
複製代碼
看得出來跟proc很像,有個兩個小地方是
0 a* C' j7 E6 D- E
0 T" U4 ^  h5 ]. v1 E1. line 207,用ldmia不是用ldmda。原因就在於存放arch 和 proc info 的位址剛好相反。一個在lable3的上面。一個在的下方。設計上應該是可以做修改的。) `+ L2 o  K! F- G
6 j8 h2 d5 t% J% Q2 V' Q
2. arch定義的方式是透過macro,可以先看 ./include/asm-arm/mach/arch.h。裡頭有個 MACHINE_START ,這邊會設好macro,到時候直接使用就可以把arch的info宣告好。 例如 ./arch/arm/mach-omap1/board-generic.c
  1. /* macro */( B" N6 Q0 ]; v2 e) H$ r& I$ T
  2.      50 #define MACHINE_START(_type,_name)                      \
    5 k9 O. r1 P" D
  3.      51 static const struct machine_desc __mach_desc_##_type    \
    2 k* n! K6 m. e0 q' p9 Y( M
  4.      52  __used                                                 \
    ( G% }9 z0 a+ r0 y1 t5 Z
  5.      53  __attribute__((__section__(".arch.info.init"))) = {    \
    * h9 J: Q: u" T; p. Q
  6.      54         .nr             = MACH_TYPE_##_type,            \! D) [4 j) S& F5 T- M. }3 x, t
  7.      55         .name           = _name,
    2 c+ I& K$ T( \
  8.      56
    . R6 K$ y0 w$ L( v* P  S, ?
  9.      57 #define MACHINE_END                             \! f, z: V0 H5 I* M' M  ]
  10.      58 };( ]2 Q6 v: x$ e8 {
  11.      /* 用法 */+ H9 _* Q  P9 c7 ]' i
  12.      93 MACHINE_START(OMAP_GENERIC, "Generic OMAP1510/1610/1710")
    : j# G$ ]; Q- s! [1 D
  13.      94         /* Maintainer: Tony Lindgren <tony@atomide.com> */
    6 i2 Y- x% W: g* Y) x% t
  14.      95         .phys_io        = 0xfff00000,/ e( p# M  G% J  u3 s! d2 P' T
  15.      96         .io_pg_offst    = ((0xfef00000) >> 18) & 0xfffc,
    * ]. f% P7 `; e" [, S5 d1 Y8 P
  16.      97         .boot_params    = 0x10000100,
    ' a& @  u7 |7 n9 N
  17.      98         .map_io         = omap_generic_map_io,' ~3 [% O3 F- g$ e/ M% }
  18.      99         .init_irq       = omap_generic_init_irq,
    , L. J( Z# s$ X6 ~$ A8 k) K
  19.     100         .init_machine   = omap_generic_init,2 K! e0 f$ _# C2 w" s
  20.     101         .timer          = &omap_timer,3 N5 g3 c2 m6 R
  21.     102 MACHINE_END, z& ^7 g7 a3 C' h. V
  22. 3 `: y7 \5 i. s# X% m$ b
  23.     /* func */
    ! q- w5 e8 L( |9 U. P$ U
  24.     204         .type   __lookup_machine_type, %function0 `% X% s' X7 `( x9 m1 P2 `
  25.     205 __lookup_machine_type:! p1 E( Y/ c- M$ L
  26.     206         adr     r3, 3b
    % W* [7 ?% ~8 c" a
  27.     207         ldmia   r3, {r4, r5, r6}" R2 d6 O: a( X# k
  28.     208         sub     r3, r3, r4                      @ get offset between virt&phys
    : ^) v" u* E4 _" ~# b8 R. {$ G
  29.     209         add     r5, r5, r3                      @ convert virt addresses to
    . _. L" N$ b& R/ B6 t" g
  30.     210         add     r6, r6, r3                      @ physical address space8 @) P7 Z$ w7 Z
  31.     211 1:      ldr     r3, [r5, #MACHINFO_TYPE]        @ get machine type
    % N7 l( n, G$ Q2 y$ {( p# j
  32.     212         teq     r3, r1                          @ matches loader number?
    ) x& _; ^) O# E% \( H- y9 E
  33.     213         beq     2f                              @ found
    1 v& E1 L4 b& l% [- d! V7 R: ^
  34.     214         add     r5, r5, #SIZEOF_MACHINE_DESC    @ next machine_desc2 \5 c7 X' ~! X" d
  35.     215         cmp     r5, r6
    ! [8 B5 {& E  x3 W6 D1 j2 i# w
  36.     216         blo     1b
    ) M3 T3 \, Z" B% g% `. h$ |
  37.     217         mov     r5, #0                          @ unknown machine+ v5 k: h' p3 F7 F' f- t+ Z0 b
  38.     218 2:      mov     pc, lr
複製代碼
4#
 樓主| 發表於 2008-10-13 17:56:46 | 只看該作者
接著我們又返回到head.S,
% O  w1 j2 e$ Y5 b; s  aline 87~88也是做check動作。
( X" @- x5 R8 o, D. V* c, v' f6 Gline 89跳到vet_atags。在head-common.S
  1.      87         movs    r8, r5                          @ invalid machine (r5=0)?
    : w7 a+ m" o4 {0 W) h6 ]4 T
  2.      88         beq     __error_a                       @ yes, error 'a'
    ; u2 q4 P" w2 C
  3.      89         bl      __vet_atags, x/ B2 [+ I/ A! D8 i
  4.      90         bl      __create_page_tables
複製代碼
line 245, tst會去做and動作。並且update flags。這邊的用意是判斷位址是不是aligned。( [, |; u$ ~/ t
line 246, 沒有aligned跳到label 1,就返回了。
/ O8 P8 O4 T; O" A/ B1 Bline 248~250, 讀取atags所在的address裡頭的值到r5,看看是否不等於ATAG_CORE_SIZE,不等於的話也是返回。
' \" `. ]$ W! l( cline 251~254, 判斷一下第一個達到的atag pointer是不是等於ATAG_CORE。如果正確的話,等一下要讀取atag的資料,才會正確。
' [. k8 ?3 f( O(atag是由bootloader帶給linux kernel的東西,用來告知booting所需要知道的參數。例如螢幕寬度,記憶體大小等等)
  1.      14 #define ATAG_CORE 0x54410001
    $ I- S8 @( q) w4 w% i! p
  2.      15 #define ATAG_CORE_SIZE ((2*4 + 3*4) >> 2). K7 d2 x1 s# S" I

  3. ( L  u  P- @! t# w3 L
  4.     243         .type   __vet_atags, %function
    , n, h* F7 u5 i0 {
  5.     244 __vet_atags:( k4 \4 G# P/ {
  6.     245         tst     r2, #0x3                        @ aligned?- _8 F. U- W9 X1 s
  7.     246         bne     1f6 I6 P5 g& E3 n. a
  8.     247
    % M- R2 Q- H/ z8 `
  9.     248         ldr     r5, [r2, #0]                    @ is first tag ATAG_CORE?. g8 w7 ^' |  N" g/ p4 s9 x: }
  10.     249         subs    r5, r5, #ATAG_CORE_SIZE" Q1 F. z8 b6 N+ J+ B
  11.     250         bne     1f, ^1 |' x0 D/ P  M
  12.     251         ldr     r5, [r2, #4]
    7 o' Q$ b, C9 M8 @
  13.     252         ldr     r6, =ATAG_CORE
    0 a$ \1 e/ [; S1 l* t
  14.     253         cmp     r5, r6
    " J5 h# M" W8 D/ Z* T
  15.     254         bne     1f' s/ _* r; H4 p+ W
  16.     255
    7 h0 D$ Q8 G; g" M
  17.     256         mov     pc, lr                          @ atag pointer is ok: \: \) O. |* E' C) p
  18.     257
    - n: e# J* B4 Z, r$ L
  19.     258 1:      mov     r2, #0
    % ~' h5 |* J0 Y7 G* d2 K8 ~3 N, f
  20.     259         mov     pc, lr
複製代碼
接著我們又跳回去head.S。  . N% ?2 \6 `; J" R  t
line 90,又跳到 __create_page_tables。   (很累人....應該會死不少腦細胞)/ ?7 A1 q7 I9 X( }1 Y" W
哇!page table?!!如雷貫耳的東西,不知道會怎麼做。剛剛偷看了一下,@@還蠻長了,先到這邊好了,下次在繼續寫。
5#
 樓主| 發表於 2008-10-14 12:13:51 | 只看該作者
由於code看起來似乎越來越難解釋,會需要在檔案之間跳來跳去。建議一些基礎的東西可以再多複習幾次(其實是在說我自己 ),閱讀source code上收穫會比較多。例如:! c3 P+ g/ ^' r( u8 f1 _

0 ~' q, K1 e6 v) @1. arm instruction set - 這個最好每個指令的意思都大略看過一次,行有餘力多看幾個版本,armv4, v5 or v6。. L- _; `8 |8 L; `
2. compiler & assembler & linker - toolchain工具做的事情和功能,大致的流程和功能要有概念。% h' Q# f$ j: o( k% W5 r
3. Makefile & link script - 這兩個功能和撰寫的方式要有簡單的概念。: [  ~) X& Z8 [

" q) w: Z( }; P' O4 n7 m以上1是非常重要的重點,2&3只要有大致上的概念就可以,因為trace code的時候,有時需要跳到Makeflie&Link script去看最後object code編排的位址。5 [0 n( ]8 U" H" o- [( i" v' x% E
$ H$ ~' V4 v6 w# l$ J/ _
由於我們trace到了page table這個關鍵字,在開始之前,稍微簡短的解釋,為了幫助了解,儘量用易懂的概念講,有些用詞可能會和一些真實狀況不同,但是懂了之後,應該會有能力分辨,至於正式而學術上解說,很多書上應該都有,或是google一下就很多啦∼
6#
 樓主| 發表於 2008-10-14 12:14:47 | 只看該作者
page table本身是很抽象的東西,尤其是對所謂的 user-mode application programmer 來說,大部分的狀況它是不需要被考慮到的部份,但是他的產生卻對os和driver帶來了很多影響。我們從一個小小的疑問出發。6 V$ f6 {8 s% V

2 @  L, z' ?  P『產生page table到底是要給誰用的?』
& j. {' N& ?6 |! }$ p, i" p* `* {2 Z$ }- K0 b
其實真正作用在它上面的H/W是MMU,一旦CPU啟用了MMU,當cpu嘗試去記憶體讀取一個operand的時候,cpu內部打出去的位址訊號都會先送到mmu,mmu會拿著這個位址去對照page table,看看這個位址是不是被轉換到另外一個位置(還會確認讀寫權力),最後才會到真正的位址去讀寫。( e& ^" F" G; r
7 X1 s* F. J1 {* E
這樣來看CPU其實一開始打出去的位址訊號其實不是最後可以拿到資料的位址,我們稱為virtual address(va),mmu打出來的位址才能真正拿到資料,所以我們把mmu打出去的位址稱為physical address(pa)。那用來查詢這個位址對照關係的表格,就是page table。
& v( K$ j7 K6 O
' f/ i. P. W6 `7 ^5 W到這邊我們有一個簡單的概念。來想像一個小問題,一個普通的周邊(例如你的顯示卡),因為他並不具有MMU功能,hw只看得懂pa,但是CPU卻是作用在va,假如我想去對hw的控制暫存器做讀寫,到底要用va還是pa?
7#
 樓主| 發表於 2008-10-14 12:15:51 | 只看該作者
這時,寫driver的人就必須要小心,設定給硬體看的位址必須要先轉成pa(像是設定DMA),給CPU執行的程式碼要使用va,這對一開始嘗試寫driver但是又不瞭解va pa之間的差別的人,常常產生很多疑問。- ?1 O- N& c/ x2 G: v

/ @( }2 y9 m, Z1 R% F現在我們回頭想想OS,既然我們跑到create page table,可以預期的是他想要將MMU打開,因此希望預先建立起一個page table,讓MMU知道目前os想規劃的位址對應和讀取權力是如何被安排的。所以os必須考慮到現在的系統究竟是長怎樣?應該要如何被安排?是不是有那些要被保護?好吧∼因為我們完全對os不了解,也不知道該安排什麼,只能祈禱在trace code完後,可以找到這些問題的答案,或者發現一些沒想到的問題。
& i4 T; ^1 G' Y( T* W  Q+ V1 i1 ^/ s6 \* m2 Y8 z( L3 ^
知道了page table的大致上的功能,下篇就可以專心的研究這個table的長相,和它想規劃出的系統模樣。
; X! T' S! W# e/ h1 G3 C6 I( A+ [& \! C* K# y& @) u
p.s. 字數限制好像變短了。   (看來很難寫)
8#
 樓主| 發表於 2008-10-14 13:56:17 | 只看該作者
由於字數限制挑整,用詞儘量精簡,以便可以貼必要source。另外,以後寫到一段落,考慮收集成一篇完整的文章,這樣應就不會因為用回覆的方式,造成閱讀斷斷續續的問題。希望有人對文章呈現方式有想法的話,可以跟我講。
9#
 樓主| 發表於 2008-10-14 13:57:39 | 只看該作者
現在,讓我們跳入create_page_tables吧∼
1 A( D; r& Q) G8 l0 I6 [( h" eline 216,會跳到pgtbl的macro去執行,其實只是載入一個位址。
8 S( M( X/ [2 x  w3 R$ Y, r
0 ^! Q" t2 w( k1 X. |7 N. O只是這個位址因為你硬體規劃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 */
    . Z. }- D2 p' \# ~
  2.      95 textofs-y       := 0x000080005 S+ V' y+ t: D$ |2 a$ x  I7 p1 y
  3.     152 TEXT_OFFSET := $(textofs-y)
複製代碼
  1. /* include/asm-arm/arch-omap/memory.h */! B- E9 T) p- W4 h; l7 o# h8 T: {; W
  2.      40 #define PHYS_OFFSET             UL(0x10000000)
    0 P& i1 N# q# a0 f. h
  3. * v, G1 n# \1 U9 [7 o6 F
  4.      /* arch/arm/kernel/head.S */
      w9 B3 }+ ^% K8 V" a
  5.      29 #define KERNEL_RAM_VADDR        (PAGE_OFFSET + TEXT_OFFSET)
    ; e4 N# f) C1 E% |, _
  6.      30 #define KERNEL_RAM_PADDR        (PHYS_OFFSET + TEXT_OFFSET)# r/ j' s3 ]5 j4 r2 Y
  7. ; V0 X; y6 y8 G# y& P
  8.      47         .macro  pgtbl, rd
    - I8 q+ ~5 u& l- p. r/ w
  9.      48         ldr     \rd, =(KERNEL_RAM_PADDR - 0x4000)
    4 A! F- L1 k+ N- k- k& i
  10.      49         .endm
    - K4 U5 A* T* |9 p/ T

  11. 1 p$ }  q0 x4 J1 M) I; D8 g) g- y" m
  12.     216         pgtbl   r4                              @ page table address
複製代碼
10#
 樓主| 發表於 2008-10-14 14:16:53 | 只看該作者
得到pg table的開始位置之後,當然就是初始化囉。$ f2 I% Y( q- O5 S' d3 M* m
line 221, 將pg table的base addr放到r0.: t1 I3 s2 E1 d
line 223, 將pg table的end addr放到r6.: r) N' {5 l" o( S  e
line 224~228, 反覆地將0x0寫到pg table的區段裡頭,每個loop寫16bytes. (4x4),直到碰到end,結果就是把它全部clear成0x0.
  1.     221         mov     r0, r4
    + z6 |+ H/ D/ ]% ?$ e" f8 J5 }( t
  2.     222         mov     r3, #0
    5 R, m( f/ [3 `: m; b
  3.     223         add     r6, r0, #0x4000
    . j9 i" x2 ?0 V' N1 a2 C8 E/ c
  4.     224 1:      str     r3, [r0], #42 q1 g% K9 H" G
  5.     225         str     r3, [r0], #4
    ( Q. |& n0 t9 P1 y. A6 U& h
  6.     226         str     r3, [r0], #4
    7 E$ L5 v. ~! W% R+ F5 W$ i
  7.     227         str     r3, [r0], #42 _8 o+ G) T" R% l
  8.     228         teq     r0, r6
    ! c* m, b" {4 J# |5 y8 A
  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 | 只看該作者
問題怎麼填值??9 d/ E: ?  G3 w$ C9 A% C
拿出ARM的手冊,翻到MMU章節。一看發現page有很多種,還得分first level和second level。 ; ^/ v- o* _2 j# _7 m6 @

0 N) ^; s3 K+ a8 X9 F念書時的印象,是從1st level在去查2nd level,先看怎麼設定1st level (不知以前老師有沒有亂教)
' a6 w( Z! Z1 p/ a) o1. [31:20]存著section base addr/ m: K" I& ^, I/ _% E+ W
2. [19:2]存著mmu flags
6 @. i7 \. Y4 I( `5 y  r( q0 @3. [1:0]用來辨別這是存放哪種page, 有四種:
5 I( b0 H% J$ ]' _   a. fault (00) b. coarse page (01) c. section (1st level) (10) d. fine page (11)
4 z' ]& e  E7 u# o; l4. pg tabel資料要存放到 [31:14] = translation base, [13:2] = table index , [1:0] = 0b00 的位址
3 N0 ]( k5 z) }1 {6 S* F' C
% Y( S/ U( L. L6 i; r/ ?9 f來看code是怎麼設定。
4 u) \6 S9 i! u- X
$ s# X; _1 O" Q: A# Hline 239, 將pc的值往右shift 20次放到r6.這是取得前20高位元的資料,有點像是 1.。
% }" m" {9 ^  n$ y9 r8 Eline 240, 將r6往左shift 20次之後,和r7做or的動作,剛好也是 2.提到的mmu flags和1.處理好的資料做整理。! M( O, J# @. _# h
所以前面兩個做完,就完成了bit[31:2]。
6 B: ~' q  X' q: Wline 241, 將r3的資料寫到r4+(r6<<0x2)的地方去,剛好是4.提到的位址。(lucky)
  1.     239         mov     r6, pc, lsr #20
    - o7 w& p  h7 h. O
  2.     240         orr     r3, r7, r6, lsl #20  L: l, d1 o  O0 C4 [6 ?% E+ U9 C) V$ ?+ Y; z
  3.     241         str     r3, [r4, r6, lsl #2]
複製代碼
p.s. 用pc值剛好可以算出當前的page。
12#
 樓主| 發表於 2008-10-22 19:47:03 | 只看該作者
最近又被釘上了,開始忙碌,大概沒太多時間貼文∼
; K  W6 D) `( e, E8 e1 `
) W# G, r! [2 o: q1 |/ y- O* Z, R上篇已經將pc所屬的page table entry(pte)設定好,接著繼續看+ V1 e# D' n( f
line 247, 248, 將KERNEL_START的位址往右shift 18算出pte的offset,(等於line239&241,239&241是先shift right 20然後shift left 2),並將剛剛r3的值設給pte。『!』的意思是會把address存到r0。5 w+ t0 `: x6 z9 K/ n3 H3 Z' a" D1 m; L6 N
, o  `0 D. R2 T9 t8 ]7 _
line 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
    # |2 f5 Z* [) g' `7 F9 M1 M8 ?
  2.     248         str     r3, [r0, #(KERNEL_START & 0x00f00000) >> 18]!
    , c4 \" x: R+ F+ p+ m: A4 K$ m
  3.     249         ldr     r6, =(KERNEL_END - 1)1 ]# W3 s1 t* d7 t1 ]" y/ D- I
  4.     250         add     r0, r0, #4
    * g% e5 ^4 N1 T- u' \' h; |- q8 t
  5.     251         add     r6, r4, r6, lsr #18* I. J5 G3 C" H) O8 X+ w- S# \) [
  6.     252 1:      cmp     r0, r64 n: d8 {4 r4 R
  7.     253         add     r3, r3, #1 << 20  f  l1 B2 {3 [. P+ @& E* P
  8.     254         strls   r3, [r0], #4/ S" r+ K0 z# b) H; E
  9.     255         bls     1b
複製代碼
13#
 樓主| 發表於 2008-10-22 20:24:58 | 只看該作者
line279,PAGE_OFFSET是規範RAM mapping完後的virtual address,我們將這個位址所屬的pte算出來放到r0。5 x8 P' Y. H5 p
line 280~283,將要 map 的physical address的方式算出來放到r6。
9 A! p9 D$ k: Y# X/ S0 Z8 _. Lline 284,最後將結果存到r0所指到的pte。
) S. n5 k$ p5 b9 E2 Q' j# T
# y' y6 R& P- y! d% v4 V% G- f以上三個動作,就是做好 ram 起頭的位址一開始map,還沒做完整塊map。由於我們目前的設定方式每塊是1MB,所以這邊是將RAM開始的1MB做map。
3 j3 A7 d1 g5 v1 i+ F; D2 ^# [* ~. @0 ~
line 327,返回,結束一開始的create page table的動作,我們可以看出其實並沒有做完整的初始化page table,所以之後應該會有其他page table的細節。
  1.     279         add     r0, r4, #PAGE_OFFSET >> 184 l" }4 d8 o5 _; y
  2.     280         orr     r6, r7, #(PHYS_OFFSET & 0xff000000)
    0 s5 [8 N" M: m
  3.     281         .if     (PHYS_OFFSET & 0x00f00000)0 N+ P0 @% y' q+ l0 [; N5 D
  4.     282         orr     r6, r6, #(PHYS_OFFSET & 0x00f00000); R$ U* u0 S' |2 m/ x( r; J
  5.     283         .endif
    8 T! T4 ?' [+ f' b# ^
  6.     284         str     r6, [r0]: v' U6 w. ~2 z$ Y6 r$ r5 t
  7.     327         mov     pc, lr
複製代碼
附帶一提,我們這邊省略了一些用ifdef包起來的程式碼,像是一開始會印一些output message的程式碼(line286~326)。
14#
 樓主| 發表於 2008-10-22 20:37:08 | 只看該作者
自create page table返回後,我們偷偷看一下接下來的程式碼,  L: x4 {$ k2 N# ]$ E
line 99, 將switch_data擺到r13
& r% ]3 }6 Z8 U  t  _" ~! \line 101, 將enable_mmu擺到lr
6 o/ E7 A& }( c. Tline 102, 將pc改跳到r10+PROCINFO_INITFUNC的地方去2 O, e% a2 B$ L/ U3 z
& H. s9 b9 s% K  s/ Z
其實這邊有點玄機,switch_data和enable_mmu都是function,結果位址都只是被存起來,並沒有直接跳過去執行。其實,雖然程式碼是順序switch_data->enable_mmu->proc init function,但其實執行的順序會是 procinfo 的init function-> enable_mmu -> switch_data 。至於,為什麼要這樣寫的原因?就不清楚了,也還沒仔細推敲過。 " _7 V2 Z/ V6 y# ~: O- }, P
5 ~2 ]' p7 Q7 t3 X
switch_data最後就會跳到大家都很熟悉的start_kernel(). 詳細要賣個關子,最近會忙一下,得要過一陣子才能貼文∼  
  1.      99         ldr     r13, __switch_data              @ address to jump to after
    : g8 R( t6 l3 l3 f) z
  2.     100                                                 @ mmu has been enabled
    7 _' _) K3 S* s5 ?) M, W9 j
  3.     101         adr     lr, __enable_mmu                @ return (PIC) address
    5 Q# x3 y% Q2 g) ~2 K- W5 u
  4.     102         add     pc, r10, #PROCINFO_INITFUNC
複製代碼
15#
 樓主| 發表於 2009-7-4 01:09:36 | 只看該作者
老店重新開張~' `; s( U) t1 i: d% J! \* \

9 `0 x) T1 |* O花了一些時間把舊的貼文整理到一個blog
3 s9 \. e# o( }) z4 H, P有把一些敘述修改過
4 p5 t6 Q( z" ~+ e# \+ b希望會比較容易集中閱讀
& y: f, T5 t+ U+ ?# y目前因為某些敘述不容易
2 ~% b  _* l3 e) L還是比較偏向筆記式而且用字不夠精確- X( G9 a  R0 a/ [, E& X
希望之後能夠慢慢有系統地整理
" }* S/ F. j$ f8 c( M- ]/ P, I2 V大家有興趣的話4 S4 j( o7 g& ]: {4 b" _+ z( T
可以來看看和討論
' j. b- e/ b: y& J* b' [http://gogojesseco.blogspot.com/& J7 w' p( P8 r
8 i1 {5 o; J. R7 t3 @7 r
以後可能會採取  先在chip123貼新文章6 ?# Z2 O% q3 h8 B- A
慢慢整理到blog上的方式
/ H/ R- e/ f% h6 X因為chip123比較方便討論 =)! s, z5 _9 S* W
blog編輯修改起來比較方便
6 J- q" q7 o" F+ V; i& N閱讀也比較集中   大家可以在這邊看到討論
! C6 _7 r4 O1 F1 R/ [然後在blog看到完整的文章 (類似BBS精華區的感覺)

評分

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

查看全部評分

16#
 樓主| 發表於 2009-7-15 17:07:03 | 只看該作者
隔了很長一段時間沒update, r% t7 C3 i/ f( A1 {
之前程式碼走到 ./arch/arm/kernel/head.S 的 line 99 附近
  1.      99         ldr     r13, __switch_data              @ address to jump to after
    1 w' D& H4 @4 E
  2.     100                                                 @ mmu has been enabled5 ~0 q0 P' R3 l2 E# y' P
  3.     101         adr     lr, __enable_mmu                @ return (PIC) address
    ( C+ L+ ^, K4 x" w' ^
  4.     102         add     pc, r10, #PROCINFO_INITFUNC
複製代碼
line 99, 將__switch_data放到r13。(留作之後用)5 J4 r) `* A; Q& [* l( l
line 101, 將__enable_mmu的addr放到lr。(留作之後用)
$ r" r' Q) ^; Z* j6 j/ iline 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, #function2 |& \: G& P, B% y8 X
  2. 374 __arm926_setup:+ x  V: \! z8 d9 N( `7 Q
  3. 375         mov     r0, #0
    4 N. B' e! j: M: G
  4. 376         mcr     p15, 0, r0, c7, c7              @ invalidate I,D caches on v4
    0 p' ~: V8 r4 V$ t
  5. 377         mcr     p15, 0, r0, c7, c10, 4          @ drain write buffer on v4
    0 ^9 L( q' {9 C2 d+ ^1 x. \0 g; w
  6. 378 #ifdef CONFIG_MMU
    : C% J8 F0 j6 Z9 y: c' r) k
  7. 379         mcr     p15, 0, r0, c8, c7              @ invalidate I,D TLBs on v4
    % `- O2 B  E# w2 ]! U- A
  8. 380 #endif
    3 n2 k$ ]& {8 i+ N' U1 _
  9. 8 n! f8 T* q* b1 a
  10. 388         adr     r5, arm926_crval0 z5 g7 s8 ]( t5 o1 z. D7 B
  11. 389         ldmia   r5, {r5, r6}
      J; V8 W5 t+ u4 @5 y9 v
  12. 390         mrc     p15, 0, r0, c1, c0              @ get control register v4
    4 M# f6 D5 ~6 \, i5 V/ g) ?1 }* y/ `
  13. 391         bic     r0, r0, r57 ^/ I7 e/ b+ W3 \2 w( K
  14. 392         orr     r0, r0, r6# S1 P8 Y+ f+ R
  15. # _6 R, [1 y& ?& O
  16. 396         mov     pc, lr
    , }- u- p$ U) g! G; ?
  17. 397         .size   __arm926_setup, . - __arm926_setup
複製代碼
這邊的程式碼就跟CPU有很大的相依性,& R5 k5 i- O$ U" J; f5 H" q
line 375~380, 主要就是invalidate CPU的I&D cache和清空write buffer。
2 x! r9 Y  y* O2 u3 ]; iline 388~392, 把cp15的設定讀出來,並且將一些預設狀態做好運算放到r0。(預設值從arm926_crval這邊可以取得。)+ L* s0 m. s3 U5 T
line 396, 直接把pc跳到lr,因為我們之前已經將lr = enable_mmu,所以直接跳過去。
17#
 樓主| 發表於 2009-7-15 17:29:45 | 只看該作者
  1. 155 __enable_mmu:, b' T9 L% `1 e
  2. 170         mov     r5, #(domain_val(DOMAIN_USER, DOMAIN_MANAGER) | \
    7 ^+ x9 m5 v4 b7 H9 h: ?. }  c+ H# }
  3. 171                       domain_val(DOMAIN_KERNEL, DOMAIN_MANAGER) | \
    6 x! u6 C# k8 N* C
  4. 172                       domain_val(DOMAIN_TABLE, DOMAIN_MANAGER) | \" @4 A/ R9 ]/ G5 l8 v4 E; W) F! Z
  5. 173                       domain_val(DOMAIN_IO, DOMAIN_CLIENT))/ V8 J( U9 _; o
  6. 174         mcr     p15, 0, r5, c3, c0, 0           @ load domain access register" l6 M4 ~7 `1 x; ~8 ^
  7. 175         mcr     p15, 0, r4, c2, c0, 0           @ load page table pointer
    ! ]6 V/ C5 ?5 i1 [
  8. 176         b       __turn_mmu_on% S6 S) H& g  v$ {5 ?3 j  g( V" b
  9. 177 ENDPROC(__enable_mmu)
複製代碼
line 170~174,設置好domain access。(domain access可以先當成設置access的權限,或許以後可以寫詳細的文章說明)1 D- x  M3 M& I. i* w/ t
line 175~176,將create好的page table開頭丟給mmu。跳到turn_mmu_on
  1. 191 __turn_mmu_on:; n4 x! _+ i4 j9 x2 p
  2. 192         mov     r0, r01 _5 m  I. U  `6 x2 j3 D4 c
  3. 193         mcr     p15, 0, r0, c1, c0, 0           @ write control reg1 X% n, p$ Z3 F4 m1 ]# n' k
  4. 194         mrc     p15, 0, r3, c0, c0, 0           @ read id reg
    6 N6 i: j' o# @) r! s/ t
  5. 195         mov     r3, r35 T' k% k9 K+ `$ C2 C
  6. 196         mov     r3, r3
    " m* H3 F1 S- B6 A  V
  7. 197         mov     pc, r13
    , E; W, c0 G; {. _9 D- Z" |5 f
  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:
    & b( K$ J4 r0 [! }1 I3 \( j5 u
  2. 19         .long   __mmap_switched6 |- ?5 O  {9 b6 b2 _* P5 F/ K# w
  3. 20         .long   __data_loc                      @ r4. o2 x7 A& \* t, E
  4. 21         .long   _data                           @ r52 r6 @7 j4 l& ^8 H: W+ s- I1 \
  5. 22         .long   __bss_start                     @ r6
    9 u! s% S+ m" Q" s; x% \$ W
  6. 23         .long   _end                            @ r7' {" F/ ]  A5 t' q" d/ N# |
  7. 24         .long   processor_id                    @ r4: U" f6 u6 f* t' \
  8. 25         .long   __machine_arch_type             @ r52 C) P3 M% \$ {7 V) N+ }5 b) g
  9. 26         .long   __atags_pointer                 @ r6
    1 B* p, q. z, \0 h  ^7 K
  10. 27         .long   cr_alignment                    @ r7/ N) t$ v8 x' Q
  11. 28         .long   init_thread_union + THREAD_START_SP @ sp
    . Q6 q9 z, W5 k; C) _' F
  12. 29+ E; `8 H3 m6 \+ \" P- u1 e
複製代碼
switch_data的第一行就是 __mmap_switched,所以我們直接看line 39。
18#
 樓主| 發表於 2009-7-15 17:30:00 | 只看該作者
  1. 39 __mmap_switched:  P" |; {" u# U% F) f" L, ^
  2. 40         adr     r3, __switch_data + 4
    , ]4 s7 N& f( C' m6 n
  3. 41
    # H& v% {# E. ?' _* u( u
  4. 42         ldmia   r3!, {r4, r5, r6, r7}
    & K8 v/ e! `" ~% [, x
  5. 43         cmp     r4, r5                          @ Copy data segment if needed
    # Y! A/ L0 ]; ~0 u* M
  6. 44 1:      cmpne   r5, r6
    ; [8 t; L' ~, L3 \
  7. 45         ldrne   fp, [r4], #4
    4 j& ~8 N7 k) D
  8. 46         strne   fp, [r5], #4
    % ]+ l& n1 L1 D0 l
  9. 47         bne     1b
    7 }4 v, l3 l5 W% N* F  _7 i
  10. 48: e: M0 i+ r# n7 Z
  11. 49         mov     fp, #0                          @ Clear BSS (and zero fp)  \! X( X: H% F; j. W* N8 F/ n- d
  12. 50 1:      cmp     r6, r7
    + e- @6 W! V# s: s/ W  H
  13. 51         strcc   fp, [r6],#4# y5 g2 z$ _! t8 f
  14. 52         bcc     1b4 W, a& y8 ]+ N4 m9 C
  15. 53
    2 k5 x7 }8 f8 ?( b0 A
  16. 54         ldmia   r3, {r4, r5, r6, r7, sp}# K. D' U) c: O, Y2 B
  17. 55         str     r9, [r4]                        @ Save processor ID( ~) M) v4 {" A* @9 d" J
  18. 56         str     r1, [r5]                        @ Save machine type* S) g3 M6 z8 T" C" z
  19. 57         str     r2, [r6]                        @ Save atags pointer
    8 I. ~: G' ~' [8 n
  20. 58         bic     r4, r0, #CR_A                   @ Clear 'A' bit
    ) E9 S- t# x, j" J8 h
  21. 59         stmia   r7, {r0, r4}                    @ Save control register values& ]' R. e4 W) L: ~( V9 I& a. M+ [) X
  22. 60         b       start_kernel
    & h& C2 @* J5 e8 ^
  23. 61 ENDPROC(__mmap_switched)
複製代碼
switch_data的第一行就是 __mmap_switched,所以我們直接看line 39。
/ k, O# v2 X6 kline 39,將__data_loc的addr放到r3
- h* k, l' g1 Gline 42,從r3的位址,連續讀取四筆資料到r4, r5, r6, r7
+ d9 @5 w& z: |line 43~47,看看data segment是不是需要搬動。7 c+ w# E* B& v  t3 }
line 49~52, clear BSS。' {9 |  s1 w& Z! }% h. F' _
2 w6 r0 J  \5 U9 r/ d- P7 e4 j
由於linux kernel在進入start_kernel前有一些前提必須要滿足:8 Z7 v5 Z7 k) A2 R: R6 Y0 I
r0  = cp#15 control register
) G2 i. R; X# Gr1  = machine ID! F, G7 ^& X- _. L* H7 S9 a5 @
r2  = atags pointer! b! U- q' k% q! h8 M( H" @
r9  = processor ID
  B# S& I& W0 N6 r& u, J' O2 R( `" k4 [; {2 P- @+ t
所以line 54~59就是在做這些準備。2 X5 |3 e/ m4 Z- A- J) I
最後呢? 我們就跳到start_kernel了。(而且還是用b start_kernel,表示我們不會再回來了)
+ ?- g& s% T# [
- _: d$ X1 b, J1 Y3 }/ S看一下start_kernel()在./init/main.c,終於跳出architecture specific的目錄,表示* t# q: c( d  u( h
我們真正的開始linux kernel的初始化。
% B& b1 z% u$ L& F; e* E) F' J像是 shedule init, console init, memory init, irq init等等都在start_kernel裡頭。3 e" e4 a+ D+ D: O$ X6 X* n2 z
到這邊之後,應該就可以深入linux kernel中,各項比較跟hardware不那麼相關的軟體部分。

評分

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

查看全部評分

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

本版積分規則

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

GMT+8, 2024-6-6 05:30 PM , Processed in 0.167021 second(s), 19 queries .

Powered by Discuz! X3.2

© 2001-2013 Comsenz Inc.

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