Chip123 科技應用創新平台

 找回密碼
 申請會員

QQ登錄

只需一步,快速開始

Login

用FB帳號登入

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

trace linux kernel source - ARM - 03

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

0 P8 e. g; \4 j) s+ ?有興趣的人可以看一下 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)9 Z, q2 f$ ~0 f0 f
我們可以發現第一個section是『.text.head』,裡頭的_stext從目前的位置開始放。9 p4 J$ {! W- g$ L! a# S
於是我們曉得只要找到屬於.text.head這個section,並且是_stext這個symbol的程式碼,就是解壓縮完後的第一行程式碼。
  1.      26     .text.head : {
    6 y/ S2 g  \( E" M5 ]. q8 P9 n+ ~
  2.      27         _stext = .;
    , `& F9 M9 Y# S
  3.      28         _sinittext = .;; _2 f. {- g/ [
  4.      29         *(.text.head); J3 w: k+ t; P3 M2 @
  5.      30     }
複製代碼
用指令搜尋一下,發現 ./arch/arm/kernel/head.S 裡頭有關鍵字(如下),結果我們從./arch/arm/boot/compressed/head.S跳到了./arch/arm/kernel/head.S
  1.      77     .section ".text.head", "ax"5 p* m+ P  b4 O" q4 @
  2.      78     .type   stext, %function
    0 E* a# E2 B* A( h7 k
  3.      79 ENTRY(stext)) q' u9 b$ s: W! h
  4.      80     msr cpsr_c, #PSR_F_BIT | PSR_I_BIT | SVC_MODE @ ensure svc mode- d3 z- C4 a5 T) @3 a
  5.      81                         @ and irqs disabled
    - H. ?- x7 K9 J
  6.      82     mrc p15, 0, r9, c0, c0      @ get processor id
    ( ~/ k1 p+ r0 `& O: l8 g! L
  7.      83     bl  __lookup_processor_type     @ r5=procinfo r9=cpuid
    2 T8 D9 H- {' w  S+ H+ ~9 R: X- @
  8.      84     movs    r10, r5             @ invalid processor (r5=0)?
    % b  u/ B6 C+ h1 E4 [
  9.      85     beq __error_p           @ yes, error 'p'/ V# S4 s2 }+ u' I4 u7 @6 `
  10.      86     bl  __lookup_machine_type       @ r5=machinfo( I1 A7 t& x+ r
  11.      87     movs    r8, r5              @ invalid machine (r5=0)?
    # G, i. W5 P: z& W  A8 Q- r3 Y
  12.      88     beq __error_a           @ yes, error 'a'
    ) B9 |9 E7 H  j/ Z5 d; U' g
  13.      89     bl  __vet_atags
      N. ]3 _$ J1 P
  14.      90     bl  __create_page_tables
複製代碼
既然找到了檔案,我們又可以開始繼續。
, _6 s* f$ r. t
/ F& x& ?- V9 B- J! T0 ^2 S4 Y看了一下,程式碼多了很多bl的跳躍動作,看來會在各個function跳來跳去。
分享到:  QQ好友和群QQ好友和群 QQ空間QQ空間 騰訊微博騰訊微博 騰訊朋友騰訊朋友
收藏收藏 分享分享 頂 踩 分享分享
2#
 樓主| 發表於 2008-10-9 15:32:16 | 只看該作者
既然跳到真正的kernel開始跑,表示進入重頭戲,在進入kernel之前arm的平台有一些預設的狀況,也就是說arm kernel image會預設目前的cpu和系統的狀況是在某個狀態,這樣對一個剛要跑起來的OS比較決定目前要怎麼boot起來。6 k7 Q" `$ k! j
) D# p7 m' p! G' |7 |' ~# i
可以看一下comment,有清楚的描述。這也幫助我們了解為什麼decompresser的程式跑完之後,還要把cache關掉。
  1.      59 /*
    . m% J5 d( P; g- {" I8 L9 x( U
  2.      60  * Kernel startup entry point.
    - z, f: d, \5 \. p, T) [6 F9 f
  3.      61  * ---------------------------
    2 B) u% ~% x8 \' v- x7 t9 I: u  |
  4.      62  *
    # }* _/ t0 O% I. D" t
  5.      63  * This is normally called from the decompressor code.  The requirements2 E! o0 K! I6 E$ b6 q, Z
  6.      64  * are: MMU = off, D-cache = off, I-cache = dont care, r0 = 0,
    3 s0 c8 l9 D/ x5 |- U  k
  7.      65  * r1 = machine nr, r2 = atags pointer.
複製代碼
基於以上的假設,當program counter指到kernel的程式的時候,就會從line 80開始跑。& d4 U7 M4 A( V  P' u9 s" t% d
line 80, msr指令會把, operand的值搬到cpsr_c裡面。這是用來確保arm cpu目前是跑在svc mode, irq&fiq都disable。(設成0x1就會disable,definition在./include/asm-arm/ptrace.h)
! }# p6 w4 K1 r- p* B& p" \line 82, 讀取CPU ID到r9
* S+ i8 r% r. X9 c4 r, R  sline 83, 跳到 __lookup_processor_type 執行(bl會記住返回位址)。
  1.      77     .section ".text.head", "ax"2 D& }* @; X+ ~) z- ~, X
  2.      78     .type   stext, %function6 \& E$ a8 I. `* @5 C& Q+ k7 h* ]
  3.      79 ENTRY(stext)  _) q" b* l- ~, Z3 j6 V
  4.      80     msr cpsr_c, #PSR_F_BIT | PSR_I_BIT | SVC_MODE @ ensure svc mode, C* T6 c2 L$ E4 q" u8 ~+ a
  5.      81                         @ and irqs disabled  Y( v7 o8 e- `7 c; j$ C' N) e
  6.      82     mrc p15, 0, r9, c0, c0      @ get processor id; P, N- {  m/ a; h+ |# J
  7.      83     bl  __lookup_processor_type     @ r5=procinfo r9=cpuid
複製代碼
接著會跳到head-common.S這個檔,
! ~* O: s. [6 W/ G9 e1 l) ^line 158, (3f表示forware往前找叫做『3』label)將label 3的address放到r3。
+ B9 {4 e2 ], X$ iline 159, 將r3指到的位址的資料,依序放到r7, r6, r5.(ldmda的da是要每次都位址減一次)
; W4 {, p  e" y9 k/ g& b, Y" t% ~line l60, 161, 利用真實得到位址r3減去取得資料的位址得到一個offset值,這樣可計算出r5, r6真正應該要指到的地方。1 m7 |; F% e+ y+ U
line 163~169,這邊的程式應該不陌生,就是在各個CPU info裡面找尋對應的structure。找到的話就跳到line 171,返回head.S
& r+ z% b5 k/ G  ~9 C) }line 170, 找不到的話,r5的processor id就放0x0.表示unknown id。8 O4 t9 \/ k; y- E1 O
" k7 Z* _4 w! h
__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, %function7 C+ I' K+ a# j2 K# z! K! J
  2.     157 __lookup_processor_type:
    ' k& |$ A- H" B# R4 s
  3.     158     adr r3, 3f
    . t( U0 L6 z  s% e: x7 T/ u) S& w6 v
  4.     159     ldmda   r3, {r5 - r7}. Z" a7 c0 U' v& R: \
  5.     160     sub r3, r3, r7          @ get offset between virt&phys
    ) |% ~0 l8 @' }! Z9 _
  6.     161     add r5, r5, r3          @ convert virt addresses to
    ; I7 F2 @7 K. \3 L# ^. V; l
  7.     162     add r6, r6, r3          @ physical address space
    1 f7 [5 w0 \" b3 Q4 Q
  8.     163 1:  ldmia   r5, {r3, r4}            @ value, mask
    : I9 f" N5 {; [
  9.     164     and r4, r4, r9          @ mask wanted bits
    . ~; m# e2 J1 G6 v! \9 ^3 u3 q
  10.     165     teq r3, r4
    ! y' ~: d9 n9 y- J
  11.     166     beq 2f
    ' j( E% `" c  k1 i2 R0 z5 n
  12.     167     add r5, r5, #PROC_INFO_SZ       @ sizeof(proc_info_list)# ~  @# _2 {0 k
  13.     168     cmp r5, r6
    2 }" }# ~( C2 p/ F" w2 ^" }
  14.     169     blo 1b5 A8 L$ ^' w4 B/ u' a" M
  15.     170     mov r5, #0              @ unknown processor2 P0 U5 B" ^' y* Y( k( r
  16.     171 2:  mov pc, lr
    4 t$ m9 ~6 T( p  y1 {

  17. . f$ S) G$ E/ ~! G! @
  18.     187     .long   __proc_info_begin% |! s/ n7 j* U7 Z0 Y9 q
  19.     188     .long   __proc_info_end
    # P4 l  _* H7 S$ G
  20.     189 3:  .long   .6 ?: V* i- {7 n1 J
  21.     190     .long   __arch_info_begin
    % R4 l  N; B, M' s4 K6 t% H) K
  22.     191     .long   __arch_info_end
複製代碼
跳了很多檔案,建議指令和object code如何link的概念要有,就會很清楚了。
3#
 樓主| 發表於 2008-10-13 17:20:35 | 只看該作者
我們從 head-common.S返回之後,接著繼續看。, M- [# L& }$ q# E. o
+ ^& M2 v) d. o( e4 y6 H/ a
line 84, movs的意思是說,做mov的動作,並且將指令的S bit設起來,最後的結果也會update CPSR。這個指令執行的過程當中,會根據你要搬動的值去把N and Z flag設好。這個是有助於下個指令做check的動作。
  c8 ]4 A. u$ R7 I  J  Pline 85, 就是r5 = 0的話,就跳到__error_p去執行。4 U% y3 i/ d* {1 a+ C! W  O( k9 \/ A
line 86, 跳到__lookup_machine_type。原理跟剛剛找proc的資料一樣。
  1.      83         bl      __lookup_processor_type         @ r5=procinfo r9=cpuid
    8 @4 f9 U2 H0 w$ h+ t- P1 g; i
  2.      84         movs    r10, r5                         @ invalid processor (r5=0)?$ I  u# q- I. S0 S
  3.      85         beq     __error_p                       @ yes, error 'p', Z4 @4 Q, v  m: V
  4.      86         bl      __lookup_machine_type           @ r5=machinfo
複製代碼
看得出來跟proc很像,有個兩個小地方是
1 k" f  F! C! ]- H& y% x6 T- L: Z) t8 @# @5 I* J7 p6 {
1. line 207,用ldmia不是用ldmda。原因就在於存放arch 和 proc info 的位址剛好相反。一個在lable3的上面。一個在的下方。設計上應該是可以做修改的。
" ^0 K4 M( ]! M+ _
# K9 ?6 I% o* ^4 h" C. R2. arch定義的方式是透過macro,可以先看 ./include/asm-arm/mach/arch.h。裡頭有個 MACHINE_START ,這邊會設好macro,到時候直接使用就可以把arch的info宣告好。 例如 ./arch/arm/mach-omap1/board-generic.c
  1. /* macro */! B- }  i9 E8 h! t1 F
  2.      50 #define MACHINE_START(_type,_name)                      \$ d+ Z) @8 [6 l! ~) Q! u
  3.      51 static const struct machine_desc __mach_desc_##_type    \
    8 Q+ f4 j) I4 F
  4.      52  __used                                                 \5 l( q6 ]$ f$ B5 y/ n( G7 `9 ^
  5.      53  __attribute__((__section__(".arch.info.init"))) = {    \4 ?* f  K( @6 O8 k$ E
  6.      54         .nr             = MACH_TYPE_##_type,            \
    - _$ n6 o3 F9 T1 u7 A( d/ k6 K( G
  7.      55         .name           = _name,- D5 P* f+ O' t$ N2 C* @- M
  8.      56
    - U6 a' h; t/ z; m0 k) Z: u
  9.      57 #define MACHINE_END                             \
    & K" \- x% n$ }! O4 L
  10.      58 };
    9 D" v& K% n0 y- V3 P& S. p: L6 `. a
  11.      /* 用法 */
    + G( P4 m- D! k6 a2 V
  12.      93 MACHINE_START(OMAP_GENERIC, "Generic OMAP1510/1610/1710")
    ' `$ f; {" ~: P8 ^+ Q, R- L
  13.      94         /* Maintainer: Tony Lindgren <tony@atomide.com> */, u4 D' o; ~/ K/ l
  14.      95         .phys_io        = 0xfff00000,' H6 S4 z( j5 L. Y) F/ R. L# j
  15.      96         .io_pg_offst    = ((0xfef00000) >> 18) & 0xfffc,
    ! V8 b$ h" [2 M, M3 e+ ]
  16.      97         .boot_params    = 0x10000100,  V# ~' U5 v  j% {2 g) M( P& S
  17.      98         .map_io         = omap_generic_map_io,& e2 G7 D, `7 N
  18.      99         .init_irq       = omap_generic_init_irq,
    5 z& h: M  _, u
  19.     100         .init_machine   = omap_generic_init,
    : A  S8 c3 m/ M. i" S4 r: C4 y% [
  20.     101         .timer          = &omap_timer,2 V5 Z# g, n, w' q4 ~8 }7 Y
  21.     102 MACHINE_END9 |" u6 L5 K4 O+ i7 _
  22. 8 D3 |; {1 V0 Q' u* `, m  D
  23.     /* func */
    5 G6 n! J- W6 w
  24.     204         .type   __lookup_machine_type, %function7 S% v! w0 h- f
  25.     205 __lookup_machine_type:- f1 A" [8 [) Z0 X3 Z0 @
  26.     206         adr     r3, 3b! M! g- c) a6 _* j2 d7 y' ^
  27.     207         ldmia   r3, {r4, r5, r6}9 A. h4 N% ^! p5 H3 M0 G, L( V
  28.     208         sub     r3, r3, r4                      @ get offset between virt&phys
    # M' J; ~1 Z+ M6 I2 l7 v% v
  29.     209         add     r5, r5, r3                      @ convert virt addresses to* X+ V! i2 s2 J4 s
  30.     210         add     r6, r6, r3                      @ physical address space+ Y& x& G6 N1 |( v9 o
  31.     211 1:      ldr     r3, [r5, #MACHINFO_TYPE]        @ get machine type6 y' m5 U5 ^; n5 w5 l  _( o: z
  32.     212         teq     r3, r1                          @ matches loader number?
    . g7 z/ _5 o. s9 n& c& S
  33.     213         beq     2f                              @ found1 @0 g+ R5 W" d, Y9 J; Y$ I
  34.     214         add     r5, r5, #SIZEOF_MACHINE_DESC    @ next machine_desc
    : @. W2 I% b& a0 r! F
  35.     215         cmp     r5, r62 L7 ^# ]0 j* Q5 B
  36.     216         blo     1b
    / J0 W) }) ?& Z' F3 }
  37.     217         mov     r5, #0                          @ unknown machine7 o+ |* E, t( l& x% J8 {
  38.     218 2:      mov     pc, lr
複製代碼
4#
 樓主| 發表於 2008-10-13 17:56:46 | 只看該作者
接著我們又返回到head.S,
" y2 h2 ^, j5 X- P6 f# D4 gline 87~88也是做check動作。5 U6 W: C  [8 G* D8 Z% z
line 89跳到vet_atags。在head-common.S
  1.      87         movs    r8, r5                          @ invalid machine (r5=0)?
      x, a3 s8 w% J6 K; ?
  2.      88         beq     __error_a                       @ yes, error 'a'
    5 `* P2 b$ c/ r
  3.      89         bl      __vet_atags
    0 H$ B. L" r/ i$ @9 H
  4.      90         bl      __create_page_tables
複製代碼
line 245, tst會去做and動作。並且update flags。這邊的用意是判斷位址是不是aligned。
; B* \6 `! y9 Z3 G  y: Kline 246, 沒有aligned跳到label 1,就返回了。$ I6 ^9 O1 r. a0 ^9 T
line 248~250, 讀取atags所在的address裡頭的值到r5,看看是否不等於ATAG_CORE_SIZE,不等於的話也是返回。% ~" R5 }5 a' e
line 251~254, 判斷一下第一個達到的atag pointer是不是等於ATAG_CORE。如果正確的話,等一下要讀取atag的資料,才會正確。, f$ }# H( l0 Q! T: d2 P
(atag是由bootloader帶給linux kernel的東西,用來告知booting所需要知道的參數。例如螢幕寬度,記憶體大小等等)
  1.      14 #define ATAG_CORE 0x54410001; A! r: |  @7 d0 s- i
  2.      15 #define ATAG_CORE_SIZE ((2*4 + 3*4) >> 2)3 s4 ?: G5 W% G3 u+ u# N) Y, }$ {1 J

  3. : ^1 q$ w7 R  _6 h& t
  4.     243         .type   __vet_atags, %function+ n  ^/ e/ ~6 u% E7 a: e
  5.     244 __vet_atags:0 n& x' d; C* t# t* G1 ~
  6.     245         tst     r2, #0x3                        @ aligned?0 h! f$ v9 I( p! d7 k# Z
  7.     246         bne     1f
    + a" b% }, A* s3 T/ i( l, B6 K
  8.     247% P: a' S$ e; @0 K
  9.     248         ldr     r5, [r2, #0]                    @ is first tag ATAG_CORE?
    . x( \* a4 w& _* v( a, p
  10.     249         subs    r5, r5, #ATAG_CORE_SIZE
    3 F0 J6 V8 F5 v9 E5 P
  11.     250         bne     1f1 r8 r8 {6 @* w
  12.     251         ldr     r5, [r2, #4]
    % ^  C4 P5 v, g! i. H4 D1 G
  13.     252         ldr     r6, =ATAG_CORE+ }, e( }9 w- c+ W3 M
  14.     253         cmp     r5, r6* ^. j" T0 o8 t4 E5 g
  15.     254         bne     1f
    : o8 e* U% b* f, O1 `
  16.     2551 X$ N) Z% R0 b6 s$ s
  17.     256         mov     pc, lr                          @ atag pointer is ok
    + G5 e; u. l- N
  18.     257
    - C) j6 t. K+ ?
  19.     258 1:      mov     r2, #0
    ' m! c" y7 p( ]# M4 S
  20.     259         mov     pc, lr
複製代碼
接著我們又跳回去head.S。  + ]' I3 V! o- s( y4 f
line 90,又跳到 __create_page_tables。   (很累人....應該會死不少腦細胞)+ K7 Q7 g5 s: a# D
哇!page table?!!如雷貫耳的東西,不知道會怎麼做。剛剛偷看了一下,@@還蠻長了,先到這邊好了,下次在繼續寫。
5#
 樓主| 發表於 2008-10-14 12:13:51 | 只看該作者
由於code看起來似乎越來越難解釋,會需要在檔案之間跳來跳去。建議一些基礎的東西可以再多複習幾次(其實是在說我自己 ),閱讀source code上收穫會比較多。例如:
8 U7 K/ k2 ?7 ^# h) e0 s: Y  ~- o  Z" V$ U1 X, P
1. arm instruction set - 這個最好每個指令的意思都大略看過一次,行有餘力多看幾個版本,armv4, v5 or v6。
: D6 U$ y2 W+ j2. compiler & assembler & linker - toolchain工具做的事情和功能,大致的流程和功能要有概念。
% o. m5 a* Q+ r2 m. s0 I( j/ x/ b. R3. Makefile & link script - 這兩個功能和撰寫的方式要有簡單的概念。
  q! b0 ?0 K" _" K$ H( @+ s
: `; V. V1 i' A% H- X2 \1 p9 ~以上1是非常重要的重點,2&3只要有大致上的概念就可以,因為trace code的時候,有時需要跳到Makeflie&Link script去看最後object code編排的位址。
  \- ]! h8 M/ M
' m  R4 s) }; d+ A/ Z+ ]由於我們trace到了page table這個關鍵字,在開始之前,稍微簡短的解釋,為了幫助了解,儘量用易懂的概念講,有些用詞可能會和一些真實狀況不同,但是懂了之後,應該會有能力分辨,至於正式而學術上解說,很多書上應該都有,或是google一下就很多啦∼
6#
 樓主| 發表於 2008-10-14 12:14:47 | 只看該作者
page table本身是很抽象的東西,尤其是對所謂的 user-mode application programmer 來說,大部分的狀況它是不需要被考慮到的部份,但是他的產生卻對os和driver帶來了很多影響。我們從一個小小的疑問出發。
1 p6 n" j# g: `2 H7 {! L+ ?  u6 ^: T/ B- U% A
『產生page table到底是要給誰用的?』3 m% y: D5 t* w4 k* `& T) M
" b! |, t9 t- o$ m- E6 Y6 L# v
其實真正作用在它上面的H/W是MMU,一旦CPU啟用了MMU,當cpu嘗試去記憶體讀取一個operand的時候,cpu內部打出去的位址訊號都會先送到mmu,mmu會拿著這個位址去對照page table,看看這個位址是不是被轉換到另外一個位置(還會確認讀寫權力),最後才會到真正的位址去讀寫。
7 A. D4 N( Y" F( f; b( e4 }4 ]/ n5 E+ c
這樣來看CPU其實一開始打出去的位址訊號其實不是最後可以拿到資料的位址,我們稱為virtual address(va),mmu打出來的位址才能真正拿到資料,所以我們把mmu打出去的位址稱為physical address(pa)。那用來查詢這個位址對照關係的表格,就是page table。
( o$ _+ R6 {% U' V" Q* e' a3 d3 B1 S- i
到這邊我們有一個簡單的概念。來想像一個小問題,一個普通的周邊(例如你的顯示卡),因為他並不具有MMU功能,hw只看得懂pa,但是CPU卻是作用在va,假如我想去對hw的控制暫存器做讀寫,到底要用va還是pa?
7#
 樓主| 發表於 2008-10-14 12:15:51 | 只看該作者
這時,寫driver的人就必須要小心,設定給硬體看的位址必須要先轉成pa(像是設定DMA),給CPU執行的程式碼要使用va,這對一開始嘗試寫driver但是又不瞭解va pa之間的差別的人,常常產生很多疑問。* \0 t+ ?/ x2 o- ?

) _% X& V5 i0 F+ K現在我們回頭想想OS,既然我們跑到create page table,可以預期的是他想要將MMU打開,因此希望預先建立起一個page table,讓MMU知道目前os想規劃的位址對應和讀取權力是如何被安排的。所以os必須考慮到現在的系統究竟是長怎樣?應該要如何被安排?是不是有那些要被保護?好吧∼因為我們完全對os不了解,也不知道該安排什麼,只能祈禱在trace code完後,可以找到這些問題的答案,或者發現一些沒想到的問題。
9 q+ v" `2 w5 T; L, u2 s( H% u; ^5 i% `0 r9 n
知道了page table的大致上的功能,下篇就可以專心的研究這個table的長相,和它想規劃出的系統模樣。
4 F3 I, Z) T# N& J5 _3 @9 m& {4 J  k1 ?
p.s. 字數限制好像變短了。   (看來很難寫)
8#
 樓主| 發表於 2008-10-14 13:56:17 | 只看該作者
由於字數限制挑整,用詞儘量精簡,以便可以貼必要source。另外,以後寫到一段落,考慮收集成一篇完整的文章,這樣應就不會因為用回覆的方式,造成閱讀斷斷續續的問題。希望有人對文章呈現方式有想法的話,可以跟我講。
9#
 樓主| 發表於 2008-10-14 13:57:39 | 只看該作者
現在,讓我們跳入create_page_tables吧∼
+ x% V9 Y# k# `& Y( N2 bline 216,會跳到pgtbl的macro去執行,其實只是載入一個位址。
; y7 ]. ]# f. \, M
) z% l% z0 F/ X6 S/ w9 l- 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 */3 m7 v3 Z6 [4 z
  2.      95 textofs-y       := 0x00008000- X# C! k) i& \+ T5 }* [
  3.     152 TEXT_OFFSET := $(textofs-y)
複製代碼
  1. /* include/asm-arm/arch-omap/memory.h */
    $ w4 X7 ^8 g5 S' q
  2.      40 #define PHYS_OFFSET             UL(0x10000000)2 _4 L8 H5 O) u1 E1 {) J
  3. 5 z2 ^) f, C% d8 L
  4.      /* arch/arm/kernel/head.S */
    8 H# g# B' B# s; y% q
  5.      29 #define KERNEL_RAM_VADDR        (PAGE_OFFSET + TEXT_OFFSET)2 e7 s/ l: U+ A0 Y
  6.      30 #define KERNEL_RAM_PADDR        (PHYS_OFFSET + TEXT_OFFSET)
    & F  h5 F' G- l7 \
  7. 8 K- W- M8 R0 d) u3 {- V
  8.      47         .macro  pgtbl, rd: E; o7 B4 j9 W' v7 B$ `7 M
  9.      48         ldr     \rd, =(KERNEL_RAM_PADDR - 0x4000)5 a: `2 T7 H% ^
  10.      49         .endm
    8 p3 v7 Q% t% b  w9 H& |
  11. 1 }" v+ i1 x; o5 f# m; f* j
  12.     216         pgtbl   r4                              @ page table address
複製代碼
10#
 樓主| 發表於 2008-10-14 14:16:53 | 只看該作者
得到pg table的開始位置之後,當然就是初始化囉。! m4 n6 {& k2 q0 o& ?2 @) c
line 221, 將pg table的base addr放到r0.* T# E# Y7 B, J& |4 ^
line 223, 將pg table的end addr放到r6.; ~. a6 s: _# N! H* z
line 224~228, 反覆地將0x0寫到pg table的區段裡頭,每個loop寫16bytes. (4x4),直到碰到end,結果就是把它全部clear成0x0.
  1.     221         mov     r0, r4
    ) U* u( X- ^: n% `# K: g3 e# u
  2.     222         mov     r3, #0  O. A# K) c1 C/ K! m# z' {
  3.     223         add     r6, r0, #0x4000! k1 e# z& q! I! Y
  4.     224 1:      str     r3, [r0], #4- N7 @) d6 T8 H, ^7 F8 Y
  5.     225         str     r3, [r0], #4' d2 m/ p5 A7 _/ u5 w
  6.     226         str     r3, [r0], #44 c% _' ~8 B& e; n: O1 k: V
  7.     227         str     r3, [r0], #40 _4 u% m2 W' u6 J" P
  8.     228         teq     r0, r6* m8 {' x9 ]# `9 Q9 n9 k$ x* O4 J
  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 | 只看該作者
問題怎麼填值??  h6 Y4 F5 M: l$ X: M3 X1 `! g$ |
拿出ARM的手冊,翻到MMU章節。一看發現page有很多種,還得分first level和second level。
7 C9 H! M4 F/ X& c9 r  ^
) A4 E: `9 U5 a, {念書時的印象,是從1st level在去查2nd level,先看怎麼設定1st level (不知以前老師有沒有亂教)6 ]0 l  X9 o3 t5 o
1. [31:20]存著section base addr
, g3 u3 N3 |( L2. [19:2]存著mmu flags
. E. N' ~- w. A2 o+ f* {3. [1:0]用來辨別這是存放哪種page, 有四種:
0 r8 l) Y4 m9 s   a. fault (00) b. coarse page (01) c. section (1st level) (10) d. fine page (11)
! K6 O) Z2 M5 N7 n8 z, x4. pg tabel資料要存放到 [31:14] = translation base, [13:2] = table index , [1:0] = 0b00 的位址4 }; F! Y' X* m4 E

. A6 ]6 F3 u- V  P- z$ n來看code是怎麼設定。6 u9 R, M5 F* r( s/ Q( @0 ~

+ d* L! J& ]/ `7 C% nline 239, 將pc的值往右shift 20次放到r6.這是取得前20高位元的資料,有點像是 1.。
. Y7 I$ v& w- I; Lline 240, 將r6往左shift 20次之後,和r7做or的動作,剛好也是 2.提到的mmu flags和1.處理好的資料做整理。
: @0 N- P" I8 E! z: Q* g* [4 g1 X所以前面兩個做完,就完成了bit[31:2]。  S$ ?4 {. @7 r! n
line 241, 將r3的資料寫到r4+(r6<<0x2)的地方去,剛好是4.提到的位址。(lucky)
  1.     239         mov     r6, pc, lsr #200 _% _+ v1 l9 I. Z$ N
  2.     240         orr     r3, r7, r6, lsl #20
    # q6 r9 }5 }3 o+ s
  3.     241         str     r3, [r4, r6, lsl #2]
複製代碼
p.s. 用pc值剛好可以算出當前的page。
12#
 樓主| 發表於 2008-10-22 19:47:03 | 只看該作者
最近又被釘上了,開始忙碌,大概沒太多時間貼文∼7 `) b. K& H" M# T

6 m' _! J4 x$ y% ]+ G" X上篇已經將pc所屬的page table entry(pte)設定好,接著繼續看7 h: }$ j8 y8 a! v$ {; H: b; z% y
line 247, 248, 將KERNEL_START的位址往右shift 18算出pte的offset,(等於line239&241,239&241是先shift right 20然後shift left 2),並將剛剛r3的值設給pte。『!』的意思是會把address存到r0。3 u9 c$ R6 s, e, f3 C. b9 z8 Q
! I# b$ o* O/ F% p2 \* C/ Z! q
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* S. P( i3 H" u4 {# L9 W; K7 n) W
  2.     248         str     r3, [r0, #(KERNEL_START & 0x00f00000) >> 18]!   q8 C( |! J9 Y5 j7 T9 Z
  3.     249         ldr     r6, =(KERNEL_END - 1)
    2 v7 `" V& c) Q( n4 m2 v6 z
  4.     250         add     r0, r0, #4
    + h% F, b- x2 l  P3 W3 X  X2 F9 y
  5.     251         add     r6, r4, r6, lsr #18
    - u- R% O7 f8 t$ J% @6 o( f6 r. h
  6.     252 1:      cmp     r0, r65 q) e% ~: ]+ A1 d0 w& L
  7.     253         add     r3, r3, #1 << 20
    % C3 a' W9 t4 X5 h
  8.     254         strls   r3, [r0], #4
    6 k6 ~7 b4 b2 A" j& n
  9.     255         bls     1b
複製代碼
13#
 樓主| 發表於 2008-10-22 20:24:58 | 只看該作者
line279,PAGE_OFFSET是規範RAM mapping完後的virtual address,我們將這個位址所屬的pte算出來放到r0。7 n5 `+ `  a9 I0 _
line 280~283,將要 map 的physical address的方式算出來放到r6。
2 B0 X; K9 J) K7 Xline 284,最後將結果存到r0所指到的pte。7 j: @" T$ i8 H2 ?. c

9 w5 G% N: X7 w% O以上三個動作,就是做好 ram 起頭的位址一開始map,還沒做完整塊map。由於我們目前的設定方式每塊是1MB,所以這邊是將RAM開始的1MB做map。4 v4 U) g% A7 Z" h+ |/ q" l& p

) q& ], c! [; X: y3 ?line 327,返回,結束一開始的create page table的動作,我們可以看出其實並沒有做完整的初始化page table,所以之後應該會有其他page table的細節。
  1.     279         add     r0, r4, #PAGE_OFFSET >> 18
    2 e1 G% O) ^) a
  2.     280         orr     r6, r7, #(PHYS_OFFSET & 0xff000000)# E& h( N! }/ P/ v9 q' B3 ?
  3.     281         .if     (PHYS_OFFSET & 0x00f00000)2 _9 d* ?/ Q5 b
  4.     282         orr     r6, r6, #(PHYS_OFFSET & 0x00f00000)
    ( K3 G! r, E  f# e
  5.     283         .endif: o% B8 J. M6 [% |! f
  6.     284         str     r6, [r0]" r5 S( C# U2 ]3 W- z' Q
  7.     327         mov     pc, lr
複製代碼
附帶一提,我們這邊省略了一些用ifdef包起來的程式碼,像是一開始會印一些output message的程式碼(line286~326)。
14#
 樓主| 發表於 2008-10-22 20:37:08 | 只看該作者
自create page table返回後,我們偷偷看一下接下來的程式碼,% u0 |' z* v* [# L  a
line 99, 將switch_data擺到r13/ y- h2 p0 Z" p; Y2 L4 ?
line 101, 將enable_mmu擺到lr) K1 d& p* j7 l% w6 R) d
line 102, 將pc改跳到r10+PROCINFO_INITFUNC的地方去0 K5 s: N( l. z# b8 K
8 j& k' a; V5 H2 t6 ~. S, A2 J5 T
其實這邊有點玄機,switch_data和enable_mmu都是function,結果位址都只是被存起來,並沒有直接跳過去執行。其實,雖然程式碼是順序switch_data->enable_mmu->proc init function,但其實執行的順序會是 procinfo 的init function-> enable_mmu -> switch_data 。至於,為什麼要這樣寫的原因?就不清楚了,也還沒仔細推敲過。
) Y8 X0 w  z' I6 g5 O0 V, Q# O" x  g+ c! m
switch_data最後就會跳到大家都很熟悉的start_kernel(). 詳細要賣個關子,最近會忙一下,得要過一陣子才能貼文∼  
  1.      99         ldr     r13, __switch_data              @ address to jump to after3 D" s: x% n) H) X- u
  2.     100                                                 @ mmu has been enabled
    % z( Z- f8 e9 G0 j, H
  3.     101         adr     lr, __enable_mmu                @ return (PIC) address" v3 S1 |. v5 t
  4.     102         add     pc, r10, #PROCINFO_INITFUNC
複製代碼
15#
 樓主| 發表於 2009-7-4 01:09:36 | 只看該作者
老店重新開張~
# z7 U% d! f# v- z/ V: N0 S. g/ f/ l: W- H: a* J4 v+ [
花了一些時間把舊的貼文整理到一個blog
. |1 |8 b5 v6 X4 W3 q* k3 p1 J有把一些敘述修改過
& s- I: S8 j. i, X希望會比較容易集中閱讀
/ i( L, S" p' a目前因為某些敘述不容易" ?; {; Y# g8 M
還是比較偏向筆記式而且用字不夠精確$ A/ I+ t, Y, K1 D
希望之後能夠慢慢有系統地整理' Q, ^( T: q$ t
大家有興趣的話
# ?! E6 m0 ^1 Z# m可以來看看和討論 " Q2 k2 e0 y  G9 ^* v% L6 I
http://gogojesseco.blogspot.com/6 A6 G- Z; t8 j4 d6 O1 Y7 }  ~
1 H2 e- C7 K# R* a$ h
以後可能會採取  先在chip123貼新文章* e5 `1 s, ~4 U  {
慢慢整理到blog上的方式) z/ Y, }2 v3 X4 \% c( \
因為chip123比較方便討論 =): i$ O2 m3 W+ W8 ~2 e
blog編輯修改起來比較方便
* O) q+ S$ D( I/ n: u3 p閱讀也比較集中   大家可以在這邊看到討論
8 n# L8 ~) y2 N, `% O4 V, s0 n# e然後在blog看到完整的文章 (類似BBS精華區的感覺)

評分

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

查看全部評分

16#
 樓主| 發表於 2009-7-15 17:07:03 | 只看該作者
隔了很長一段時間沒update
6 O! L! h( Q! O- N" n之前程式碼走到 ./arch/arm/kernel/head.S 的 line 99 附近
  1.      99         ldr     r13, __switch_data              @ address to jump to after/ `: X/ s/ u( b3 ^, ]4 [% a! x
  2.     100                                                 @ mmu has been enabled
    $ H" f9 ~+ }% W- K5 s
  3.     101         adr     lr, __enable_mmu                @ return (PIC) address
    . \: T+ M- R+ P4 C0 }: m' b% q
  4.     102         add     pc, r10, #PROCINFO_INITFUNC
複製代碼
line 99, 將__switch_data放到r13。(留作之後用)- V& G& n% Z2 G& S9 @
line 101, 將__enable_mmu的addr放到lr。(留作之後用)! V% I* R& A: ~0 D2 ^/ y
line 102, 將 r10+#PROCINFO_INITFUNC 放到pc,也就是jump過去的意思。r10是proc_info的位址。PROCINFO_INITFUNC則是用之前提過的技巧,指向定義在./arch/arm/mm/proc-xxx.S的資料結構,以arm926為例,最後會指到
  1. 463         b       __arm926_setup
複製代碼
所以程式碼就跳到了 __arm926_setup。
  1. 373         .type   __arm926_setup, #function
    6 m/ C8 E& n% u! e# q
  2. 374 __arm926_setup:
    * H) d! ?; [( Y2 i3 U" K; R) a
  3. 375         mov     r0, #0
      J1 j4 N+ M2 F$ ?. {; ~4 B+ N4 n
  4. 376         mcr     p15, 0, r0, c7, c7              @ invalidate I,D caches on v4
    " I4 f) w7 g! h  i9 F8 H4 [
  5. 377         mcr     p15, 0, r0, c7, c10, 4          @ drain write buffer on v48 \% ?/ m( A8 x
  6. 378 #ifdef CONFIG_MMU' n6 {. N% {1 w  Y; e
  7. 379         mcr     p15, 0, r0, c8, c7              @ invalidate I,D TLBs on v4
    ) L! r1 h( k, B+ Y+ C5 r
  8. 380 #endif
    4 s6 C+ D* ]* z3 s- u
  9. 3 l% C' N5 o$ m& b
  10. 388         adr     r5, arm926_crval  T9 {% b& i. t# G
  11. 389         ldmia   r5, {r5, r6}
      F( [) C: ?% D2 ~; {% g4 P
  12. 390         mrc     p15, 0, r0, c1, c0              @ get control register v4
    . c$ r. Z. _0 k9 Q3 e$ b
  13. 391         bic     r0, r0, r50 U# C# M# `/ S0 b" V
  14. 392         orr     r0, r0, r6
    1 m7 ~! |6 i7 E

  15. - d' y2 l+ P7 l& g: U
  16. 396         mov     pc, lr1 k1 m& U* `2 j; _
  17. 397         .size   __arm926_setup, . - __arm926_setup
複製代碼
這邊的程式碼就跟CPU有很大的相依性,, A$ ]$ N  i& D" V( x! A' o
line 375~380, 主要就是invalidate CPU的I&D cache和清空write buffer。) U: j# D; O; p7 @9 m2 w
line 388~392, 把cp15的設定讀出來,並且將一些預設狀態做好運算放到r0。(預設值從arm926_crval這邊可以取得。)
) I9 F( z4 ?# C7 Iline 396, 直接把pc跳到lr,因為我們之前已經將lr = enable_mmu,所以直接跳過去。
17#
 樓主| 發表於 2009-7-15 17:29:45 | 只看該作者
  1. 155 __enable_mmu:" q$ L( h2 e. O2 Q
  2. 170         mov     r5, #(domain_val(DOMAIN_USER, DOMAIN_MANAGER) | \
    8 ^4 S' Y8 n2 P
  3. 171                       domain_val(DOMAIN_KERNEL, DOMAIN_MANAGER) | \* y4 s  a3 h' |- \$ [: A3 n
  4. 172                       domain_val(DOMAIN_TABLE, DOMAIN_MANAGER) | \6 N; T0 [& I* b& f9 |5 o9 Q: X
  5. 173                       domain_val(DOMAIN_IO, DOMAIN_CLIENT))
    6 O) l: j* \! l
  6. 174         mcr     p15, 0, r5, c3, c0, 0           @ load domain access register
    % _- b+ V# h7 W, e- P1 K& Z, h
  7. 175         mcr     p15, 0, r4, c2, c0, 0           @ load page table pointer
    3 @# _4 C* H" S! _/ f( l- o( l
  8. 176         b       __turn_mmu_on
    7 n- Z' z+ y+ c+ [& w
  9. 177 ENDPROC(__enable_mmu)
複製代碼
line 170~174,設置好domain access。(domain access可以先當成設置access的權限,或許以後可以寫詳細的文章說明)
: T" Z, @7 B- W: Gline 175~176,將create好的page table開頭丟給mmu。跳到turn_mmu_on
  1. 191 __turn_mmu_on:
      Z7 q8 z) E1 \3 _5 g% w! R0 i3 X  b
  2. 192         mov     r0, r0
    : J) d7 K( X1 [8 u! L7 H; ]% Q
  3. 193         mcr     p15, 0, r0, c1, c0, 0           @ write control reg% k; r; n" j4 ]4 G, Z5 q" H
  4. 194         mrc     p15, 0, r3, c0, c0, 0           @ read id reg* R1 H& I$ j* j* @# l
  5. 195         mov     r3, r3/ w# K9 a3 e7 N/ w
  6. 196         mov     r3, r3+ J$ ]; q  c* F- M- V
  7. 197         mov     pc, r13
    + M4 y0 M8 e7 b; V0 J7 ~
  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:
    / U" s+ ]0 o! t* V- c! F& I
  2. 19         .long   __mmap_switched
    1 S, S1 [$ D" Y+ H) T: C! r7 ^
  3. 20         .long   __data_loc                      @ r4' @; Y' g$ i' t4 B
  4. 21         .long   _data                           @ r5
    & }, n3 R4 B; }0 x6 y/ _; B
  5. 22         .long   __bss_start                     @ r62 t5 g+ u  Z  O0 ]* C$ H/ b( j9 Z
  6. 23         .long   _end                            @ r7' }) ?  [4 d# i
  7. 24         .long   processor_id                    @ r44 }$ `4 P: |* i# X7 J1 W( s; K7 Q. N
  8. 25         .long   __machine_arch_type             @ r55 B0 e! T1 `( g& l- L; S, H7 {
  9. 26         .long   __atags_pointer                 @ r6
    ( \, y2 @* {! p4 U9 u8 p7 d( K
  10. 27         .long   cr_alignment                    @ r7) }9 O6 @$ A: W( R# Q) c3 [
  11. 28         .long   init_thread_union + THREAD_START_SP @ sp
    ; G2 s; Q# D! m  J; x9 Q+ C2 Y
  12. 29
    1 ^" ^# }; b/ K5 p; V2 y8 a2 S+ \
複製代碼
switch_data的第一行就是 __mmap_switched,所以我們直接看line 39。
18#
 樓主| 發表於 2009-7-15 17:30:00 | 只看該作者
  1. 39 __mmap_switched:/ V& z' F+ s% ~+ L+ m, d6 X
  2. 40         adr     r3, __switch_data + 45 a  e# O; T" Y( K- Y
  3. 41
    $ M( h; p$ L) k& x9 e5 e" u7 v; Z
  4. 42         ldmia   r3!, {r4, r5, r6, r7}9 F. t' Q4 |' I9 `) F" {
  5. 43         cmp     r4, r5                          @ Copy data segment if needed6 g; K0 _# Y% ]9 u/ D8 }! D( d
  6. 44 1:      cmpne   r5, r6" X- ^0 }7 ?1 }5 ]5 x
  7. 45         ldrne   fp, [r4], #4, {+ ^1 d7 H) O6 ?
  8. 46         strne   fp, [r5], #4
    ' a$ z0 ^2 n9 z& t) d# D( p
  9. 47         bne     1b& |9 d/ H/ i* @$ Q
  10. 48
    ) ]5 O# ?3 G6 F6 h% {" A
  11. 49         mov     fp, #0                          @ Clear BSS (and zero fp)
    ' \. d" }' V  J9 {5 ^9 T- i
  12. 50 1:      cmp     r6, r7/ k  C0 K/ Y# |2 O
  13. 51         strcc   fp, [r6],#45 a, L% l! Z% l1 D8 l' ]3 J
  14. 52         bcc     1b7 A+ d' o) Q% \2 g9 G7 T+ P8 G$ H
  15. 532 V! S% F' I4 n: I9 X
  16. 54         ldmia   r3, {r4, r5, r6, r7, sp}
    8 ~3 s. w+ W0 @: J7 v' U  j, p
  17. 55         str     r9, [r4]                        @ Save processor ID
    ) W" q6 p' o: r3 F
  18. 56         str     r1, [r5]                        @ Save machine type
    % }& r1 o6 w! B! i
  19. 57         str     r2, [r6]                        @ Save atags pointer* D/ [, z  O0 N* n0 k' V9 I
  20. 58         bic     r4, r0, #CR_A                   @ Clear 'A' bit5 G2 k; |7 p7 @% _
  21. 59         stmia   r7, {r0, r4}                    @ Save control register values$ z( d6 X) O) O% G1 x, S
  22. 60         b       start_kernel* M% e/ X* D- U0 \, i  t
  23. 61 ENDPROC(__mmap_switched)
