Chip123 科技應用創新平台

 找回密碼
 申請會員

QQ登錄

只需一步,快速開始

Login

用FB帳號登入

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

trace linux kernel source - ARM - 03

[複製鏈接]
跳轉到指定樓層
1#
發表於 2008-10-7 14:00:18 | 只看該作者 回帖獎勵 |倒序瀏覽 |閱讀模式
到目前為止,我們已經進展到kernel幫自己relocate完,並解將自己解壓縮到一個地方要準備開始執行,那疑問來了?到底是跳到哪裡去了,因為./compressed/head.S最後一行居然是- G0 u3 X+ i; @" o
『mov pc, r4』, I4 M6 E2 u! O) k+ v' j. D6 H2 U
r4只代表了解壓縮完後kernel的位址,那究竟整包kernel編譯的時候,哪個function哪個東西被放在最前面咧?!4 ]7 v' Y) J* G% {5 Z
7 Z# n. H" M- @, k. X$ {6 r
所以我們又必須開始找於kernel link的時候是怎麼被安排的,有了前面的基礎,我們可以從Makefile知道程式碼如何被編譯。至於link上的細節,例如有那些section和section先後順序等等,可以從 lds 檔來規範。
6 O9 o4 M, S5 d2 s" T6 k9 t& \8 J$ v2 @3 O& V# M3 B8 Z' l
有興趣的人可以看一下 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)
2 v/ V9 a/ D" M9 v% y! R/ a我們可以發現第一個section是『.text.head』,裡頭的_stext從目前的位置開始放。# X- Y& Z( j4 A% Z& K
於是我們曉得只要找到屬於.text.head這個section,並且是_stext這個symbol的程式碼,就是解壓縮完後的第一行程式碼。
  1.      26     .text.head : {/ A2 f- i) E2 U6 |2 X" G' v: j
  2.      27         _stext = .;4 r7 s" M2 G& I* g& W
  3.      28         _sinittext = .;
    * o7 h" p7 Q+ y0 i2 w4 s% W
  4.      29         *(.text.head)6 Z" Y; j/ V0 D& r# @
  5.      30     }
複製代碼
用指令搜尋一下,發現 ./arch/arm/kernel/head.S 裡頭有關鍵字(如下),結果我們從./arch/arm/boot/compressed/head.S跳到了./arch/arm/kernel/head.S
  1.      77     .section ".text.head", "ax"( D( x# u% @6 [. D% t2 c
  2.      78     .type   stext, %function
    1 i8 ^) x$ i- d
  3.      79 ENTRY(stext)8 u% _4 y- R5 F" x" e
  4.      80     msr cpsr_c, #PSR_F_BIT | PSR_I_BIT | SVC_MODE @ ensure svc mode
    ' Q# x7 U5 c* \
  5.      81                         @ and irqs disabled
    # m4 ], C: h; n; ], d4 \, v
  6.      82     mrc p15, 0, r9, c0, c0      @ get processor id
    ' n% D& Z2 D# i2 J$ |/ X' `) E4 u) e
  7.      83     bl  __lookup_processor_type     @ r5=procinfo r9=cpuid
    2 N7 V; F' J2 s( u( R
  8.      84     movs    r10, r5             @ invalid processor (r5=0)?
    , ?3 l' p  V: ]: O+ B( g
  9.      85     beq __error_p           @ yes, error 'p'2 k6 `8 v6 e+ `
  10.      86     bl  __lookup_machine_type       @ r5=machinfo/ d* M, T0 t/ q# d
  11.      87     movs    r8, r5              @ invalid machine (r5=0)?! h) k  c$ i' y, j' T2 `
  12.      88     beq __error_a           @ yes, error 'a'
    % ]$ e* n' |# }
  13.      89     bl  __vet_atags0 _3 }/ O# y% ?$ P* a0 v& E5 `
  14.      90     bl  __create_page_tables
複製代碼
既然找到了檔案,我們又可以開始繼續。
" U. }+ `7 q: h: [3 Z5 O. m" t9 O
$ [/ w# W( |) ]6 g% r看了一下,程式碼多了很多bl的跳躍動作,看來會在各個function跳來跳去。
分享到:  QQ好友和群QQ好友和群 QQ空間QQ空間 騰訊微博騰訊微博 騰訊朋友騰訊朋友
收藏收藏 分享分享 頂 踩 分享分享
2#
 樓主| 發表於 2008-10-9 15:32:16 | 只看該作者
既然跳到真正的kernel開始跑,表示進入重頭戲,在進入kernel之前arm的平台有一些預設的狀況,也就是說arm kernel image會預設目前的cpu和系統的狀況是在某個狀態,這樣對一個剛要跑起來的OS比較決定目前要怎麼boot起來。2 q. j; u1 I6 Q. l
$ Z0 q2 k2 `( c; W
可以看一下comment,有清楚的描述。這也幫助我們了解為什麼decompresser的程式跑完之後,還要把cache關掉。
  1.      59 /*7 A1 K2 Z) p6 S1 r3 O. j* {1 B
  2.      60  * Kernel startup entry point.1 A! p, ?2 q, K/ ~' b
  3.      61  * ---------------------------
      Q5 X; F- a5 F$ e+ c
  4.      62  *
    : j! t  [: T, X: P1 V5 v  M8 `' X
  5.      63  * This is normally called from the decompressor code.  The requirements
    : |: h9 O, w% }& H+ w$ _
  6.      64  * are: MMU = off, D-cache = off, I-cache = dont care, r0 = 0,# V; n9 l- `, l( s3 p
  7.      65  * r1 = machine nr, r2 = atags pointer.
