Chip123 科技應用創新平台

 找回密碼
 申請會員

QQ登錄

只需一步,快速開始

Login

用FB帳號登入

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

trace linux kernel source - ARM - 03

[複製鏈接]
跳轉到指定樓層
1#
發表於 2008-10-7 14:00:18 | 只看該作者 回帖獎勵 |倒序瀏覽 |閱讀模式
到目前為止,我們已經進展到kernel幫自己relocate完,並解將自己解壓縮到一個地方要準備開始執行,那疑問來了?到底是跳到哪裡去了,因為./compressed/head.S最後一行居然是
& l9 ~5 D% I5 S3 Z『mov pc, r4』
- ]6 S2 U9 f& I# n9 y2 L. \+ I, mr4只代表了解壓縮完後kernel的位址,那究竟整包kernel編譯的時候,哪個function哪個東西被放在最前面咧?!
& ?: A( u# W8 Y* M; C9 m! L. D
  X; C- u# L5 H所以我們又必須開始找於kernel link的時候是怎麼被安排的,有了前面的基礎,我們可以從Makefile知道程式碼如何被編譯。至於link上的細節,例如有那些section和section先後順序等等,可以從 lds 檔來規範。) |$ [* t& {( _% ~: U

) F" I& b6 q7 L  Y8 K有興趣的人可以看一下 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)' K: J6 `1 t5 @% J6 M; S
我們可以發現第一個section是『.text.head』,裡頭的_stext從目前的位置開始放。
* r1 F' _/ b- _6 T於是我們曉得只要找到屬於.text.head這個section,並且是_stext這個symbol的程式碼,就是解壓縮完後的第一行程式碼。
  1.      26     .text.head : {0 K  q7 P. b9 \: F8 @/ t
  2.      27         _stext = .;
    # e# z# d; l/ ^5 b8 x
  3.      28         _sinittext = .;
    - H" f; }/ B+ i
  4.      29         *(.text.head)
    5 k( G0 V; V" t# \9 a- Z
  5.      30     }
複製代碼
用指令搜尋一下,發現 ./arch/arm/kernel/head.S 裡頭有關鍵字(如下),結果我們從./arch/arm/boot/compressed/head.S跳到了./arch/arm/kernel/head.S
  1.      77     .section ".text.head", "ax"
    / b9 x2 J0 X$ |0 b9 t) b
  2.      78     .type   stext, %function6 J! X, F+ U6 X2 Q
  3.      79 ENTRY(stext)$ r/ ~$ s+ y& j
  4.      80     msr cpsr_c, #PSR_F_BIT | PSR_I_BIT | SVC_MODE @ ensure svc mode
    2 z* D+ ^% Z* z5 V8 ?
  5.      81                         @ and irqs disabled& a8 k0 I- ?  l
  6.      82     mrc p15, 0, r9, c0, c0      @ get processor id" L& Y1 O2 R- c1 \( _- L3 E
  7.      83     bl  __lookup_processor_type     @ r5=procinfo r9=cpuid1 Z  I# ]1 O7 }. K
  8.      84     movs    r10, r5             @ invalid processor (r5=0)?
    . H" f9 v- `& S1 f# X3 G
  9.      85     beq __error_p           @ yes, error 'p'
    ) D) y) w4 \2 R+ `
  10.      86     bl  __lookup_machine_type       @ r5=machinfo4 t3 A" z2 _8 {, K$ q
  11.      87     movs    r8, r5              @ invalid machine (r5=0)?
    7 x0 c) \, r- \( E' v' s1 j
  12.      88     beq __error_a           @ yes, error 'a'
    ; j' k) `& v, c  p
  13.      89     bl  __vet_atags( g; h$ o" ?" G- ]) F
  14.      90     bl  __create_page_tables