複製代碼
switch_data的第一行就是 __mmap_switched,所以我們直接看line 39。+ M; Q7 a1 e" q9 X4 Y6 p
line 39,將__data_loc的addr放到r3
1 f. j; t. ^5 a' d3 o* y& Qline 42,從r3的位址,連續讀取四筆資料到r4, r5, r6, r7
' \/ T; d' R3 m' y' ~2 G9 ]line 43~47,看看data segment是不是需要搬動。
* C9 U' l# [0 Q* \* B. V( V! Lline 49~52, clear BSS。
  \9 N0 A3 c/ P  p0 y8 p
' E+ r. M' n1 f$ t7 K% q由於linux kernel在進入start_kernel前有一些前提必須要滿足:1 g$ u. [6 Y1 Y
r0  = cp#15 control register
7 v1 Z1 {' u/ P5 U: F" zr1  = machine ID" O  y4 P7 J# C0 R$ N3 s1 t4 W+ J
r2  = atags pointer  X- B! |0 E8 I4 Z) c( t
r9  = processor ID
! }5 h& M3 M7 s* q
6 {5 C# g/ p* b! M! e) d6 s所以line 54~59就是在做這些準備。5 E4 o0 @# Z- s
最後呢? 我們就跳到start_kernel了。(而且還是用b start_kernel,表示我們不會再回來了)( L1 _: _/ Z- l* @
! T- p5 [) g5 ?- A( [: A
看一下start_kernel()在./init/main.c,終於跳出architecture specific的目錄,表示
0 i$ t: K' w  k) o" u6 u我們真正的開始linux kernel的初始化。- i& Z( e7 K" c4 R" Q5 U, e: T
像是 shedule init, console init, memory init, irq init等等都在start_kernel裡頭。4 Q  A7 O6 Y2 @. h: y: L
到這邊之後,應該就可以深入linux kernel中,各項比較跟hardware不那麼相關的軟體部分。

評分

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

查看全部評分

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

本版積分規則

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

GMT+8, 2024-9-21 06:22 AM , Processed in 0.225013 second(s), 19 queries .

Powered by Discuz! X3.2

© 2001-2013 Comsenz Inc.

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