複製代碼
基於以上的假設,當program counter指到kernel的程式的時候,就會從line 80開始跑。- Y& L+ o5 c/ |7 p6 p: }1 V
line 80, msr指令會把, operand的值搬到cpsr_c裡面。這是用來確保arm cpu目前是跑在svc mode, irq&fiq都disable。(設成0x1就會disable,definition在./include/asm-arm/ptrace.h)
$ f$ u# k5 w" e; k3 k) f) Gline 82, 讀取CPU ID到r9
0 z2 ]6 \6 A' t- M$ dline 83, 跳到 __lookup_processor_type 執行(bl會記住返回位址)。
  1.      77     .section ".text.head", "ax"! Y  t1 Q1 x6 i5 z; E7 W
  2.      78     .type   stext, %function1 B7 L8 U& m7 u& z$ o5 `
  3.      79 ENTRY(stext)
    3 W$ ?! k( \; t+ I! p) o8 r1 V
  4.      80     msr cpsr_c, #PSR_F_BIT | PSR_I_BIT | SVC_MODE @ ensure svc mode
    9 |- _6 x# [% S3 m  g
  5.      81                         @ and irqs disabled
    : U% X% O& J0 I$ x
  6.      82     mrc p15, 0, r9, c0, c0      @ get processor id7 z6 I" I; N" [/ F  U9 l& |
  7.      83     bl  __lookup_processor_type     @ r5=procinfo r9=cpuid
複製代碼
接著會跳到head-common.S這個檔,
% s' R) k. R7 I9 y; U5 }line 158, (3f表示forware往前找叫做『3』label)將label 3的address放到r3。! R/ w# _; g( m! R" n* q( i
line 159, 將r3指到的位址的資料,依序放到r7, r6, r5.(ldmda的da是要每次都位址減一次)
1 J  L2 N% e2 C+ a2 h- Tline l60, 161, 利用真實得到位址r3減去取得資料的位址得到一個offset值,這樣可計算出r5, r6真正應該要指到的地方。
1 N4 Y+ z; J! I8 V" Y/ L: Aline 163~169,這邊的程式應該不陌生,就是在各個CPU info裡面找尋對應的structure。找到的話就跳到line 171,返回head.S
; a, {% Z& ~! l% i) L4 F- r4 Yline 170, 找不到的話,r5的processor id就放0x0.表示unknown id。' u1 r, b% E7 h3 q

  i. ], h* R( e9 t: v/ R. H" N__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: H) ^) F2 |+ F  X8 a
  2.     157 __lookup_processor_type:9 b2 D1 V/ ~/ g8 `, g5 m) u
  3.     158     adr r3, 3f0 G9 I0 A2 h  Q2 g6 g
  4.     159     ldmda   r3, {r5 - r7}! G" p, R2 G- S! [# t
  5.     160     sub r3, r3, r7          @ get offset between virt&phys
    1 m8 ?# G/ k$ F! E* X
  6.     161     add r5, r5, r3          @ convert virt addresses to, V$ s4 d! A* g2 x2 O+ F
  7.     162     add r6, r6, r3          @ physical address space
    0 a8 N& @( a) o
  8.     163 1:  ldmia   r5, {r3, r4}            @ value, mask5 o6 T' V5 S; v: \0 d' F3 L% r% X
  9.     164     and r4, r4, r9          @ mask wanted bits4 s; H# h3 ]7 }6 L
  10.     165     teq r3, r42 G5 F, s! F5 I
  11.     166     beq 2f/ e# w4 ~" p  l7 A6 e: ~
  12.     167     add r5, r5, #PROC_INFO_SZ       @ sizeof(proc_info_list)$ ?! O( M, k4 U- W. G  c4 R
  13.     168     cmp r5, r63 v* P2 a: c& v+ v: v- Y* L% F: a
  14.     169     blo 1b
    + \% P2 \, X7 _" r" }
  15.     170     mov r5, #0              @ unknown processor9 f, u+ J# d, {* N& W- \2 j# `
  16.     171 2:  mov pc, lr
    ; a! q" G" Y. {, c

  17. 9 s2 d& _; N: i& b
  18.     187     .long   __proc_info_begin
    # x* H" s9 K6 S" U$ h3 |3 n
  19.     188     .long   __proc_info_end# K6 j8 J. y% i/ |
  20.     189 3:  .long   .# P$ h' _% B* R1 T
  21.     190     .long   __arch_info_begin
    $ r, U( |# X# }7 a6 y" y
  22.     191     .long   __arch_info_end
複製代碼
跳了很多檔案,建議指令和object code如何link的概念要有,就會很清楚了。
3#
 樓主| 發表於 2008-10-13 17:20:35 | 只看該作者
我們從 head-common.S返回之後,接著繼續看。
. S, X* p7 T( o: m) d9 F1 b( V3 L3 p( I5 R( ~$ ?
line 84, movs的意思是說,做mov的動作,並且將指令的S bit設起來,最後的結果也會update CPSR。這個指令執行的過程當中,會根據你要搬動的值去把N and Z flag設好。這個是有助於下個指令做check的動作。3 S9 ^4 G% j4 P/ {& s
line 85, 就是r5 = 0的話,就跳到__error_p去執行。
8 U: r: R7 M& d. `line 86, 跳到__lookup_machine_type。原理跟剛剛找proc的資料一樣。
  1.      83         bl      __lookup_processor_type         @ r5=procinfo r9=cpuid# s( C) e* C, O, A5 `7 N
  2.      84         movs    r10, r5                         @ invalid processor (r5=0)?2 V5 Y, g2 }5 S
  3.      85         beq     __error_p                       @ yes, error 'p'; J0 M. w/ Y; [  m4 S% ]- J' B/ D
  4.      86         bl      __lookup_machine_type           @ r5=machinfo
複製代碼
看得出來跟proc很像,有個兩個小地方是
( S: h) H  a3 d* V* x& {9 {- t0 v
" h- u; P0 _3 K+ C1. line 207,用ldmia不是用ldmda。原因就在於存放arch 和 proc info 的位址剛好相反。一個在lable3的上面。一個在的下方。設計上應該是可以做修改的。* F& Z7 M3 v1 T5 E8 L" |- `

0 ?% \* P+ y' r; K0 ~# n2 O" v2. arch定義的方式是透過macro,可以先看 ./include/asm-arm/mach/arch.h。裡頭有個 MACHINE_START ,這邊會設好macro,到時候直接使用就可以把arch的info宣告好。 例如 ./arch/arm/mach-omap1/board-generic.c
  1. /* macro */) b, w8 }, v1 s& k2 O/ R- }
  2.      50 #define MACHINE_START(_type,_name)                      \
    $ {% b9 W+ a, j! g" X: [: ], w
  3.      51 static const struct machine_desc __mach_desc_##_type    \* P$ u1 Y' f! B9 E& M! a/ w
  4.      52  __used                                                 \
    # g2 d) T; \& z$ M1 M- M3 A& d
  5.      53  __attribute__((__section__(".arch.info.init"))) = {    \
    & F( g, J: k% r2 J9 `
  6.      54         .nr             = MACH_TYPE_##_type,            \
    4 ]+ h1 g& ?1 Y  K: P
  7.      55         .name           = _name,
    * `9 \! \. a7 E- A' g! Q
  8.      56+ M; H) u2 F  U8 U! F
  9.      57 #define MACHINE_END                             \
    / b5 \; Y7 M1 b( T" O6 E+ ?
  10.      58 };3 a, S7 a& F* N, l' v/ o
  11.      /* 用法 */1 M# ^1 n2 D& ?5 G# y0 N
  12.      93 MACHINE_START(OMAP_GENERIC, "Generic OMAP1510/1610/1710")  E6 G% v& ?3 f4 I# P; y  y
  13.      94         /* Maintainer: Tony Lindgren <tony@atomide.com> */
    4 k# ]9 c) u0 W! y7 Z# f
  14.      95         .phys_io        = 0xfff00000,& T4 O7 H+ t* S1 }6 J0 R
  15.      96         .io_pg_offst    = ((0xfef00000) >> 18) & 0xfffc,
    $ ?. U$ O! s+ @% U
  16.      97         .boot_params    = 0x10000100,+ e' U, I7 n2 V0 ~
  17.      98         .map_io         = omap_generic_map_io,9 t& z5 y, X5 {6 n. z/ }
  18.      99         .init_irq       = omap_generic_init_irq,5 s. [* p$ ?. }4 O" \1 t* P
  19.     100         .init_machine   = omap_generic_init,
    " h8 K3 I% X. k3 L
  20.     101         .timer          = &omap_timer,/ ?' I" ]+ d9 j) V) [
  21.     102 MACHINE_END
    - l% T4 l8 x1 i
  22. / i! _5 D$ z  X8 E" ?
  23.     /* func */
    " H0 b5 U0 F- f, Z
  24.     204         .type   __lookup_machine_type, %function
    + e' Y& D4 e$ L
  25.     205 __lookup_machine_type:
    % D6 k0 Y9 k4 B% f0 y4 @- x
  26.     206         adr     r3, 3b3 P5 w8 N- n- G6 ~! L' _/ T+ H
  27.     207         ldmia   r3, {r4, r5, r6}& R$ _' \+ ?" L! d
  28.     208         sub     r3, r3, r4                      @ get offset between virt&phys
    & o% \9 F$ r7 A: Q- P% Z
  29.     209         add     r5, r5, r3                      @ convert virt addresses to
    9 q" C! x7 C# f3 Y
  30.     210         add     r6, r6, r3                      @ physical address space
    7 v8 e5 b+ @3 A% J
  31.     211 1:      ldr     r3, [r5, #MACHINFO_TYPE]        @ get machine type
    - m9 n' o$ z  s% z/ \
  32.     212         teq     r3, r1                          @ matches loader number?
    ! r: {! M& J# B% j3 M! a' B' v
  33.     213         beq     2f                              @ found9 U2 P" G$ o8 d8 M
  34.     214         add     r5, r5, #SIZEOF_MACHINE_DESC    @ next machine_desc/ U0 t1 x% t/ T% o3 }# h2 O
  35.     215         cmp     r5, r6! y* P8 x' j5 k! t5 g4 w
  36.     216         blo     1b) g+ s" v5 T5 D  S5 ^& C
  37.     217         mov     r5, #0                          @ unknown machine- M$ ?+ \! E) V8 Q
  38.     218 2:      mov     pc, lr
複製代碼
4#
 樓主| 發表於 2008-10-13 17:56:46 | 只看該作者
接著我們又返回到head.S,
8 U4 b  {$ N  a5 H! m5 Zline 87~88也是做check動作。
. i/ G- f- z' l; Q  {line 89跳到vet_atags。在head-common.S
  1.      87         movs    r8, r5                          @ invalid machine (r5=0)?' D7 a% q% i$ f( I1 _
  2.      88         beq     __error_a                       @ yes, error 'a'
    " u7 d3 w9 Q0 T3 s
  3.      89         bl      __vet_atags9 Q% d2 Z5 M5 Q' ^# S
  4.      90         bl      __create_page_tables
複製代碼
line 245, tst會去做and動作。並且update flags。這邊的用意是判斷位址是不是aligned。
; ]" C; O8 B$ {line 246, 沒有aligned跳到label 1,就返回了。; `- w( m. Q7 q6 z* t
line 248~250, 讀取atags所在的address裡頭的值到r5,看看是否不等於ATAG_CORE_SIZE,不等於的話也是返回。4 Y5 c+ l! H: K5 |' h3 |2 r( B5 Z/ [
line 251~254, 判斷一下第一個達到的atag pointer是不是等於ATAG_CORE。如果正確的話,等一下要讀取atag的資料,才會正確。9 |2 q! ?+ W! q  |. ^1 i
(atag是由bootloader帶給linux kernel的東西,用來告知booting所需要知道的參數。例如螢幕寬度,記憶體大小等等)
  1.      14 #define ATAG_CORE 0x544100019 S0 K  m% O& K/ R9 U# f
  2.      15 #define ATAG_CORE_SIZE ((2*4 + 3*4) >> 2)/ c$ J1 ?, X( q0 b5 X

  3. 3 q# h; B* n% I3 x* i  f
  4.     243         .type   __vet_atags, %function
    " J% f* S) Q  F/ z
  5.     244 __vet_atags:6 K9 K* n2 ]# @6 M% y+ r
  6.     245         tst     r2, #0x3                        @ aligned?4 ?$ |. O, j9 u" Y) b
  7.     246         bne     1f% ]: R3 h2 e+ b6 j( {
  8.     247( M' R# I8 Y' L$ r. `1 x. b0 W
  9.     248         ldr     r5, [r2, #0]                    @ is first tag ATAG_CORE?
    . v4 K+ J* {% D- J  X6 t4 t! M7 B! p
  10.     249         subs    r5, r5, #ATAG_CORE_SIZE: M6 B7 E* u* g& ~% ?4 h
  11.     250         bne     1f
    : `2 h6 l: ?" R7 X! o0 Z; u
  12.     251         ldr     r5, [r2, #4]& F0 @1 \" Q! M6 v
  13.     252         ldr     r6, =ATAG_CORE+ H+ Y& H; g4 R; M& Q6 k1 G. t+ Q% Q
  14.     253         cmp     r5, r6- k6 H5 R: u8 T9 f
  15.     254         bne     1f
    + J7 j* R4 a4 u, \5 z. @9 ^+ P
  16.     2554 a- O' C( u- ^3 v9 ^5 G7 {/ I
  17.     256         mov     pc, lr                          @ atag pointer is ok: `1 q9 Q* V: P$ u* R( [2 ?
  18.     2578 Z% v: `; i/ w# V' X7 Z. c7 B
  19.     258 1:      mov     r2, #0; m/ ]* S" G. e- w) f1 W  x7 Q
  20.     259         mov     pc, lr
複製代碼
接著我們又跳回去head.S。  ) q+ o& T! `. a
line 90,又跳到 __create_page_tables。   (很累人....應該會死不少腦細胞)
; D4 b' u( J0 D) M8 [( O- a! R) S哇!page table?!!如雷貫耳的東西,不知道會怎麼做。剛剛偷看了一下,@@還蠻長了,先到這邊好了,下次在繼續寫。
5#
 樓主| 發表於 2008-10-14 12:13:51 | 只看該作者
由於code看起來似乎越來越難解釋,會需要在檔案之間跳來跳去。建議一些基礎的東西可以再多複習幾次(其實是在說我自己 ),閱讀source code上收穫會比較多。例如:! f8 ^3 O8 ^- S, r& k

1 y, O! e1 D- C1. arm instruction set - 這個最好每個指令的意思都大略看過一次,行有餘力多看幾個版本,armv4, v5 or v6。* N1 c3 u. o; H# D3 r# A5 T
2. compiler & assembler & linker - toolchain工具做的事情和功能,大致的流程和功能要有概念。
8 G+ N2 |$ o: {' |  N3. Makefile & link script - 這兩個功能和撰寫的方式要有簡單的概念。' w: [; J, b) l! d9 _! l! [
2 [6 g" }+ X  `7 z8 |) D
以上1是非常重要的重點,2&3只要有大致上的概念就可以,因為trace code的時候,有時需要跳到Makeflie&Link script去看最後object code編排的位址。
1 q3 Z! I+ @: `0 q
6 }; B% P9 H7 K1 q; c由於我們trace到了page table這個關鍵字,在開始之前,稍微簡短的解釋,為了幫助了解,儘量用易懂的概念講,有些用詞可能會和一些真實狀況不同,但是懂了之後,應該會有能力分辨,至於正式而學術上解說,很多書上應該都有,或是google一下就很多啦∼
6#
 樓主| 發表於 2008-10-14 12:14:47 | 只看該作者
page table本身是很抽象的東西,尤其是對所謂的 user-mode application programmer 來說,大部分的狀況它是不需要被考慮到的部份,但是他的產生卻對os和driver帶來了很多影響。我們從一個小小的疑問出發。
- ~% e" J  O" Q* U( O, b/ [% y8 F5 O) o$ f0 h, C
『產生page table到底是要給誰用的?』
$ j$ t. x& Q+ \
8 }% x/ K; K' _; |' g: v3 _其實真正作用在它上面的H/W是MMU,一旦CPU啟用了MMU,當cpu嘗試去記憶體讀取一個operand的時候,cpu內部打出去的位址訊號都會先送到mmu,mmu會拿著這個位址去對照page table,看看這個位址是不是被轉換到另外一個位置(還會確認讀寫權力),最後才會到真正的位址去讀寫。
/ Z% M! a; |/ {1 M/ {7 r7 ^9 E* M" D3 V3 s
這樣來看CPU其實一開始打出去的位址訊號其實不是最後可以拿到資料的位址,我們稱為virtual address(va),mmu打出來的位址才能真正拿到資料,所以我們把mmu打出去的位址稱為physical address(pa)。那用來查詢這個位址對照關係的表格,就是page table。& e% a* W3 `' r8 ^, x" b2 V
' e( }) Q3 h, T6 i0 Z5 n
到這邊我們有一個簡單的概念。來想像一個小問題,一個普通的周邊(例如你的顯示卡),因為他並不具有MMU功能,hw只看得懂pa,但是CPU卻是作用在va,假如我想去對hw的控制暫存器做讀寫,到底要用va還是pa?
7#
 樓主| 發表於 2008-10-14 12:15:51 | 只看該作者
這時,寫driver的人就必須要小心,設定給硬體看的位址必須要先轉成pa(像是設定DMA),給CPU執行的程式碼要使用va,這對一開始嘗試寫driver但是又不瞭解va pa之間的差別的人,常常產生很多疑問。: o+ |& R2 \6 v
1 C4 _7 M! a; u
現在我們回頭想想OS,既然我們跑到create page table,可以預期的是他想要將MMU打開,因此希望預先建立起一個page table,讓MMU知道目前os想規劃的位址對應和讀取權力是如何被安排的。所以os必須考慮到現在的系統究竟是長怎樣?應該要如何被安排?是不是有那些要被保護?好吧∼因為我們完全對os不了解,也不知道該安排什麼,只能祈禱在trace code完後,可以找到這些問題的答案,或者發現一些沒想到的問題。
' t3 ^+ j! `5 _' v( z/ ^$ f
' ^& k2 y  a+ q! P1 q& x3 |知道了page table的大致上的功能,下篇就可以專心的研究這個table的長相,和它想規劃出的系統模樣。
( w. M" A3 N/ \( O
* J$ F/ I0 j  }2 h3 F, xp.s. 字數限制好像變短了。   (看來很難寫)
8#
 樓主| 發表於 2008-10-14 13:56:17 | 只看該作者
由於字數限制挑整,用詞儘量精簡,以便可以貼必要source。另外,以後寫到一段落,考慮收集成一篇完整的文章,這樣應就不會因為用回覆的方式,造成閱讀斷斷續續的問題。希望有人對文章呈現方式有想法的話,可以跟我講。
9#
 樓主| 發表於 2008-10-14 13:57:39 | 只看該作者
現在,讓我們跳入create_page_tables吧∼2 w3 m# [5 S* n, X) N
line 216,會跳到pgtbl的macro去執行,其實只是載入一個位址。5 s% Z' n# v1 |5 O

; s- D8 R# v  N( s% y( X, @. s只是這個位址因為你硬體規劃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 */
    8 |; g' d( D) }5 [
  2.      95 textofs-y       := 0x00008000
    8 H( A0 L9 D" z7 ?
  3.     152 TEXT_OFFSET := $(textofs-y)
複製代碼
  1. /* include/asm-arm/arch-omap/memory.h */
    & D, u& T! u) t; D
  2.      40 #define PHYS_OFFSET             UL(0x10000000). ~8 W$ q$ b. M/ k2 a
  3. * `) T2 A3 [1 e( I& E
  4.      /* arch/arm/kernel/head.S */9 y9 @) p; q) X" Q, o8 i/ t$ H
  5.      29 #define KERNEL_RAM_VADDR        (PAGE_OFFSET + TEXT_OFFSET)  `$ Z( C: |) @" L* R" T
  6.      30 #define KERNEL_RAM_PADDR        (PHYS_OFFSET + TEXT_OFFSET)
    % s$ K! Z6 x1 t! y( Z

  7. $ a- f$ _+ f* M  W0 X5 O9 s
  8.      47         .macro  pgtbl, rd
    1 c# p; \1 A8 }; Z. L7 L( G" V
  9.      48         ldr     \rd, =(KERNEL_RAM_PADDR - 0x4000)
    ' S4 W3 s, N5 |2 Q0 I  J
  10.      49         .endm
    9 i% a6 L5 S& m+ n& Z/ t: X, H
  11. 3 P4 g, m# K2 r
  12.     216         pgtbl   r4                              @ page table address
複製代碼
10#
 樓主| 發表於 2008-10-14 14:16:53 | 只看該作者
得到pg table的開始位置之後,當然就是初始化囉。4 z  B9 G" o3 i% t! A% A
line 221, 將pg table的base addr放到r0.& Q9 J0 j! f7 G4 f
line 223, 將pg table的end addr放到r6.
8 u0 V: b3 ?5 z# Dline 224~228, 反覆地將0x0寫到pg table的區段裡頭,每個loop寫16bytes. (4x4),直到碰到end,結果就是把它全部clear成0x0.
  1.     221         mov     r0, r4# q) h" Q  i2 q- I. B6 l# a
  2.     222         mov     r3, #0) l& i' j5 _) e1 x" I/ y# E. [
  3.     223         add     r6, r0, #0x4000& W" O9 ?. k* {1 i1 u, Q9 m
  4.     224 1:      str     r3, [r0], #4) ~# W8 W  S( e8 G1 T2 A' R
  5.     225         str     r3, [r0], #49 l# U8 H1 O/ Q6 j$ W
  6.     226         str     r3, [r0], #4
      T2 U' k: U2 d. B$ E% M
  7.     227         str     r3, [r0], #4
    7 ~/ y/ @7 m% }* v' R. q- t
  8.     228         teq     r0, r60 J) h" ^4 R* x0 W; S
  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 | 只看該作者
問題怎麼填值??
; ]# Z7 t  g9 C5 W% M5 _% S1 c拿出ARM的手冊,翻到MMU章節。一看發現page有很多種,還得分first level和second level。
8 D# {' n% D) A$ y$ P$ C2 G, s2 h) B9 Z, u9 o; d9 Z3 X
念書時的印象,是從1st level在去查2nd level,先看怎麼設定1st level (不知以前老師有沒有亂教)6 @1 y8 W' n; R1 i& P1 T
1. [31:20]存著section base addr- t( b/ `. W0 b: ?4 L! ^* w( H& `) d
2. [19:2]存著mmu flags6 Z: y/ z1 {( i, V) _; K
3. [1:0]用來辨別這是存放哪種page, 有四種:
: C5 B2 o; o5 u! V* l; z! O2 U   a. fault (00) b. coarse page (01) c. section (1st level) (10) d. fine page (11)
: e! p8 x& K- m& m0 k4. pg tabel資料要存放到 [31:14] = translation base, [13:2] = table index , [1:0] = 0b00 的位址* f, x, M& \+ v0 i# Q7 o
2 d8 H& g& u; v/ x0 x& M5 t" B
來看code是怎麼設定。+ a- ~/ \* V* t4 J5 m4 Z
9 K$ P; h) J/ l
line 239, 將pc的值往右shift 20次放到r6.這是取得前20高位元的資料,有點像是 1.。
+ H! G  O6 g+ g! o" Yline 240, 將r6往左shift 20次之後,和r7做or的動作,剛好也是 2.提到的mmu flags和1.處理好的資料做整理。$ m  n7 z. @  {) W! k# N
所以前面兩個做完,就完成了bit[31:2]。3 @4 j' H# ]7 k
line 241, 將r3的資料寫到r4+(r6<<0x2)的地方去,剛好是4.提到的位址。(lucky)
  1.     239         mov     r6, pc, lsr #20
    * q- A0 M& V: D0 U4 d
  2.     240         orr     r3, r7, r6, lsl #20
    , s* R7 S; i" u; {
  3.     241         str     r3, [r4, r6, lsl #2]
複製代碼
p.s. 用pc值剛好可以算出當前的page。
12#
 樓主| 發表於 2008-10-22 19:47:03 | 只看該作者
最近又被釘上了,開始忙碌,大概沒太多時間貼文∼& t* [% k: l5 x0 M# h9 @4 m
6 s5 _/ q, ~0 U; J4 I( }
上篇已經將pc所屬的page table entry(pte)設定好,接著繼續看
' `9 c, Y  Q/ a( E  O- r, p; b" S/ Q% pline 247, 248, 將KERNEL_START的位址往右shift 18算出pte的offset,(等於line239&241,239&241是先shift right 20然後shift left 2),並將剛剛r3的值設給pte。『!』的意思是會把address存到r0。
4 m: A& d# w3 |, b: d: ?7 Y7 H' n' s- t! G3 r# Z, G" N/ [
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
    5 b9 M/ X4 {& _* B% p) x
  2.     248         str     r3, [r0, #(KERNEL_START & 0x00f00000) >> 18]! 7 D$ V: f) M& M! |: n
  3.     249         ldr     r6, =(KERNEL_END - 1)
    ( L! a& `5 R1 I5 ~- L" T8 L( f
  4.     250         add     r0, r0, #4) B9 P9 b1 u9 @
  5.     251         add     r6, r4, r6, lsr #18
      R. G; U: A- d" C4 G9 C, F8 ]5 n( g
  6.     252 1:      cmp     r0, r6  N5 A: w( K, p
  7.     253         add     r3, r3, #1 << 20
    7 ~& M% t, P: \* m' o, h
  8.     254         strls   r3, [r0], #4
    " |# h% G) X0 \9 a3 r. v7 [( Q
  9.     255         bls     1b
複製代碼
13#
 樓主| 發表於 2008-10-22 20:24:58 | 只看該作者
line279,PAGE_OFFSET是規範RAM mapping完後的virtual address,我們將這個位址所屬的pte算出來放到r0。
# v) }! L8 o9 B! v9 r8 }line 280~283,將要 map 的physical address的方式算出來放到r6。
5 ^; ^3 x0 w3 G0 \: ?$ H  Lline 284,最後將結果存到r0所指到的pte。
) B) L6 i, ^- V, e$ N. l; Q; ]2 N8 n4 B% ^# X
以上三個動作,就是做好 ram 起頭的位址一開始map,還沒做完整塊map。由於我們目前的設定方式每塊是1MB,所以這邊是將RAM開始的1MB做map。
, j% I0 Q' g# O$ c" E: i! p' c# I( g0 ?; G3 q: u) i* q4 v
line 327,返回,結束一開始的create page table的動作,我們可以看出其實並沒有做完整的初始化page table,所以之後應該會有其他page table的細節。
  1.     279         add     r0, r4, #PAGE_OFFSET >> 18- Y" [$ }  ]6 \) U0 u
  2.     280         orr     r6, r7, #(PHYS_OFFSET & 0xff000000)8 j/ ?% U' m& o$ i; [
  3.     281         .if     (PHYS_OFFSET & 0x00f00000)
    1 Y; V1 X6 X4 m$ k0 Z7 j  A0 h: N
  4.     282         orr     r6, r6, #(PHYS_OFFSET & 0x00f00000)
    , N1 \% ?0 s$ d
  5.     283         .endif. r  ?+ S9 I- q# x/ |3 |
  6.     284         str     r6, [r0]
    - ]! T* n8 ?* ^7 T
  7.     327         mov     pc, lr
複製代碼
附帶一提,我們這邊省略了一些用ifdef包起來的程式碼,像是一開始會印一些output message的程式碼(line286~326)。
14#
 樓主| 發表於 2008-10-22 20:37:08 | 只看該作者
自create page table返回後,我們偷偷看一下接下來的程式碼,
3 f' E  M* z: l7 ~3 |. @line 99, 將switch_data擺到r13
$ j( o: x8 Y& z$ ?6 Aline 101, 將enable_mmu擺到lr
8 G  b) s: H$ Gline 102, 將pc改跳到r10+PROCINFO_INITFUNC的地方去
' X- t6 S8 G# r, c# K& g- G& U- X6 L$ c5 |: m
其實這邊有點玄機,switch_data和enable_mmu都是function,結果位址都只是被存起來,並沒有直接跳過去執行。其實,雖然程式碼是順序switch_data->enable_mmu->proc init function,但其實執行的順序會是 procinfo 的init function-> enable_mmu -> switch_data 。至於,為什麼要這樣寫的原因?就不清楚了,也還沒仔細推敲過。
) @9 s" k/ @( H( S# J; g- a( I  f/ M
) G6 s" ^$ X( D5 Sswitch_data最後就會跳到大家都很熟悉的start_kernel(). 詳細要賣個關子,最近會忙一下,得要過一陣子才能貼文∼  
  1.      99         ldr     r13, __switch_data              @ address to jump to after3 b, ^2 v/ M$ ^  h- B( ^& u
  2.     100                                                 @ mmu has been enabled
    9 c0 B8 u% h; S# R1 ~
  3.     101         adr     lr, __enable_mmu                @ return (PIC) address# u& R. y0 y  T
  4.     102         add     pc, r10, #PROCINFO_INITFUNC
複製代碼
15#
 樓主| 發表於 2009-7-4 01:09:36 | 只看該作者
老店重新開張~# Z" K' K6 \8 @" h3 y* z9 `

# Z$ x: \5 o4 ?3 S+ P2 \4 v花了一些時間把舊的貼文整理到一個blog' v! G8 c" h* {, O( B8 z  o) L1 m
有把一些敘述修改過
5 R( X/ o3 ]/ P5 Y4 Y8 g希望會比較容易集中閱讀# y' {& v; h/ o- W' D
目前因為某些敘述不容易0 d8 j$ N% |4 d# [4 C
還是比較偏向筆記式而且用字不夠精確
& U  \6 Z$ `6 ?2 {. G/ v1 s6 d希望之後能夠慢慢有系統地整理
4 l1 u; y: b0 L( T7 _/ s大家有興趣的話6 O# Y% H" q3 l; `6 p
可以來看看和討論
* m! h7 T7 h. [http://gogojesseco.blogspot.com/
7 u, I7 E  }) N) ?$ _. p' M% W6 h8 @, z6 `, G( x; l( N( K5 g
以後可能會採取  先在chip123貼新文章% C- Q$ f# J1 X2 @( u
慢慢整理到blog上的方式+ E* D" ]) ]7 K( B* ^
因為chip123比較方便討論 =)& Q! N% G# d' L& i/ }6 b2 W" _5 X
blog編輯修改起來比較方便
# A! s1 U0 Y- f' e2 [閱讀也比較集中   大家可以在這邊看到討論
. \+ W& U" Z! g+ {* R: O然後在blog看到完整的文章 (類似BBS精華區的感覺)

評分

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

查看全部評分

16#
 樓主| 發表於 2009-7-15 17:07:03 | 只看該作者
隔了很長一段時間沒update! l7 s1 J) p4 v0 V0 o
之前程式碼走到 ./arch/arm/kernel/head.S 的 line 99 附近
  1.      99         ldr     r13, __switch_data              @ address to jump to after
    ; U' X. L* C8 {& S, s+ @. f% s- _
  2.     100                                                 @ mmu has been enabled
    ; k: R1 @3 D1 H0 K
  3.     101         adr     lr, __enable_mmu                @ return (PIC) address
    4 h4 G$ Q( C# ?: m
  4.     102         add     pc, r10, #PROCINFO_INITFUNC
複製代碼
line 99, 將__switch_data放到r13。(留作之後用)
5 S$ _+ N$ q* ]6 k- m) dline 101, 將__enable_mmu的addr放到lr。(留作之後用)
5 D- q: i' n4 ]$ W! V; t( qline 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
    , Y- S0 {' v9 h& h
  2. 374 __arm926_setup:
    " J+ i$ T3 C! a- t2 ], ]. F- r7 E
  3. 375         mov     r0, #0) w# T2 B' \4 \5 F* [6 x. L
  4. 376         mcr     p15, 0, r0, c7, c7              @ invalidate I,D caches on v4, u, A  k3 P$ y+ _$ w
  5. 377         mcr     p15, 0, r0, c7, c10, 4          @ drain write buffer on v4; t  g  F: H+ K+ Y  S' r
  6. 378 #ifdef CONFIG_MMU/ f/ \# w, ?  z3 _! D& V! V
  7. 379         mcr     p15, 0, r0, c8, c7              @ invalidate I,D TLBs on v4
    % {- a) Z6 `3 l; t# K
  8. 380 #endif  j1 e! l+ F/ @  E( R6 M, H! a0 i; x% j

  9. . E& j  w" v- F/ b  o
  10. 388         adr     r5, arm926_crval/ C: D: Q! L: a2 t; \' P2 h5 e
  11. 389         ldmia   r5, {r5, r6}
    0 |& v. i3 R9 `5 Q
  12. 390         mrc     p15, 0, r0, c1, c0              @ get control register v4
    , B) k, {- ^0 `$ V3 s( b- _
  13. 391         bic     r0, r0, r5
    + Z7 |$ j6 o) ]: t( g- B+ O
  14. 392         orr     r0, r0, r6
    + X+ A7 U5 M8 _0 M7 P
  15. 9 N" K' s- b) `
  16. 396         mov     pc, lr
    5 v2 _9 T- q6 I+ q2 U2 Q& d$ }
  17. 397         .size   __arm926_setup, . - __arm926_setup
複製代碼
這邊的程式碼就跟CPU有很大的相依性,
9 n+ C; C3 t, p! j  H, A, \+ rline 375~380, 主要就是invalidate CPU的I&D cache和清空write buffer。9 D* f  K" R3 U$ b3 N+ Y
line 388~392, 把cp15的設定讀出來,並且將一些預設狀態做好運算放到r0。(預設值從arm926_crval這邊可以取得。)
0 J' C: O: w$ B3 e2 m8 Aline 396, 直接把pc跳到lr,因為我們之前已經將lr = enable_mmu,所以直接跳過去。
17#
 樓主| 發表於 2009-7-15 17:29:45 | 只看該作者
  1. 155 __enable_mmu:
    1 `! ~8 r- l* x
  2. 170         mov     r5, #(domain_val(DOMAIN_USER, DOMAIN_MANAGER) | \
    ! t# f( A6 x) t& f0 `. j. _( A
  3. 171                       domain_val(DOMAIN_KERNEL, DOMAIN_MANAGER) | \
    3 r! n) a$ ^/ V; G" Z: \6 O
  4. 172                       domain_val(DOMAIN_TABLE, DOMAIN_MANAGER) | \
      r$ P* b' y, Y& `
  5. 173                       domain_val(DOMAIN_IO, DOMAIN_CLIENT))3 H) `" ]2 b% O4 U
  6. 174         mcr     p15, 0, r5, c3, c0, 0           @ load domain access register
    4 i2 ?* o+ |1 Y6 u
  7. 175         mcr     p15, 0, r4, c2, c0, 0           @ load page table pointer3 {$ B# {8 o1 v4 a8 R5 R8 M
  8. 176         b       __turn_mmu_on+ {9 ?/ v( Q3 D1 Z# V6 F# }* v3 _
  9. 177 ENDPROC(__enable_mmu)
複製代碼
line 170~174,設置好domain access。(domain access可以先當成設置access的權限,或許以後可以寫詳細的文章說明)+ j' O$ `5 q& ~6 |
line 175~176,將create好的page table開頭丟給mmu。跳到turn_mmu_on
  1. 191 __turn_mmu_on:4 T$ D% P9 X7 t2 y* v" D
  2. 192         mov     r0, r0
    / s9 G; h8 i( l7 u* h4 v  x' u0 C
  3. 193         mcr     p15, 0, r0, c1, c0, 0           @ write control reg8 _, O$ o* I4 R0 ^
  4. 194         mrc     p15, 0, r3, c0, c0, 0           @ read id reg
    1 X- q% E" C/ j
  5. 195         mov     r3, r31 p9 F: o! V# U
  6. 196         mov     r3, r3
      B0 p! G0 ]5 \4 x; F
  7. 197         mov     pc, r13- T% U3 Z, y: n8 x# ^
  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:
    ( ]* x' s( H7 c
  2. 19         .long   __mmap_switched
    % M6 ]9 ~: b7 P
  3. 20         .long   __data_loc                      @ r41 {7 E4 t4 A3 A. P8 ]
  4. 21         .long   _data                           @ r5: y4 o; a, P2 P+ I3 N$ I
  5. 22         .long   __bss_start                     @ r6
    - |1 f) }; {4 ~) B
  6. 23         .long   _end                            @ r7$ p, }$ P) \5 p' c4 i/ h, ~
  7. 24         .long   processor_id                    @ r4
    5 a9 h; q) B1 D" b' J  R
  8. 25         .long   __machine_arch_type             @ r58 Q4 h2 J$ m9 W1 u% A; t7 r
  9. 26         .long   __atags_pointer                 @ r6
    - s- @3 ?- E. x2 @' N, i
  10. 27         .long   cr_alignment                    @ r77 s! L, P% h! \% H
  11. 28         .long   init_thread_union + THREAD_START_SP @ sp
    ! n2 n5 r" P7 F; l3 i& @! X
  12. 29
    ( @: r( L7 z3 [6 s. ~+ n
複製代碼
switch_data的第一行就是 __mmap_switched,所以我們直接看line 39。
18#
 樓主| 發表於 2009-7-15 17:30:00 | 只看該作者
  1. 39 __mmap_switched:
    0 O: r5 h# h( I2 J( J9 I. X
  2. 40         adr     r3, __switch_data + 4( {9 F5 M/ |3 ^) p5 `
  3. 41* M# B; y5 V% r- p1 X5 l0 l
  4. 42         ldmia   r3!, {r4, r5, r6, r7}  }8 m, B3 B' I) Q
  5. 43         cmp     r4, r5                          @ Copy data segment if needed" B+ V1 v" y: a# f; S0 C
  6. 44 1:      cmpne   r5, r6( g" W- v, o# s( [
  7. 45         ldrne   fp, [r4], #4! E6 S! V- s& [3 z6 X4 _3 R2 a: W
  8. 46         strne   fp, [r5], #44 M1 \2 E+ _! B% R3 ~# r
  9. 47         bne     1b
    # `% F- @( W. u2 k+ {$ b
  10. 48: ?1 X% @9 V& l: p1 s
  11. 49         mov     fp, #0                          @ Clear BSS (and zero fp)
    0 R$ U% K( n, q9 L5 g
  12. 50 1:      cmp     r6, r7
    . a; _+ t; z" e  V& [( ]6 l9 q
  13. 51         strcc   fp, [r6],#46 U  s# m" E6 V
  14. 52         bcc     1b
    0 y) I+ T4 L3 _5 {- X2 L
  15. 53
    % n$ L, h& f+ ^+ F0 u" p4 D
  16. 54         ldmia   r3, {r4, r5, r6, r7, sp}9 _, ^* R$ X( `3 q' I
  17. 55         str     r9, [r4]                        @ Save processor ID- t; m* c7 B7 ?  d
  18. 56         str     r1, [r5]                        @ Save machine type, _* y+ A3 }4 R5 S' l
  19. 57         str     r2, [r6]                        @ Save atags pointer
    ! @: L5 W* `  Q: I" y5 X
  20. 58         bic     r4, r0, #CR_A                   @ Clear 'A' bit* O4 s9 x& F% f+ b8 @' [8 J; T
  21. 59         stmia   r7, {r0, r4}                    @ Save control register values. V+ ^- b5 g& ^8 g1 F) I* q
  22. 60         b       start_kernel8 v4 L9 E% T9 B+ Y, N6 {( `6 ?
  23. 61 ENDPROC(__mmap_switched)
複製代碼
switch_data的第一行就是 __mmap_switched,所以我們直接看line 39。5 e, T1 _/ k$ g- y
line 39,將__data_loc的addr放到r3
2 f$ M. ^2 K  q: s+ H6 ^7 sline 42,從r3的位址,連續讀取四筆資料到r4, r5, r6, r75 g- ~! r+ b+ J
line 43~47,看看data segment是不是需要搬動。
! m2 Q' T' k/ _line 49~52, clear BSS。- |* T, y9 U- ~) z3 [. g2 n$ F
+ V/ z! t) E9 ^) T2 i! t. e
由於linux kernel在進入start_kernel前有一些前提必須要滿足:' u( S2 {, t% x# c" [
r0  = cp#15 control register. [% x% {/ l8 I
r1  = machine ID
* c" r6 j( C; h5 ~% f$ j; D) wr2  = atags pointer- R! r9 n+ }* H; q7 Z
r9  = processor ID( I: t4 B2 s) O/ b/ X, h! ~
; x% d: u4 n2 |
所以line 54~59就是在做這些準備。" Z; Z" o3 f8 W4 ]$ ]# K1 ?8 M
最後呢? 我們就跳到start_kernel了。(而且還是用b start_kernel,表示我們不會再回來了)9 r! L  E. L! H: C% B
2 d* |0 n5 p) k
看一下start_kernel()在./init/main.c,終於跳出architecture specific的目錄,表示
3 J$ u8 O- J( Z% W! A3 r8 g) g5 o+ g我們真正的開始linux kernel的初始化。
7 h, O0 N# ]2 O, W4 Q" H' D$ Y像是 shedule init, console init, memory init, irq init等等都在start_kernel裡頭。- V+ z& X6 J$ j+ l8 L: v  c
到這邊之後,應該就可以深入linux kernel中,各項比較跟hardware不那麼相關的軟體部分。

評分

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

查看全部評分

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

本版積分規則

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

GMT+8, 2024-9-27 09:12 PM , Processed in 0.221013 second(s), 19 queries .

Powered by Discuz! X3.2

© 2001-2013 Comsenz Inc.

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