複製代碼
既然找到了檔案,我們又可以開始繼續。# L( e/ b9 e- O- ?/ ?, u3 w
% m/ r: ^1 R7 `! ~
看了一下,程式碼多了很多bl的跳躍動作,看來會在各個function跳來跳去。
分享到:  QQ好友和群QQ好友和群 QQ空間QQ空間 騰訊微博騰訊微博 騰訊朋友騰訊朋友
收藏收藏 分享分享 頂 踩 分享分享
2#
 樓主| 發表於 2008-10-9 15:32:16 | 只看該作者
既然跳到真正的kernel開始跑,表示進入重頭戲,在進入kernel之前arm的平台有一些預設的狀況,也就是說arm kernel image會預設目前的cpu和系統的狀況是在某個狀態,這樣對一個剛要跑起來的OS比較決定目前要怎麼boot起來。
5 i. B5 N# J/ _. J) R( G* n4 B; C
4 @' H" K/ B2 ~& F6 }可以看一下comment,有清楚的描述。這也幫助我們了解為什麼decompresser的程式跑完之後,還要把cache關掉。
  1.      59 /*1 w/ S. z' i$ ^' V% S; d6 R
  2.      60  * Kernel startup entry point., Z+ D! ?- B  k( o; @" f) h6 Y. A
  3.      61  * ---------------------------- j+ H% _8 x$ w( A" r8 x
  4.      62  *
    ! p. T8 D2 L4 O, k2 p8 e6 ~& V4 d
  5.      63  * This is normally called from the decompressor code.  The requirements
    % o& |* t1 b7 V& T
  6.      64  * are: MMU = off, D-cache = off, I-cache = dont care, r0 = 0,
    * ?+ L6 h" ]7 O5 F# A1 X
  7.      65  * r1 = machine nr, r2 = atags pointer.
複製代碼
基於以上的假設,當program counter指到kernel的程式的時候,就會從line 80開始跑。
" H+ ]' f' S. z" r3 L3 F. K% I6 ~line 80, msr指令會把, operand的值搬到cpsr_c裡面。這是用來確保arm cpu目前是跑在svc mode, irq&fiq都disable。(設成0x1就會disable,definition在./include/asm-arm/ptrace.h)
: v$ u; Z1 f! ^1 m3 q' |line 82, 讀取CPU ID到r9' Q; \* a% r5 m8 g5 {  c2 N
line 83, 跳到 __lookup_processor_type 執行(bl會記住返回位址)。
  1.      77     .section ".text.head", "ax"" \) N4 f6 r9 {! ^; s% g
  2.      78     .type   stext, %function
    4 J; I$ f3 S1 `9 l
  3.      79 ENTRY(stext)
    ( S+ h. \( D0 l% L" y9 ]
  4.      80     msr cpsr_c, #PSR_F_BIT | PSR_I_BIT | SVC_MODE @ ensure svc mode7 k4 g; e' K5 q3 b: A# n
  5.      81                         @ and irqs disabled+ c( C% \# G8 D$ A3 D
  6.      82     mrc p15, 0, r9, c0, c0      @ get processor id6 R& F, t6 ?7 P+ F) J8 h
  7.      83     bl  __lookup_processor_type     @ r5=procinfo r9=cpuid
複製代碼
接著會跳到head-common.S這個檔,
3 [8 i) h$ ~: I! Fline 158, (3f表示forware往前找叫做『3』label)將label 3的address放到r3。
& t  T8 P/ M, b7 s3 y) Fline 159, 將r3指到的位址的資料,依序放到r7, r6, r5.(ldmda的da是要每次都位址減一次)1 J  }) D, u) f) L8 g% ]
line l60, 161, 利用真實得到位址r3減去取得資料的位址得到一個offset值,這樣可計算出r5, r6真正應該要指到的地方。" k, s$ Z7 u) s  M7 F# |6 V" J4 _4 y
line 163~169,這邊的程式應該不陌生,就是在各個CPU info裡面找尋對應的structure。找到的話就跳到line 171,返回head.S9 H. f: E# l9 E/ B0 c8 x' ~7 v  ~
line 170, 找不到的話,r5的processor id就放0x0.表示unknown id。* R& d; ?9 Z, C2 \. d3 i
' u; j; n! i- ^3 @/ ?, t1 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$ p, X0 G! F9 ]& o/ L% N
  2.     157 __lookup_processor_type:
    " O1 |- Y- H" M: G2 q& H
  3.     158     adr r3, 3f0 q3 _5 Q3 R- F% q  o, i
  4.     159     ldmda   r3, {r5 - r7}# r0 F. }/ x6 x0 Q' j! q
  5.     160     sub r3, r3, r7          @ get offset between virt&phys6 e$ S. o4 n+ S
  6.     161     add r5, r5, r3          @ convert virt addresses to! l8 l* |" r' ^4 ]1 `
  7.     162     add r6, r6, r3          @ physical address space
    " M: D: d5 h! L+ f/ [: l6 p
  8.     163 1:  ldmia   r5, {r3, r4}            @ value, mask2 U9 z+ c/ w6 j; x1 W, \
  9.     164     and r4, r4, r9          @ mask wanted bits
    . i' q% w- v, i- F4 p9 y( D% D6 q  J
  10.     165     teq r3, r4
    4 J- q4 m$ K/ k: z2 {: @+ M
  11.     166     beq 2f
    ; ]. A1 p7 H2 ?6 R
  12.     167     add r5, r5, #PROC_INFO_SZ       @ sizeof(proc_info_list)/ O2 s. I  C, {* E. F- Q. w3 l5 e
  13.     168     cmp r5, r6
    & _& d$ k6 _. j: ]6 X% s6 g8 ~
  14.     169     blo 1b- o# u$ x3 {, P6 H% l6 D9 x
  15.     170     mov r5, #0              @ unknown processor
    5 a) |1 @  m/ w0 |( e- `* I4 T
  16.     171 2:  mov pc, lr
    ; I1 a9 l+ T" D4 _- C0 Z, ~/ }

  17. : P, z+ x# O3 W8 d7 A
  18.     187     .long   __proc_info_begin
    . E/ U- s' V' A+ E9 W9 |: A8 u
  19.     188     .long   __proc_info_end$ Q7 \# v+ g9 }) Q$ o
  20.     189 3:  .long   .
    + ^! F2 R2 u" t, u
  21.     190     .long   __arch_info_begin5 D# [. B- S; ^- o% U$ y
  22.     191     .long   __arch_info_end
複製代碼
跳了很多檔案,建議指令和object code如何link的概念要有,就會很清楚了。
3#
 樓主| 發表於 2008-10-13 17:20:35 | 只看該作者
我們從 head-common.S返回之後,接著繼續看。
8 r+ C' Q9 U: G% n/ \3 d& p8 `  ?- S9 c1 r, \  B
line 84, movs的意思是說,做mov的動作,並且將指令的S bit設起來,最後的結果也會update CPSR。這個指令執行的過程當中,會根據你要搬動的值去把N and Z flag設好。這個是有助於下個指令做check的動作。
- R0 ]" ~9 \6 tline 85, 就是r5 = 0的話,就跳到__error_p去執行。
! e# Z0 W. k+ Nline 86, 跳到__lookup_machine_type。原理跟剛剛找proc的資料一樣。
  1.      83         bl      __lookup_processor_type         @ r5=procinfo r9=cpuid* O" Z8 V3 E, B! P8 I4 A/ o
  2.      84         movs    r10, r5                         @ invalid processor (r5=0)?
    3 P# K) P8 p4 F+ Z
  3.      85         beq     __error_p                       @ yes, error 'p'
    9 c- d; U# H" R
  4.      86         bl      __lookup_machine_type           @ r5=machinfo
複製代碼
看得出來跟proc很像,有個兩個小地方是) b% m, o$ C0 L5 F! B

5 K- ]6 V1 R; {3 A1. line 207,用ldmia不是用ldmda。原因就在於存放arch 和 proc info 的位址剛好相反。一個在lable3的上面。一個在的下方。設計上應該是可以做修改的。
# C: N+ d, Y  G+ V9 q
! g6 t  h' M+ u2. arch定義的方式是透過macro,可以先看 ./include/asm-arm/mach/arch.h。裡頭有個 MACHINE_START ,這邊會設好macro,到時候直接使用就可以把arch的info宣告好。 例如 ./arch/arm/mach-omap1/board-generic.c
  1. /* macro */
    + g  K* B4 i7 F' L$ j; w
  2.      50 #define MACHINE_START(_type,_name)                      \
    ; }7 T: J1 N( T7 V
  3.      51 static const struct machine_desc __mach_desc_##_type    \/ y7 |9 E; u, A8 S& |8 I
  4.      52  __used                                                 \- R" n/ o. A4 w" Z
  5.      53  __attribute__((__section__(".arch.info.init"))) = {    \+ T4 D( v3 r; _; Q' o1 X% m
  6.      54         .nr             = MACH_TYPE_##_type,            \* T* ^. Z3 t" [7 _2 p
  7.      55         .name           = _name," \9 A% m4 z. W3 X; T4 w
  8.      56  f* s! t/ A! e2 g9 Q
  9.      57 #define MACHINE_END                             \
    6 T- F, z! S' f
  10.      58 };4 V" j' {9 H- [( Y0 y
  11.      /* 用法 */
    , P: {/ X: C4 b+ g- e& q- C! Z
  12.      93 MACHINE_START(OMAP_GENERIC, "Generic OMAP1510/1610/1710")& p; @& n( ?. q: }4 X
  13.      94         /* Maintainer: Tony Lindgren <tony@atomide.com> */
    0 m7 x8 \: b5 L
  14.      95         .phys_io        = 0xfff00000,' f" F( H# Y  ]+ T8 Q4 {  k, Y
  15.      96         .io_pg_offst    = ((0xfef00000) >> 18) & 0xfffc,
    . U- v' F- ]$ i8 x9 x
  16.      97         .boot_params    = 0x10000100,- ^7 J- O; A7 m4 Q$ V/ u
  17.      98         .map_io         = omap_generic_map_io,
    0 o: [: ]( \) w
  18.      99         .init_irq       = omap_generic_init_irq,, j& X: h; _! C) G& E/ _) H
  19.     100         .init_machine   = omap_generic_init,
    # J8 r3 S7 K& i0 {
  20.     101         .timer          = &omap_timer,7 H: H0 y- W) m4 g5 @
  21.     102 MACHINE_END) s+ B" L7 a2 W3 F' h) x
  22. , P) _. F+ ~* k; ?
  23.     /* func */
    8 m  |& W# m8 q& c5 E
  24.     204         .type   __lookup_machine_type, %function
    7 h- k) s; ?: l. [0 c" o
  25.     205 __lookup_machine_type:9 g  |1 m  H* R/ [3 W
  26.     206         adr     r3, 3b9 |  M, N" }% r' D
  27.     207         ldmia   r3, {r4, r5, r6}2 v1 Q0 G6 h" _9 C2 v' b
  28.     208         sub     r3, r3, r4                      @ get offset between virt&phys9 Q( Q. I4 D7 B+ H2 Z# N& a2 `
  29.     209         add     r5, r5, r3                      @ convert virt addresses to3 Y0 w+ E4 c; Z, y: n
  30.     210         add     r6, r6, r3                      @ physical address space
    ! ~4 ?' q6 c% l. H
  31.     211 1:      ldr     r3, [r5, #MACHINFO_TYPE]        @ get machine type
    ; O+ I9 C. c; J( l
  32.     212         teq     r3, r1                          @ matches loader number?* v- K+ G/ |! v: X: s: L
  33.     213         beq     2f                              @ found7 {: r) C, v9 u
  34.     214         add     r5, r5, #SIZEOF_MACHINE_DESC    @ next machine_desc3 H4 H  b7 ?5 ~) O% _( C; f
  35.     215         cmp     r5, r6
    . }* f, p7 Z" s/ b% Q
  36.     216         blo     1b8 w# ?2 w# M  f" l6 w% H8 c8 o! p
  37.     217         mov     r5, #0                          @ unknown machine
    : s# L  C: m" R1 Y0 t% t4 c0 o  k
  38.     218 2:      mov     pc, lr
複製代碼
4#
 樓主| 發表於 2008-10-13 17:56:46 | 只看該作者
接著我們又返回到head.S,
" `9 j+ u, V$ G( P8 Lline 87~88也是做check動作。+ x- e/ V% C* Z  X& T2 W1 q$ N# C
line 89跳到vet_atags。在head-common.S
  1.      87         movs    r8, r5                          @ invalid machine (r5=0)?3 H5 H) Q" K4 F9 ~& Y* j6 x
  2.      88         beq     __error_a                       @ yes, error 'a'
    2 b- B2 T! a+ }: k! L7 G
  3.      89         bl      __vet_atags
      t# }! k1 I# C
  4.      90         bl      __create_page_tables
複製代碼
line 245, tst會去做and動作。並且update flags。這邊的用意是判斷位址是不是aligned。
! T& H5 I' S6 W! |4 f* z& r; Lline 246, 沒有aligned跳到label 1,就返回了。
& s4 ~# b; q- zline 248~250, 讀取atags所在的address裡頭的值到r5,看看是否不等於ATAG_CORE_SIZE,不等於的話也是返回。
" W, i' R9 w7 m. P8 O7 R' O# ?7 }9 n" tline 251~254, 判斷一下第一個達到的atag pointer是不是等於ATAG_CORE。如果正確的話,等一下要讀取atag的資料,才會正確。$ }* Y: {0 ]+ i- s  O( W
(atag是由bootloader帶給linux kernel的東西,用來告知booting所需要知道的參數。例如螢幕寬度,記憶體大小等等)
  1.      14 #define ATAG_CORE 0x54410001  x$ d) ~( @8 t( N% x9 ~
  2.      15 #define ATAG_CORE_SIZE ((2*4 + 3*4) >> 2)
    ! N0 h/ a$ g# d  J) u3 P" y7 K

  3. , A6 x% ^, G5 D
  4.     243         .type   __vet_atags, %function
    + C4 r6 z' Q$ G& s9 `
  5.     244 __vet_atags:
    ; p4 x1 P  d! a( \0 o
  6.     245         tst     r2, #0x3                        @ aligned?0 X! J: ~5 l, J
  7.     246         bne     1f
    0 j6 Z, ~6 c$ l+ ]7 i+ K5 [# Q
  8.     247
    0 I/ r9 w% P9 E. s
  9.     248         ldr     r5, [r2, #0]                    @ is first tag ATAG_CORE?+ `4 A3 j$ C- T
  10.     249         subs    r5, r5, #ATAG_CORE_SIZE
    2 r% c4 ?# Z/ W# y
  11.     250         bne     1f! |, _6 ]- t% c$ m$ s5 |
  12.     251         ldr     r5, [r2, #4]4 u, q0 w' e3 d% R6 ?$ v# j
  13.     252         ldr     r6, =ATAG_CORE% y4 b* P4 z0 K  Y( Z
  14.     253         cmp     r5, r6
    9 v3 Q7 T# E% K' T1 q7 Y
  15.     254         bne     1f
    / j0 _/ A! M" ]* K/ M! b- d
  16.     255
    ! ~4 ~8 N! L/ W2 j2 y7 w: c0 N
  17.     256         mov     pc, lr                          @ atag pointer is ok
    ! g) Q2 p+ v$ Y# S
  18.     257* Z! O3 E2 B, S4 T% ?$ B6 C
  19.     258 1:      mov     r2, #0
    ' K( l% n; F: X: d1 c0 H; L
  20.     259         mov     pc, lr
複製代碼
接著我們又跳回去head.S。  
& l% v4 i$ y; E/ iline 90,又跳到 __create_page_tables。   (很累人....應該會死不少腦細胞)& s4 }& {/ x+ u# h: y
哇!page table?!!如雷貫耳的東西,不知道會怎麼做。剛剛偷看了一下,@@還蠻長了,先到這邊好了,下次在繼續寫。
5#
 樓主| 發表於 2008-10-14 12:13:51 | 只看該作者
由於code看起來似乎越來越難解釋,會需要在檔案之間跳來跳去。建議一些基礎的東西可以再多複習幾次(其實是在說我自己 ),閱讀source code上收穫會比較多。例如:: z" v8 X9 g6 B$ |
/ s2 s5 f" `' _& u4 J; w
1. arm instruction set - 這個最好每個指令的意思都大略看過一次,行有餘力多看幾個版本,armv4, v5 or v6。
9 p9 X: A. Q% S/ V" a, A2. compiler & assembler & linker - toolchain工具做的事情和功能,大致的流程和功能要有概念。# ?3 E6 g2 G1 V+ s5 x
3. Makefile & link script - 這兩個功能和撰寫的方式要有簡單的概念。* V3 r( s. D8 ^6 F: d6 o% [
- Y4 m! ~. {/ _+ ?* M8 P4 e( _# {
以上1是非常重要的重點,2&3只要有大致上的概念就可以,因為trace code的時候,有時需要跳到Makeflie&Link script去看最後object code編排的位址。
" Q. D. B- n; s) g# P$ \: H4 ~6 l& g+ j/ O( w' U
由於我們trace到了page table這個關鍵字,在開始之前,稍微簡短的解釋,為了幫助了解,儘量用易懂的概念講,有些用詞可能會和一些真實狀況不同,但是懂了之後,應該會有能力分辨,至於正式而學術上解說,很多書上應該都有,或是google一下就很多啦∼
6#
 樓主| 發表於 2008-10-14 12:14:47 | 只看該作者
page table本身是很抽象的東西,尤其是對所謂的 user-mode application programmer 來說,大部分的狀況它是不需要被考慮到的部份,但是他的產生卻對os和driver帶來了很多影響。我們從一個小小的疑問出發。
, `2 }  Y+ T9 h- [( X5 z. h; C  C) R8 V. b: O
『產生page table到底是要給誰用的?』+ k5 l* r: a  ?5 m4 z: C

% A/ c. A8 d2 g" a- q7 o7 j; A其實真正作用在它上面的H/W是MMU,一旦CPU啟用了MMU,當cpu嘗試去記憶體讀取一個operand的時候,cpu內部打出去的位址訊號都會先送到mmu,mmu會拿著這個位址去對照page table,看看這個位址是不是被轉換到另外一個位置(還會確認讀寫權力),最後才會到真正的位址去讀寫。" \; S1 ^& \% Z" }9 g% n4 g9 o( t
! P3 O  a$ x# r) T
這樣來看CPU其實一開始打出去的位址訊號其實不是最後可以拿到資料的位址,我們稱為virtual address(va),mmu打出來的位址才能真正拿到資料,所以我們把mmu打出去的位址稱為physical address(pa)。那用來查詢這個位址對照關係的表格,就是page table。
& T) Q& M1 s' B& N
* i- O6 x  y0 r+ T) E1 L5 C" P. S到這邊我們有一個簡單的概念。來想像一個小問題,一個普通的周邊(例如你的顯示卡),因為他並不具有MMU功能,hw只看得懂pa,但是CPU卻是作用在va,假如我想去對hw的控制暫存器做讀寫,到底要用va還是pa?
7#
 樓主| 發表於 2008-10-14 12:15:51 | 只看該作者
這時,寫driver的人就必須要小心,設定給硬體看的位址必須要先轉成pa(像是設定DMA),給CPU執行的程式碼要使用va,這對一開始嘗試寫driver但是又不瞭解va pa之間的差別的人,常常產生很多疑問。0 V! J5 Y; M- D3 D; n
$ n9 `0 c/ k! \& c* e
現在我們回頭想想OS,既然我們跑到create page table,可以預期的是他想要將MMU打開,因此希望預先建立起一個page table,讓MMU知道目前os想規劃的位址對應和讀取權力是如何被安排的。所以os必須考慮到現在的系統究竟是長怎樣?應該要如何被安排?是不是有那些要被保護?好吧∼因為我們完全對os不了解,也不知道該安排什麼,只能祈禱在trace code完後,可以找到這些問題的答案,或者發現一些沒想到的問題。
% m/ g! s2 w3 ~& J
5 y- B2 Y; n! d; j! j. m2 f+ l& o知道了page table的大致上的功能,下篇就可以專心的研究這個table的長相,和它想規劃出的系統模樣。
+ H2 [* \9 F+ U4 t1 U0 C6 b0 ]. o  F  v; e2 F
p.s. 字數限制好像變短了。   (看來很難寫)
8#
 樓主| 發表於 2008-10-14 13:56:17 | 只看該作者
由於字數限制挑整,用詞儘量精簡,以便可以貼必要source。另外,以後寫到一段落,考慮收集成一篇完整的文章,這樣應就不會因為用回覆的方式,造成閱讀斷斷續續的問題。希望有人對文章呈現方式有想法的話,可以跟我講。
9#
 樓主| 發表於 2008-10-14 13:57:39 | 只看該作者
現在,讓我們跳入create_page_tables吧∼, H1 V1 J/ C6 |7 p
line 216,會跳到pgtbl的macro去執行,其實只是載入一個位址。6 a3 h$ x5 ]3 t1 X% y( r
" E$ s5 O: Z: F, y0 y, n
只是這個位址因為你硬體規劃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 */
    * p& j% r, d. ^* v. Z3 M
  2.      95 textofs-y       := 0x000080005 Y& w* s  E: `/ y  }
  3.     152 TEXT_OFFSET := $(textofs-y)
複製代碼
  1. /* include/asm-arm/arch-omap/memory.h */
    8 S- C. \! r# h( K4 F  `1 z3 q2 a2 M0 l
  2.      40 #define PHYS_OFFSET             UL(0x10000000)
    + H, s. O+ o4 M" }7 i* {

  3. : j( e  a4 L5 h3 R) U! w5 K: [
  4.      /* arch/arm/kernel/head.S */! ?& ]! D2 i7 ]: [9 c! p2 ]
  5.      29 #define KERNEL_RAM_VADDR        (PAGE_OFFSET + TEXT_OFFSET)) \' I8 V6 [3 a: l
  6.      30 #define KERNEL_RAM_PADDR        (PHYS_OFFSET + TEXT_OFFSET)' s5 a% T8 r5 q" u

  7.   e4 h0 @1 |  y  u6 n
  8.      47         .macro  pgtbl, rd
    4 u( h  |7 O9 m( t; X& H: S
  9.      48         ldr     \rd, =(KERNEL_RAM_PADDR - 0x4000)4 g; D) O5 a# Y( v
  10.      49         .endm% Q7 M( b6 Z3 D; ]( T
  11. , z7 L0 ?* A0 ?' G: B
  12.     216         pgtbl   r4                              @ page table address
複製代碼
10#
 樓主| 發表於 2008-10-14 14:16:53 | 只看該作者
得到pg table的開始位置之後,當然就是初始化囉。
- T  M6 E/ K5 g+ P% rline 221, 將pg table的base addr放到r0.2 i7 ~* a' ?2 P# L
line 223, 將pg table的end addr放到r6.
% |% b/ ~6 l6 J) L/ L' jline 224~228, 反覆地將0x0寫到pg table的區段裡頭,每個loop寫16bytes. (4x4),直到碰到end,結果就是把它全部clear成0x0.
  1.     221         mov     r0, r4
    - [6 }9 ~# r8 {& u1 `; f
  2.     222         mov     r3, #0: r. ~2 s4 G% _. v- {. A; Z
  3.     223         add     r6, r0, #0x4000
    ) E, P& l+ ~3 E3 m) F4 z; p
  4.     224 1:      str     r3, [r0], #4
    & m* s! h* X1 G2 b' w8 o- q2 v# J
  5.     225         str     r3, [r0], #44 W: `" f. r# t+ [, M
  6.     226         str     r3, [r0], #4
    : p* W9 H) Z- @: ~2 [
  7.     227         str     r3, [r0], #4
    8 E& w+ k+ {- H* b: j7 f3 w
  8.     228         teq     r0, r66 L! |8 A" v: l* n( P8 V
  9.     229         bne     1b
複製代碼
line 231, 將位址等於 r10+PROCINFO_MM_MMUFLAGS 裡頭的值放到r7。r10是proc_info的位址。proc的info data structure被定義在『./include/asm-arm/procinfo.h』,offset取得的方式用compiler的功能,以便以後新增structure的欄位的時候不需要更動程式碼。這邊的動作合起來就是讀預設要設給mmu flags的值。
  1.    231         ldr     r7, [r10, #PROCINFO_MM_MMUFLAGS] @ mm_mmuflags
複製代碼
11#
 樓主| 發表於 2008-10-14 15:11:48 | 只看該作者
問題怎麼填值??% _  G" t; ]- ~: I6 \
拿出ARM的手冊,翻到MMU章節。一看發現page有很多種,還得分first level和second level。   g6 ]; G$ R0 G& \

0 g9 Y) B" l( V* G念書時的印象,是從1st level在去查2nd level,先看怎麼設定1st level (不知以前老師有沒有亂教)
2 Q' u" X! P* T9 t- J1. [31:20]存著section base addr
( f- E+ ^# `! j# \  a) d* u6 W2. [19:2]存著mmu flags
% _, t+ Y0 t. S. I& [% j# M; Y- c: |3. [1:0]用來辨別這是存放哪種page, 有四種:) B' J; I; |7 Z7 I
   a. fault (00) b. coarse page (01) c. section (1st level) (10) d. fine page (11)
) d9 F0 x4 e* J/ Z3 i8 y3 ^: u4. pg tabel資料要存放到 [31:14] = translation base, [13:2] = table index , [1:0] = 0b00 的位址5 `" k$ ]/ q0 z' E+ ~) e; p8 h
  j9 ^& I! m, U' B7 A
來看code是怎麼設定。. Y6 _  K3 g0 g
4 |2 b* [4 u! Q3 n. m$ b6 R
line 239, 將pc的值往右shift 20次放到r6.這是取得前20高位元的資料,有點像是 1.。
3 R' r* }) g; A  u8 D0 t- f! R; Xline 240, 將r6往左shift 20次之後,和r7做or的動作,剛好也是 2.提到的mmu flags和1.處理好的資料做整理。" F3 k; o6 I0 G; X2 _
所以前面兩個做完,就完成了bit[31:2]。
+ A- v6 I& ~5 T6 C; ]line 241, 將r3的資料寫到r4+(r6<<0x2)的地方去,剛好是4.提到的位址。(lucky)
  1.     239         mov     r6, pc, lsr #20
    9 ^# ~" k% h5 V
  2.     240         orr     r3, r7, r6, lsl #20
    6 C5 q9 D, G7 P( m
  3.     241         str     r3, [r4, r6, lsl #2]
複製代碼
p.s. 用pc值剛好可以算出當前的page。
12#
 樓主| 發表於 2008-10-22 19:47:03 | 只看該作者
最近又被釘上了,開始忙碌,大概沒太多時間貼文∼
, b5 a0 S3 Y7 p0 C5 o, p& @1 i& L! B
上篇已經將pc所屬的page table entry(pte)設定好,接著繼續看* p( a' A" [" z, e1 {
line 247, 248, 將KERNEL_START的位址往右shift 18算出pte的offset,(等於line239&241,239&241是先shift right 20然後shift left 2),並將剛剛r3的值設給pte。『!』的意思是會把address存到r0。
' Z# n5 Z, ?+ a2 n2 W. P* ?
% B7 y; o$ e* j+ Zline 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
    9 w. \4 M# y3 t7 d
  2.     248         str     r3, [r0, #(KERNEL_START & 0x00f00000) >> 18]! ( G/ t! g# V" L
  3.     249         ldr     r6, =(KERNEL_END - 1)) c/ l* [' z! T" I+ d' e( q" y7 H
  4.     250         add     r0, r0, #4# c$ q3 [# m3 |6 B9 a
  5.     251         add     r6, r4, r6, lsr #18
    6 F2 |7 ?2 h3 D# X) r' B# t9 {7 k
  6.     252 1:      cmp     r0, r66 c, U& U/ t8 N! Y" k* u
  7.     253         add     r3, r3, #1 << 205 x5 T  ?5 H: ]0 y
  8.     254         strls   r3, [r0], #4: }- G9 o  C9 {& s* U' |( V
  9.     255         bls     1b
複製代碼
13#
 樓主| 發表於 2008-10-22 20:24:58 | 只看該作者
line279,PAGE_OFFSET是規範RAM mapping完後的virtual address,我們將這個位址所屬的pte算出來放到r0。$ Y8 X+ Q' w( t8 Q
line 280~283,將要 map 的physical address的方式算出來放到r6。
4 Q6 ]6 j. x4 j( p7 y- i" }line 284,最後將結果存到r0所指到的pte。
3 ^4 A) q# ]0 a6 m' o
9 @  O& Q" u# C0 h( {# s+ X以上三個動作,就是做好 ram 起頭的位址一開始map,還沒做完整塊map。由於我們目前的設定方式每塊是1MB,所以這邊是將RAM開始的1MB做map。! g' ^* G# n5 a# {( O+ D
: G. v9 X) l/ @+ c6 V5 |
line 327,返回,結束一開始的create page table的動作,我們可以看出其實並沒有做完整的初始化page table,所以之後應該會有其他page table的細節。
  1.     279         add     r0, r4, #PAGE_OFFSET >> 18
    * X7 p5 a. N: N9 g! e9 A$ f# K
  2.     280         orr     r6, r7, #(PHYS_OFFSET & 0xff000000)- f, K# j4 u3 G6 {: p- h
  3.     281         .if     (PHYS_OFFSET & 0x00f00000)6 v$ O* Q+ i7 E' {
  4.     282         orr     r6, r6, #(PHYS_OFFSET & 0x00f00000)" _' ^( w) C% i" n- N
  5.     283         .endif
    - y- d6 }7 T& G( E' L0 w
  6.     284         str     r6, [r0]) O* a9 n) S7 ^7 t. p8 i
  7.     327         mov     pc, lr
複製代碼
附帶一提,我們這邊省略了一些用ifdef包起來的程式碼,像是一開始會印一些output message的程式碼(line286~326)。
14#
 樓主| 發表於 2008-10-22 20:37:08 | 只看該作者
自create page table返回後,我們偷偷看一下接下來的程式碼,
; ~2 _7 Q) h! ^line 99, 將switch_data擺到r13/ A1 k* T) N/ v3 Q
line 101, 將enable_mmu擺到lr( b9 T& _0 M! s3 @, U5 L0 ^
line 102, 將pc改跳到r10+PROCINFO_INITFUNC的地方去' B  O$ @* n" d
) V* N0 Q1 W7 ?0 A# t
其實這邊有點玄機,switch_data和enable_mmu都是function,結果位址都只是被存起來,並沒有直接跳過去執行。其實,雖然程式碼是順序switch_data->enable_mmu->proc init function,但其實執行的順序會是 procinfo 的init function-> enable_mmu -> switch_data 。至於,為什麼要這樣寫的原因?就不清楚了,也還沒仔細推敲過。
5 d: Z8 S8 |7 |
" L% j  L: c1 u- \switch_data最後就會跳到大家都很熟悉的start_kernel(). 詳細要賣個關子,最近會忙一下,得要過一陣子才能貼文∼  
  1.      99         ldr     r13, __switch_data              @ address to jump to after
    7 C5 l8 I; g, ~- @. R
  2.     100                                                 @ mmu has been enabled2 |4 I/ H: m( |  y
  3.     101         adr     lr, __enable_mmu                @ return (PIC) address
    7 P- j$ J; g" O0 b
  4.     102         add     pc, r10, #PROCINFO_INITFUNC
複製代碼
15#
 樓主| 發表於 2009-7-4 01:09:36 | 只看該作者
老店重新開張~' Q) S# |! f# H

0 E! D, n) ~+ d2 D9 v1 ~花了一些時間把舊的貼文整理到一個blog  T* C% {" M3 S% o
有把一些敘述修改過6 a1 E0 ]6 i& c' l5 a! J( j6 x
希望會比較容易集中閱讀
! q- P/ _, Q6 `6 i% a" T% f; \目前因為某些敘述不容易
3 N" G- v9 x$ B7 I5 U還是比較偏向筆記式而且用字不夠精確) Y8 R/ W$ P; b; f) }: P' W
希望之後能夠慢慢有系統地整理
) `* O+ z, ]& V& }3 V2 }7 C, X大家有興趣的話
% g' v9 n9 q, `  D5 O可以來看看和討論
, s4 J4 L7 B7 s/ qhttp://gogojesseco.blogspot.com/
9 r, R' O, _8 z$ Q# y# C
% f4 [9 q/ G) y, h7 h0 [3 ?" R! [以後可能會採取  先在chip123貼新文章- f) \1 L, c5 _- d; u0 d
慢慢整理到blog上的方式. p# b- X0 j' U! ^0 W0 r; d, F+ v
因為chip123比較方便討論 =)5 o- ~! H! b9 l+ R3 w( O
blog編輯修改起來比較方便
* I+ ]+ Y) B9 @4 ?閱讀也比較集中   大家可以在這邊看到討論
% _* F; B# B2 L3 q2 ]# l' Q; }然後在blog看到完整的文章 (類似BBS精華區的感覺)

評分

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

查看全部評分

16#
 樓主| 發表於 2009-7-15 17:07:03 | 只看該作者
隔了很長一段時間沒update
9 @/ N7 l$ b' M2 b$ Q之前程式碼走到 ./arch/arm/kernel/head.S 的 line 99 附近
  1.      99         ldr     r13, __switch_data              @ address to jump to after
    5 l* N) Q) a( q) b- \
  2.     100                                                 @ mmu has been enabled$ Q* p: Q/ X! A8 G" Y1 Z; v
  3.     101         adr     lr, __enable_mmu                @ return (PIC) address
    $ j3 C4 m7 T/ i1 O( |' x
  4.     102         add     pc, r10, #PROCINFO_INITFUNC
複製代碼
line 99, 將__switch_data放到r13。(留作之後用)
' }5 c, {1 `3 ?: k5 @line 101, 將__enable_mmu的addr放到lr。(留作之後用)* Z- C" f' u) [8 \
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* N% Z* C" r: B9 }1 Z6 I7 g: t5 C
  2. 374 __arm926_setup:
    0 }2 c& @( j7 s# l+ e5 s5 ]; f) W
  3. 375         mov     r0, #0
    2 p( r  u- a" t) D! G
  4. 376         mcr     p15, 0, r0, c7, c7              @ invalidate I,D caches on v4
    1 }' S6 S& v, r
  5. 377         mcr     p15, 0, r0, c7, c10, 4          @ drain write buffer on v4- x" r/ z0 k  c) E9 g
  6. 378 #ifdef CONFIG_MMU. u7 _9 D  x  A5 Z0 ^( [& H
  7. 379         mcr     p15, 0, r0, c8, c7              @ invalidate I,D TLBs on v4
    ) \7 K5 u1 k" A7 e5 e# _
  8. 380 #endif
    1 j) r% n; v0 F6 u4 y; O; G
  9. 3 z' C1 D4 E, e+ @. g" c0 V
  10. 388         adr     r5, arm926_crval
    " V# L" I' O' `9 r/ F: J$ ]# g
  11. 389         ldmia   r5, {r5, r6}% u0 f0 {" p, a' k/ N) z8 W
  12. 390         mrc     p15, 0, r0, c1, c0              @ get control register v4
    5 r+ t" n: F& Y$ h; b! j* i# j
  13. 391         bic     r0, r0, r5
    & X1 e. R( A& P8 X2 ?
  14. 392         orr     r0, r0, r65 n: w$ C6 N8 v7 T1 A0 Z6 ]

  15. " q6 z$ C  |  v: F5 V8 Y
  16. 396         mov     pc, lr2 U4 j- w* W3 U" v. D
  17. 397         .size   __arm926_setup, . - __arm926_setup
複製代碼
這邊的程式碼就跟CPU有很大的相依性,
( M1 e/ ^6 t- B" cline 375~380, 主要就是invalidate CPU的I&D cache和清空write buffer。$ c- I0 P# P7 ?4 r$ q4 G
line 388~392, 把cp15的設定讀出來,並且將一些預設狀態做好運算放到r0。(預設值從arm926_crval這邊可以取得。)
: X: G3 r9 I0 f6 s$ Lline 396, 直接把pc跳到lr,因為我們之前已經將lr = enable_mmu,所以直接跳過去。
17#
 樓主| 發表於 2009-7-15 17:29:45 | 只看該作者
  1. 155 __enable_mmu:
    ; @  X: f5 r9 x8 k$ ^
  2. 170         mov     r5, #(domain_val(DOMAIN_USER, DOMAIN_MANAGER) | \
    % e$ c) @& A4 \* e) x
  3. 171                       domain_val(DOMAIN_KERNEL, DOMAIN_MANAGER) | \
    7 Y5 O2 _$ Y" _
  4. 172                       domain_val(DOMAIN_TABLE, DOMAIN_MANAGER) | \
    - Y/ U5 E: _5 G/ w0 z) k% J
  5. 173                       domain_val(DOMAIN_IO, DOMAIN_CLIENT))+ }5 [' C1 o9 P8 W1 i9 Z! p
  6. 174         mcr     p15, 0, r5, c3, c0, 0           @ load domain access register
    3 \9 |9 z9 M1 H9 c
  7. 175         mcr     p15, 0, r4, c2, c0, 0           @ load page table pointer2 f9 ^8 X3 |( _' ?- b
  8. 176         b       __turn_mmu_on* H* S7 L# [( s1 }
  9. 177 ENDPROC(__enable_mmu)
複製代碼
line 170~174,設置好domain access。(domain access可以先當成設置access的權限,或許以後可以寫詳細的文章說明)5 }( o6 q, M' E* Q. [4 C
line 175~176,將create好的page table開頭丟給mmu。跳到turn_mmu_on
  1. 191 __turn_mmu_on:
    7 C# Y% i. E. K9 `7 L4 R
  2. 192         mov     r0, r0
    2 D2 o+ N! b5 j/ T; `
  3. 193         mcr     p15, 0, r0, c1, c0, 0           @ write control reg
    + W8 j4 N8 e# }8 s' A, y
  4. 194         mrc     p15, 0, r3, c0, c0, 0           @ read id reg
    ' b( r- w# M5 {: f! {$ }
  5. 195         mov     r3, r3
    / Z8 h. E! I* p5 e) N5 k+ k$ R* e% U
  6. 196         mov     r3, r3
    % c8 B" w9 p4 q0 I
  7. 197         mov     pc, r13
    , d  {, D) r  n" h  }+ D" u7 T
  8. 198 ENDPROC(__turn_mmu_on)
複製代碼
顧名思義就是把mmu打開,將我們準備好的r0設定交給mmu,並讀取id到r3,接著pc跳到r13,r13剛剛在head.S已經先擺好__switch_data。所以會跳到head-common.S。
  1. 18 __switch_data:8 W7 n" `  _' ~  V) O5 K
  2. 19         .long   __mmap_switched1 Q( a2 E% P9 p( a. q1 O5 v% s
  3. 20         .long   __data_loc                      @ r4
    $ S9 c2 `. z& G
  4. 21         .long   _data                           @ r5
    % q5 C$ ^" n4 E5 r% z, T+ G
  5. 22         .long   __bss_start                     @ r6
    : T" M' ~: c4 z0 ~! O  u
  6. 23         .long   _end                            @ r7
    1 G2 f% t; s9 G( X, p5 l$ n
  7. 24         .long   processor_id                    @ r4
    ! i4 ]8 Q0 ~8 N) K
  8. 25         .long   __machine_arch_type             @ r59 S  U  E& a6 k
  9. 26         .long   __atags_pointer                 @ r65 t' T* N& t# w; X6 ]4 J
  10. 27         .long   cr_alignment                    @ r7
    % j# |3 Q4 Y8 D5 B- W. c& S4 |! v
  11. 28         .long   init_thread_union + THREAD_START_SP @ sp# ^: u* L) n0 w, H
  12. 29- ~, K+ J+ Y7 \
複製代碼
switch_data的第一行就是 __mmap_switched,所以我們直接看line 39。
18#
 樓主| 發表於 2009-7-15 17:30:00 | 只看該作者
  1. 39 __mmap_switched:- T- D( e% V" U
  2. 40         adr     r3, __switch_data + 4
    5 W% V& e8 m, O' |5 z
  3. 41
    8 T, `& W6 @9 w( m! s: G* ?- s
  4. 42         ldmia   r3!, {r4, r5, r6, r7}
    6 d2 ^0 p9 J5 w, n. m( {( D; w
  5. 43         cmp     r4, r5                          @ Copy data segment if needed" w( T# [9 V, u+ {
  6. 44 1:      cmpne   r5, r6& d+ L" o' Q- u0 v2 v7 U1 N
  7. 45         ldrne   fp, [r4], #4- D3 u; ]1 s2 b( e7 z# U. _
  8. 46         strne   fp, [r5], #4
    , q/ P$ G0 R5 X! m1 g3 W. H
  9. 47         bne     1b
    # k, [6 s  [1 H$ [5 x" t. B
  10. 48
    - _3 ^5 p+ J7 K, ?+ x+ v! i
  11. 49         mov     fp, #0                          @ Clear BSS (and zero fp)2 {, u5 n  O) v
  12. 50 1:      cmp     r6, r7- t$ f- ~9 V9 Q( ]! L: ~' @9 }
  13. 51         strcc   fp, [r6],#4
    * ?& t/ a2 R5 N9 i, n; R* e
  14. 52         bcc     1b
    $ T' U$ S5 W7 M+ \* T# Q
  15. 53+ D% }4 b+ Y- g( {, h7 t
  16. 54         ldmia   r3, {r4, r5, r6, r7, sp}8 n- q) w  Q* M- z! h) i
  17. 55         str     r9, [r4]                        @ Save processor ID) S3 T) t$ h9 ~9 z, X, c% @) o0 W. y
  18. 56         str     r1, [r5]                        @ Save machine type
    ; z- s7 _' B: ]1 a0 \) @
  19. 57         str     r2, [r6]                        @ Save atags pointer
    ) z5 U, ]& d& }1 G( r
  20. 58         bic     r4, r0, #CR_A                   @ Clear 'A' bit
    $ c: \9 `7 U- v2 [
  21. 59         stmia   r7, {r0, r4}                    @ Save control register values
    - `7 r' b5 y* ]$ f2 V" e8 i1 j
  22. 60         b       start_kernel
      K" \: a3 S( {3 ^: `4 p
  23. 61 ENDPROC(__mmap_switched)
複製代碼
switch_data的第一行就是 __mmap_switched,所以我們直接看line 39。
3 @) O8 X; z5 h( Q0 Qline 39,將__data_loc的addr放到r3
% k1 J% C, {  |: k! V/ w1 F8 ^- C: ]! U: _line 42,從r3的位址,連續讀取四筆資料到r4, r5, r6, r7
3 x1 q; w: _0 m" `) p5 dline 43~47,看看data segment是不是需要搬動。$ ]8 a2 I  f, b4 @1 `7 q
line 49~52, clear BSS。6 V/ Z3 a& ^! `! L, i, x% m
; F/ N3 v( V2 m$ k# |
由於linux kernel在進入start_kernel前有一些前提必須要滿足:) M  }! f1 i* _2 f
r0  = cp#15 control register
. w4 E3 O8 i% r: E0 A5 @0 I) [r1  = machine ID
, |6 N4 k6 T& a, H7 q7 v  G* x- ir2  = atags pointer5 f/ X9 W) P; O9 b5 T3 b
r9  = processor ID
! q1 d# A7 c1 d5 C" K' f3 J8 f- m2 L$ K, _
所以line 54~59就是在做這些準備。
7 F/ u4 H) u9 Q3 s最後呢? 我們就跳到start_kernel了。(而且還是用b start_kernel,表示我們不會再回來了)  r! T/ U" a! K0 P7 y
6 I) b; \$ T: K$ a% b
看一下start_kernel()在./init/main.c,終於跳出architecture specific的目錄,表示
; X# J1 X2 a, G, H8 f我們真正的開始linux kernel的初始化。
9 K  u( A0 A3 D) g: W2 E像是 shedule init, console init, memory init, irq init等等都在start_kernel裡頭。
) e! o5 ]0 ~9 t+ X/ d0 @1 \% e% X到這邊之後,應該就可以深入linux kernel中,各項比較跟hardware不那麼相關的軟體部分。

評分

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

查看全部評分

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

本版積分規則

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

GMT+8, 2024-6-6 06:28 PM , Processed in 0.165521 second(s), 19 queries .

Powered by Discuz! X3.2

© 2001-2013 Comsenz Inc.

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