Chip123 科技應用創新平台

 找回密碼
 申請會員

QQ登錄

只需一步,快速開始

Login

用FB帳號登入

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

trace linux kernel source - ARM - 03

[複製鏈接]
跳轉到指定樓層
1#
發表於 2008-10-7 14:00:18 | 只看該作者 回帖獎勵 |倒序瀏覽 |閱讀模式
到目前為止,我們已經進展到kernel幫自己relocate完,並解將自己解壓縮到一個地方要準備開始執行,那疑問來了?到底是跳到哪裡去了,因為./compressed/head.S最後一行居然是
" P% _" T0 m- E" o# f, a) P4 t『mov pc, r4』1 P7 S3 n2 {/ c- z0 s
r4只代表了解壓縮完後kernel的位址,那究竟整包kernel編譯的時候,哪個function哪個東西被放在最前面咧?!
$ \: i# \( L, Z( j' J* ~& a& z6 f; q% i- R% ^- J- P
所以我們又必須開始找於kernel link的時候是怎麼被安排的,有了前面的基礎,我們可以從Makefile知道程式碼如何被編譯。至於link上的細節,例如有那些section和section先後順序等等,可以從 lds 檔來規範。
, l8 l8 R+ q" p1 h  o
( Z. H/ ~' Y" J- T有興趣的人可以看一下 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)
% D  E+ p1 i( Q我們可以發現第一個section是『.text.head』,裡頭的_stext從目前的位置開始放。+ L% B  b# g! [
於是我們曉得只要找到屬於.text.head這個section,並且是_stext這個symbol的程式碼,就是解壓縮完後的第一行程式碼。
  1.      26     .text.head : {
    / s( \6 a- _) n5 ^+ ~0 c  z
  2.      27         _stext = .;" a# w4 Y9 i2 v6 f- l4 a
  3.      28         _sinittext = .;4 ]9 D% m0 }* o; Q: \' H1 G! V: s, J
  4.      29         *(.text.head)8 d7 s8 \  G; h+ J
  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/ A. A! e( e% \1 E0 H1 y" C" w% a
  2.      78     .type   stext, %function- Z3 `8 b# J/ k" I+ y
  3.      79 ENTRY(stext)
    7 {) E# X7 j9 ~( \% Y  L& K' h' I
  4.      80     msr cpsr_c, #PSR_F_BIT | PSR_I_BIT | SVC_MODE @ ensure svc mode; L% b. Y  P8 \' I- U" O
  5.      81                         @ and irqs disabled( E! m* V, L" v) V+ M: c" X4 Q
  6.      82     mrc p15, 0, r9, c0, c0      @ get processor id
    ' H3 \: Z1 G4 D$ `9 `
  7.      83     bl  __lookup_processor_type     @ r5=procinfo r9=cpuid
    8 W5 h- J( I- u% ]# C
  8.      84     movs    r10, r5             @ invalid processor (r5=0)?
    , }  m; Y( E' z! M2 ~1 ^
  9.      85     beq __error_p           @ yes, error 'p'. T$ w, S, x, @5 ^! \; l8 Q) P
  10.      86     bl  __lookup_machine_type       @ r5=machinfo
    " @: c! L: y, s- W
  11.      87     movs    r8, r5              @ invalid machine (r5=0)?
    " x! Y' L8 H: W8 c( E/ [! `2 n
  12.      88     beq __error_a           @ yes, error 'a'" h& d) ]+ R) f6 ~; }% T
  13.      89     bl  __vet_atags5 n  |* T6 d$ k% M9 ~
  14.      90     bl  __create_page_tables
複製代碼
既然找到了檔案,我們又可以開始繼續。
! Y1 k/ U- F& o
+ |# c) c6 ~* P. A# }看了一下,程式碼多了很多bl的跳躍動作,看來會在各個function跳來跳去。
分享到:  QQ好友和群QQ好友和群 QQ空間QQ空間 騰訊微博騰訊微博 騰訊朋友騰訊朋友
收藏收藏 分享分享 頂 踩 分享分享
2#
 樓主| 發表於 2008-10-9 15:32:16 | 只看該作者
既然跳到真正的kernel開始跑,表示進入重頭戲,在進入kernel之前arm的平台有一些預設的狀況,也就是說arm kernel image會預設目前的cpu和系統的狀況是在某個狀態,這樣對一個剛要跑起來的OS比較決定目前要怎麼boot起來。( X3 G# G5 Q7 Z

: l) ^- i. E  c  _7 i" p可以看一下comment,有清楚的描述。這也幫助我們了解為什麼decompresser的程式跑完之後,還要把cache關掉。
  1.      59 /*9 q; a( j" S' R  c5 n
  2.      60  * Kernel startup entry point.
    " ?% I- w5 s( D( |% C7 z
  3.      61  * ---------------------------
    7 s; }# j* l, ?& M( K, q9 O
  4.      62  *
    * F$ {9 P$ L# O1 {, h2 r
  5.      63  * This is normally called from the decompressor code.  The requirements
    " q6 p5 ^% `3 E: o. R6 X" c3 J
  6.      64  * are: MMU = off, D-cache = off, I-cache = dont care, r0 = 0,7 m. A  F* y) f4 J) ]1 z
  7.      65  * r1 = machine nr, r2 = atags pointer.
複製代碼
基於以上的假設,當program counter指到kernel的程式的時候,就會從line 80開始跑。
, p+ _$ u* U5 }' f7 mline 80, msr指令會把, operand的值搬到cpsr_c裡面。這是用來確保arm cpu目前是跑在svc mode, irq&fiq都disable。(設成0x1就會disable,definition在./include/asm-arm/ptrace.h)1 ]( n3 z  H/ w
line 82, 讀取CPU ID到r9
3 B$ m5 N9 v3 M/ e+ jline 83, 跳到 __lookup_processor_type 執行(bl會記住返回位址)。
  1.      77     .section ".text.head", "ax"" U2 W6 @5 ]/ q- Z& H: P
  2.      78     .type   stext, %function3 r6 D, c6 M5 e' E" R: l$ m
  3.      79 ENTRY(stext)0 J0 \1 U0 s3 G! Y) j
  4.      80     msr cpsr_c, #PSR_F_BIT | PSR_I_BIT | SVC_MODE @ ensure svc mode
    " I* N5 D/ I; _% n9 I6 n. t
  5.      81                         @ and irqs disabled1 S! o2 {, }: O  H5 o4 j
  6.      82     mrc p15, 0, r9, c0, c0      @ get processor id: L/ ]( ?2 t: g" {
  7.      83     bl  __lookup_processor_type     @ r5=procinfo r9=cpuid
複製代碼
接著會跳到head-common.S這個檔,
; ]1 h) T1 c# m6 eline 158, (3f表示forware往前找叫做『3』label)將label 3的address放到r3。. i  @4 Z6 K+ t' M, P! U9 F+ Y
line 159, 將r3指到的位址的資料,依序放到r7, r6, r5.(ldmda的da是要每次都位址減一次)! K" B8 p4 j3 v2 N# F
line l60, 161, 利用真實得到位址r3減去取得資料的位址得到一個offset值,這樣可計算出r5, r6真正應該要指到的地方。) G. Z! L& O- ~  N4 [  H
line 163~169,這邊的程式應該不陌生,就是在各個CPU info裡面找尋對應的structure。找到的話就跳到line 171,返回head.S
2 t7 T* y# j7 \line 170, 找不到的話,r5的processor id就放0x0.表示unknown id。
! ~# A% h" K! d( K/ ]
" d) G! _* m8 ]# K% 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
    - J" [2 f) G  R* E* V
  2.     157 __lookup_processor_type:4 t# w2 U' O* i7 \4 R) |5 ?" d, @
  3.     158     adr r3, 3f$ q6 z2 z2 G; G8 M
  4.     159     ldmda   r3, {r5 - r7}  F' N, ?4 o4 k) q4 N5 ?  m0 ?
  5.     160     sub r3, r3, r7          @ get offset between virt&phys
    " e6 c- f2 q1 h. O7 T
  6.     161     add r5, r5, r3          @ convert virt addresses to
    ( t5 Z. A9 k- }
  7.     162     add r6, r6, r3          @ physical address space, j& I+ n4 _% t, Z& k& |
  8.     163 1:  ldmia   r5, {r3, r4}            @ value, mask
    3 r. v- I8 f1 `: N% p8 _
  9.     164     and r4, r4, r9          @ mask wanted bits1 d, P5 T2 F; C- z
  10.     165     teq r3, r47 d6 ]# v0 U4 Y0 _
  11.     166     beq 2f% V2 m1 U3 v: E& A8 _
  12.     167     add r5, r5, #PROC_INFO_SZ       @ sizeof(proc_info_list)+ q. `. n: ]" `% X
  13.     168     cmp r5, r6! _4 a/ F1 m6 }, @. n. U4 e5 Q
  14.     169     blo 1b( p6 }4 Z+ s; w7 f! K0 d
  15.     170     mov r5, #0              @ unknown processor
    * `" d; L4 K* A: Z- V
  16.     171 2:  mov pc, lr: m% y# L2 G2 M; ]* \$ @# h" ~2 ^

  17. 2 a, V. H) A$ l# u: Y% T* g" `
  18.     187     .long   __proc_info_begin
    5 V6 T) i  U4 ~* ]; ]" s
  19.     188     .long   __proc_info_end
    # A) Q+ B& ]2 }$ A
  20.     189 3:  .long   .& _2 D+ l% y6 E7 }* B- _3 g+ Z( E
  21.     190     .long   __arch_info_begin* i" R. |# z$ X) e) @; H3 ]: f
  22.     191     .long   __arch_info_end
複製代碼
跳了很多檔案,建議指令和object code如何link的概念要有,就會很清楚了。
3#
 樓主| 發表於 2008-10-13 17:20:35 | 只看該作者
我們從 head-common.S返回之後,接著繼續看。
2 D/ p5 j* c+ f0 P! A9 K; K! `6 x9 Z8 K/ h+ w% G+ w
line 84, movs的意思是說,做mov的動作,並且將指令的S bit設起來,最後的結果也會update CPSR。這個指令執行的過程當中,會根據你要搬動的值去把N and Z flag設好。這個是有助於下個指令做check的動作。; i4 f6 Y% N$ a* q
line 85, 就是r5 = 0的話,就跳到__error_p去執行。+ L. r$ K' R# t& M- [' I/ M5 n
line 86, 跳到__lookup_machine_type。原理跟剛剛找proc的資料一樣。
  1.      83         bl      __lookup_processor_type         @ r5=procinfo r9=cpuid! O4 k4 @9 x% Y9 ]& v8 V0 }
  2.      84         movs    r10, r5                         @ invalid processor (r5=0)?- B1 \4 `: e  t' i$ Z
  3.      85         beq     __error_p                       @ yes, error 'p'! D# N6 v- [+ l/ A: C
  4.      86         bl      __lookup_machine_type           @ r5=machinfo
複製代碼
看得出來跟proc很像,有個兩個小地方是* R0 P: h  n$ p$ U

1 a) M6 H0 q" r/ B& C. r1. line 207,用ldmia不是用ldmda。原因就在於存放arch 和 proc info 的位址剛好相反。一個在lable3的上面。一個在的下方。設計上應該是可以做修改的。- {; u* D7 F; F( o

% y3 ^+ @( o2 m, B2 j. U2. arch定義的方式是透過macro,可以先看 ./include/asm-arm/mach/arch.h。裡頭有個 MACHINE_START ,這邊會設好macro,到時候直接使用就可以把arch的info宣告好。 例如 ./arch/arm/mach-omap1/board-generic.c
  1. /* macro */
    - r  Q' ~/ T5 `: U
  2.      50 #define MACHINE_START(_type,_name)                      \
    ) A5 r9 m; _: L* m
  3.      51 static const struct machine_desc __mach_desc_##_type    \
    + A( o# d/ C& m. R$ |& h7 H
  4.      52  __used                                                 \; a# J* G# N+ l( W8 ^8 z
  5.      53  __attribute__((__section__(".arch.info.init"))) = {    \
    4 S& h- D4 d" D) K4 V+ P
  6.      54         .nr             = MACH_TYPE_##_type,            \
    8 r8 U. N8 w* T( N: Z* e# W
  7.      55         .name           = _name,0 m, ^$ E6 k% ?2 n
  8.      565 r3 j+ B7 \1 @& o
  9.      57 #define MACHINE_END                             \
    ) T) M5 P* ?" R
  10.      58 };( j7 d( t8 T9 e; |1 G
  11.      /* 用法 */! {) N/ }: T( z% e( R4 X
  12.      93 MACHINE_START(OMAP_GENERIC, "Generic OMAP1510/1610/1710")1 Z+ v$ l& {, f: h- @* l1 q
  13.      94         /* Maintainer: Tony Lindgren <tony@atomide.com> */) d. ~& s( }5 v8 f& }& `9 x
  14.      95         .phys_io        = 0xfff00000,  x$ V( S: ^: ^, j  B% v; s
  15.      96         .io_pg_offst    = ((0xfef00000) >> 18) & 0xfffc,
    7 U* C; g% F1 T9 d
  16.      97         .boot_params    = 0x10000100," s( o& ?( R6 M2 I! J6 j
  17.      98         .map_io         = omap_generic_map_io,
    6 w) d8 _8 `6 {# k8 r1 ~1 M, ~- |6 l
  18.      99         .init_irq       = omap_generic_init_irq,
    0 D$ l' [- x; w7 B, H, G/ S' H" f5 p
  19.     100         .init_machine   = omap_generic_init,4 D$ {, b3 Z6 a% Q# C8 S. E6 h
  20.     101         .timer          = &omap_timer,
    3 d( e1 t+ Y# c$ D* I0 R
  21.     102 MACHINE_END
    ( C# T4 ^% ?, p
  22. : T& E" u, _; v0 G$ E8 w
  23.     /* func */$ \3 N* X& s" {& z
  24.     204         .type   __lookup_machine_type, %function
    9 l! H* @- U" e4 h
  25.     205 __lookup_machine_type:  w& E) s4 S5 v  X
  26.     206         adr     r3, 3b5 n" ^* d# h* ^% F* e+ `
  27.     207         ldmia   r3, {r4, r5, r6}
    % {" O! R/ Q+ f) n, |7 F8 D
  28.     208         sub     r3, r3, r4                      @ get offset between virt&phys
    $ a5 f& J3 X3 {, Y6 j9 N# {0 {. ~
  29.     209         add     r5, r5, r3                      @ convert virt addresses to8 {1 F$ p. }; b2 g. m
  30.     210         add     r6, r6, r3                      @ physical address space
    + h7 n! a0 L! M6 k  z- d* j' N
  31.     211 1:      ldr     r3, [r5, #MACHINFO_TYPE]        @ get machine type
    1 c2 H7 a  R2 C0 |# W' x) b) F6 `
  32.     212         teq     r3, r1                          @ matches loader number?
    8 l- S8 W* a; o2 P+ a+ g7 u/ q0 }
  33.     213         beq     2f                              @ found
    * j) r, A9 O! B" ?# S. I
  34.     214         add     r5, r5, #SIZEOF_MACHINE_DESC    @ next machine_desc
    * E# C* o) t, F
  35.     215         cmp     r5, r6; X7 {7 N9 e  n" s7 k7 O; D+ W
  36.     216         blo     1b% K1 l% C* F, F% s' ~- }
  37.     217         mov     r5, #0                          @ unknown machine  H3 w! N. O+ C
  38.     218 2:      mov     pc, lr
複製代碼
4#
 樓主| 發表於 2008-10-13 17:56:46 | 只看該作者
接著我們又返回到head.S,
8 Z. L: k. W* d" [line 87~88也是做check動作。
) `8 G% o9 V0 O5 D/ c% p/ Hline 89跳到vet_atags。在head-common.S
  1.      87         movs    r8, r5                          @ invalid machine (r5=0)?
    6 E) e* R7 o9 R
  2.      88         beq     __error_a                       @ yes, error 'a'* q0 u4 q, z7 n
  3.      89         bl      __vet_atags
    " \: c3 c( h* _: }# E/ v+ _& a0 V8 Z
  4.      90         bl      __create_page_tables
複製代碼
line 245, tst會去做and動作。並且update flags。這邊的用意是判斷位址是不是aligned。, p) O, K( r# Y" G
line 246, 沒有aligned跳到label 1,就返回了。
1 O% |. T( a( H" @. Kline 248~250, 讀取atags所在的address裡頭的值到r5,看看是否不等於ATAG_CORE_SIZE,不等於的話也是返回。
6 J9 g7 H, j9 a8 c& sline 251~254, 判斷一下第一個達到的atag pointer是不是等於ATAG_CORE。如果正確的話,等一下要讀取atag的資料,才會正確。
+ t9 N! O$ ^/ N& H(atag是由bootloader帶給linux kernel的東西,用來告知booting所需要知道的參數。例如螢幕寬度,記憶體大小等等)
  1.      14 #define ATAG_CORE 0x54410001
    % z5 N$ b. V) s
  2.      15 #define ATAG_CORE_SIZE ((2*4 + 3*4) >> 2)
    % \2 {- o. b9 Q1 Y1 [3 |

  3. / B! k0 s+ @0 ~* M, q3 u, P) z& r5 ^8 k. A
  4.     243         .type   __vet_atags, %function# J# I$ y2 p  |- W' y9 o
  5.     244 __vet_atags:2 y: e9 s5 E1 D2 G7 ~- F" x, u$ p
  6.     245         tst     r2, #0x3                        @ aligned?
    : J  u" L- K. `4 g& D9 h3 d
  7.     246         bne     1f
    * Q: X7 [! t; f0 }% j
  8.     247) R4 S" F& T4 g
  9.     248         ldr     r5, [r2, #0]                    @ is first tag ATAG_CORE?
    9 o& B8 q0 W0 u" ^* b. N* s
  10.     249         subs    r5, r5, #ATAG_CORE_SIZE
    % W) t! s" u5 R# g( x
  11.     250         bne     1f
    - {1 e6 z, @% J: u$ }. p
  12.     251         ldr     r5, [r2, #4]
    & E. t. R' |1 p! |' E% O# e- Z
  13.     252         ldr     r6, =ATAG_CORE
    - S' l0 D7 A: B# O8 I( V
  14.     253         cmp     r5, r6: @2 J/ i3 h8 k; V/ `
  15.     254         bne     1f7 e9 [0 W5 ~% P# F6 ^
  16.     2554 s* s' [! u; G5 O$ U1 `8 s
  17.     256         mov     pc, lr                          @ atag pointer is ok4 b+ d1 L  T$ W7 b7 L
  18.     257
    3 q6 o5 {! H4 R+ s0 @7 s  Z
  19.     258 1:      mov     r2, #08 d. J+ y" t4 P4 @2 P/ G$ _
  20.     259         mov     pc, lr
複製代碼
接著我們又跳回去head.S。  ; u: B9 e1 g& u7 i  |9 f1 s
line 90,又跳到 __create_page_tables。   (很累人....應該會死不少腦細胞)6 g5 T  y, J* H* k8 N
哇!page table?!!如雷貫耳的東西,不知道會怎麼做。剛剛偷看了一下,@@還蠻長了,先到這邊好了,下次在繼續寫。
5#
 樓主| 發表於 2008-10-14 12:13:51 | 只看該作者
由於code看起來似乎越來越難解釋,會需要在檔案之間跳來跳去。建議一些基礎的東西可以再多複習幾次(其實是在說我自己 ),閱讀source code上收穫會比較多。例如:
, q  z4 a! ?5 j$ v% M( K* Y# l9 ^
1. arm instruction set - 這個最好每個指令的意思都大略看過一次,行有餘力多看幾個版本,armv4, v5 or v6。3 o* N% g' [# o% i2 u3 N
2. compiler & assembler & linker - toolchain工具做的事情和功能,大致的流程和功能要有概念。
+ `- }& W6 ?3 ^9 a3. Makefile & link script - 這兩個功能和撰寫的方式要有簡單的概念。- V: ^' ^+ o1 R8 s1 s& b( \
4 h" e5 G$ N$ L; f
以上1是非常重要的重點,2&3只要有大致上的概念就可以,因為trace code的時候,有時需要跳到Makeflie&Link script去看最後object code編排的位址。+ l" X! C  p, y9 u' z2 f* ~
1 H9 w! j* k# F4 w* g$ q
由於我們trace到了page table這個關鍵字,在開始之前,稍微簡短的解釋,為了幫助了解,儘量用易懂的概念講,有些用詞可能會和一些真實狀況不同,但是懂了之後,應該會有能力分辨,至於正式而學術上解說,很多書上應該都有,或是google一下就很多啦∼
6#
 樓主| 發表於 2008-10-14 12:14:47 | 只看該作者
page table本身是很抽象的東西,尤其是對所謂的 user-mode application programmer 來說,大部分的狀況它是不需要被考慮到的部份,但是他的產生卻對os和driver帶來了很多影響。我們從一個小小的疑問出發。
2 u: s! Y1 Z& K1 M9 s- @' M2 w" \) l7 @/ y" X
『產生page table到底是要給誰用的?』5 p5 A! z6 H- e" X
* y$ ]$ d2 ?- ^* ~+ N% d
其實真正作用在它上面的H/W是MMU,一旦CPU啟用了MMU,當cpu嘗試去記憶體讀取一個operand的時候,cpu內部打出去的位址訊號都會先送到mmu,mmu會拿著這個位址去對照page table,看看這個位址是不是被轉換到另外一個位置(還會確認讀寫權力),最後才會到真正的位址去讀寫。
1 ]0 R0 t4 n. s" k( |* |
( [& B  g& y8 k" I5 ?這樣來看CPU其實一開始打出去的位址訊號其實不是最後可以拿到資料的位址,我們稱為virtual address(va),mmu打出來的位址才能真正拿到資料,所以我們把mmu打出去的位址稱為physical address(pa)。那用來查詢這個位址對照關係的表格,就是page table。
+ ^% R, O1 l; u1 h9 c  O7 _  m9 |: \" R
到這邊我們有一個簡單的概念。來想像一個小問題,一個普通的周邊(例如你的顯示卡),因為他並不具有MMU功能,hw只看得懂pa,但是CPU卻是作用在va,假如我想去對hw的控制暫存器做讀寫,到底要用va還是pa?
7#
 樓主| 發表於 2008-10-14 12:15:51 | 只看該作者
這時,寫driver的人就必須要小心,設定給硬體看的位址必須要先轉成pa(像是設定DMA),給CPU執行的程式碼要使用va,這對一開始嘗試寫driver但是又不瞭解va pa之間的差別的人,常常產生很多疑問。9 G) a2 W/ z- z0 R

3 E2 s# k8 c( o# S' X3 a9 O現在我們回頭想想OS,既然我們跑到create page table,可以預期的是他想要將MMU打開,因此希望預先建立起一個page table,讓MMU知道目前os想規劃的位址對應和讀取權力是如何被安排的。所以os必須考慮到現在的系統究竟是長怎樣?應該要如何被安排?是不是有那些要被保護?好吧∼因為我們完全對os不了解,也不知道該安排什麼,只能祈禱在trace code完後,可以找到這些問題的答案,或者發現一些沒想到的問題。# {2 c6 U+ J' L9 W$ p8 g
- J) t- _  N; C# d9 }5 j& Z* v5 `
知道了page table的大致上的功能,下篇就可以專心的研究這個table的長相,和它想規劃出的系統模樣。. E+ Q% @/ g; S

- t' h% y- q/ Ap.s. 字數限制好像變短了。   (看來很難寫)
8#
 樓主| 發表於 2008-10-14 13:56:17 | 只看該作者
由於字數限制挑整,用詞儘量精簡,以便可以貼必要source。另外,以後寫到一段落,考慮收集成一篇完整的文章,這樣應就不會因為用回覆的方式,造成閱讀斷斷續續的問題。希望有人對文章呈現方式有想法的話,可以跟我講。
9#
 樓主| 發表於 2008-10-14 13:57:39 | 只看該作者
現在,讓我們跳入create_page_tables吧∼
; D7 g& P( j9 V% n7 lline 216,會跳到pgtbl的macro去執行,其實只是載入一個位址。
- R( x) G, A5 F0 M. S& }) F
3 A- ^* x3 {* }0 r2 X; 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 */8 C) H( Z6 f, l5 i8 I) a8 {4 y
  2.      95 textofs-y       := 0x00008000
    " `8 k/ l- m3 K+ `
  3.     152 TEXT_OFFSET := $(textofs-y)
複製代碼
  1. /* include/asm-arm/arch-omap/memory.h */2 t: F- f7 U/ ^: l+ [
  2.      40 #define PHYS_OFFSET             UL(0x10000000)$ l3 f- P1 Q, c. n2 t" [5 L

  3. 6 P# ^+ c. N* T$ w( P3 _; ]8 _; e
  4.      /* arch/arm/kernel/head.S */
    " d! k5 ~  G' A, U0 n
  5.      29 #define KERNEL_RAM_VADDR        (PAGE_OFFSET + TEXT_OFFSET)
    ; r. A+ E" b2 a) i) W3 |4 H& i( t
  6.      30 #define KERNEL_RAM_PADDR        (PHYS_OFFSET + TEXT_OFFSET)
    $ [  j& _0 O7 M$ C3 \0 \

  7. & [6 n1 ]* P' Q- m$ J8 R+ |9 K- t6 ^
  8.      47         .macro  pgtbl, rd3 ]9 U" s, S% |1 N6 W( w  F
  9.      48         ldr     \rd, =(KERNEL_RAM_PADDR - 0x4000)
    / y  n. x$ z4 R/ o, T
  10.      49         .endm1 }0 _+ R, A2 D" x- z

  11. 5 \6 u7 R2 k8 E" F7 V5 B, F
  12.     216         pgtbl   r4                              @ page table address
複製代碼
10#
 樓主| 發表於 2008-10-14 14:16:53 | 只看該作者
得到pg table的開始位置之後,當然就是初始化囉。
: U1 K( u. o) sline 221, 將pg table的base addr放到r0.
" b& z; `% _/ W+ J7 u# d; }line 223, 將pg table的end addr放到r6.
* J4 W5 \; }  z. kline 224~228, 反覆地將0x0寫到pg table的區段裡頭,每個loop寫16bytes. (4x4),直到碰到end,結果就是把它全部clear成0x0.
  1.     221         mov     r0, r41 M7 [+ f6 f& r* O/ u
  2.     222         mov     r3, #01 Q7 y; N/ a0 [2 [% }: I# F
  3.     223         add     r6, r0, #0x4000
    & e2 _* T% g) \8 ~* K/ i
  4.     224 1:      str     r3, [r0], #4; N* {! m, N- A5 L: T5 h* ?
  5.     225         str     r3, [r0], #4/ P# N: Q% H; X
  6.     226         str     r3, [r0], #4
    6 Z. s4 _4 A. m' f: q& @
  7.     227         str     r3, [r0], #4
      U- c  q3 S! v1 J
  8.     228         teq     r0, r69 X3 C- R9 B" c4 {& p& H
  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 | 只看該作者
問題怎麼填值??
' n( b' A, \) R" l* v拿出ARM的手冊,翻到MMU章節。一看發現page有很多種,還得分first level和second level。
4 _* L& ~% R! d/ D+ u- l4 E6 o+ n
3 g/ |, a9 l  q+ }' r0 o: J* f念書時的印象,是從1st level在去查2nd level,先看怎麼設定1st level (不知以前老師有沒有亂教); q# a+ _5 G4 v2 N3 [2 F9 q- q
1. [31:20]存著section base addr4 L" k9 q$ n% e1 E7 U( q' ^8 X4 n
2. [19:2]存著mmu flags
! r' b; U! x8 X$ {  i4 j3. [1:0]用來辨別這是存放哪種page, 有四種:
! m- D2 m, e, w, `/ e  w6 t0 p# J" k   a. fault (00) b. coarse page (01) c. section (1st level) (10) d. fine page (11)2 j) o0 j- S9 m3 L) o8 w
4. pg tabel資料要存放到 [31:14] = translation base, [13:2] = table index , [1:0] = 0b00 的位址9 H0 U1 B! F: n: A6 y( W2 B

7 ?' e6 K% R, ~4 e9 k/ I來看code是怎麼設定。
, K3 c( G$ A* e, n# h. L: d) |& c. J" V2 D  r8 r: ~
line 239, 將pc的值往右shift 20次放到r6.這是取得前20高位元的資料,有點像是 1.。* W0 n# B( S: N  r* Z6 m% K
line 240, 將r6往左shift 20次之後,和r7做or的動作,剛好也是 2.提到的mmu flags和1.處理好的資料做整理。
# q! w9 M5 u$ k1 s' y) K所以前面兩個做完,就完成了bit[31:2]。6 e0 D6 |* W- x1 F' j# y1 ^6 G
line 241, 將r3的資料寫到r4+(r6<<0x2)的地方去,剛好是4.提到的位址。(lucky)
  1.     239         mov     r6, pc, lsr #203 `" E' _: x- g& e# C/ Z3 O
  2.     240         orr     r3, r7, r6, lsl #20% w* b6 |* e, m' U0 y
  3.     241         str     r3, [r4, r6, lsl #2]
複製代碼
p.s. 用pc值剛好可以算出當前的page。
12#
 樓主| 發表於 2008-10-22 19:47:03 | 只看該作者
最近又被釘上了,開始忙碌,大概沒太多時間貼文∼8 [( x. Z5 T% H8 B
; f, t# S9 @& w; N& z: e
上篇已經將pc所屬的page table entry(pte)設定好,接著繼續看. _/ e# k' H4 R
line 247, 248, 將KERNEL_START的位址往右shift 18算出pte的offset,(等於line239&241,239&241是先shift right 20然後shift left 2),並將剛剛r3的值設給pte。『!』的意思是會把address存到r0。
! u3 s  F: ^3 ^  X+ Z- V6 o
, J5 _' u# m+ t3 ]5 m4 Z/ z' xline 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
    - _' F) g3 t% G9 Y7 h+ N% j6 p9 L
  2.     248         str     r3, [r0, #(KERNEL_START & 0x00f00000) >> 18]! : t+ f, h- q3 S: {3 m2 t) ~5 {4 V5 e
  3.     249         ldr     r6, =(KERNEL_END - 1)  A- q( e( F0 E, p# ?% |
  4.     250         add     r0, r0, #4
    % d: P( p$ D7 b0 g% }, w  I
  5.     251         add     r6, r4, r6, lsr #18
    4 b1 b! x( I# M9 S
  6.     252 1:      cmp     r0, r6
    8 m5 r  K7 n" r* F* ~7 w
  7.     253         add     r3, r3, #1 << 20
    5 h9 p/ K, O% s- }* h4 ~" ^; w
  8.     254         strls   r3, [r0], #4
    / J# D9 G% [1 f$ H; P1 J; d
  9.     255         bls     1b
複製代碼
13#
 樓主| 發表於 2008-10-22 20:24:58 | 只看該作者
line279,PAGE_OFFSET是規範RAM mapping完後的virtual address,我們將這個位址所屬的pte算出來放到r0。
: e/ ~+ a( z  @- Sline 280~283,將要 map 的physical address的方式算出來放到r6。
& R* c1 d. ~' E3 V* e1 fline 284,最後將結果存到r0所指到的pte。0 G2 k) N& x% h0 Z

( K0 X; y0 K; L以上三個動作,就是做好 ram 起頭的位址一開始map,還沒做完整塊map。由於我們目前的設定方式每塊是1MB,所以這邊是將RAM開始的1MB做map。; W  w6 g- A) `; K$ P1 n* b% N" r

  g0 w+ ^3 ?6 m  [line 327,返回,結束一開始的create page table的動作,我們可以看出其實並沒有做完整的初始化page table,所以之後應該會有其他page table的細節。
  1.     279         add     r0, r4, #PAGE_OFFSET >> 18
    * L; _" |% n- R$ _6 [1 L
  2.     280         orr     r6, r7, #(PHYS_OFFSET & 0xff000000)" ^& c+ k5 a- }( F/ X1 }
  3.     281         .if     (PHYS_OFFSET & 0x00f00000): I( u2 f, o( H, Z' ^
  4.     282         orr     r6, r6, #(PHYS_OFFSET & 0x00f00000)- W2 B" m" [9 b! l* E3 R- t9 M
  5.     283         .endif/ p! y. p) E" J! P1 g! k
  6.     284         str     r6, [r0]4 _) P. X$ l7 Y+ T( I+ ]' x: a  C$ |
  7.     327         mov     pc, lr
複製代碼
附帶一提,我們這邊省略了一些用ifdef包起來的程式碼,像是一開始會印一些output message的程式碼(line286~326)。
14#
 樓主| 發表於 2008-10-22 20:37:08 | 只看該作者
自create page table返回後,我們偷偷看一下接下來的程式碼,
) F' Y2 y3 ]7 D- c2 Y$ Z$ xline 99, 將switch_data擺到r13
7 b7 s* F+ y7 A9 \- C4 \# E. cline 101, 將enable_mmu擺到lr+ K" f' e9 c! D6 s$ V, R; k5 z) f
line 102, 將pc改跳到r10+PROCINFO_INITFUNC的地方去
2 n: g6 h$ H5 E, C# I/ A8 j) e" b8 {. ~. X1 p- ^1 I" ~) g
其實這邊有點玄機,switch_data和enable_mmu都是function,結果位址都只是被存起來,並沒有直接跳過去執行。其實,雖然程式碼是順序switch_data->enable_mmu->proc init function,但其實執行的順序會是 procinfo 的init function-> enable_mmu -> switch_data 。至於,為什麼要這樣寫的原因?就不清楚了,也還沒仔細推敲過。
  r0 L6 R5 }7 e3 m/ E
8 }& B5 b, U$ p. g% P4 F6 u+ t" ~7 aswitch_data最後就會跳到大家都很熟悉的start_kernel(). 詳細要賣個關子,最近會忙一下,得要過一陣子才能貼文∼  
  1.      99         ldr     r13, __switch_data              @ address to jump to after) O) X: S2 Y0 ?# r
  2.     100                                                 @ mmu has been enabled9 K' H8 ?4 b  L4 v
  3.     101         adr     lr, __enable_mmu                @ return (PIC) address7 Y6 B/ U7 U: l) p* K- ~
  4.     102         add     pc, r10, #PROCINFO_INITFUNC
複製代碼
15#
 樓主| 發表於 2009-7-4 01:09:36 | 只看該作者
老店重新開張~5 H- ]" [- |- M1 {8 \
8 O- e. q0 C9 p) i) Z
花了一些時間把舊的貼文整理到一個blog) p( p' r, E. D: v3 |* q: D3 n/ G
有把一些敘述修改過
+ ^3 V' B9 j/ [8 J9 m) _/ q3 @3 j希望會比較容易集中閱讀$ Q- [  a# Z, x0 B4 E( ?. D; w$ ~+ m6 F
目前因為某些敘述不容易& Y" f* @) {* h4 Y0 X; p
還是比較偏向筆記式而且用字不夠精確
% U4 _; L# O* v0 y- y希望之後能夠慢慢有系統地整理
9 ~* S3 Q8 j' x# {1 b( I: ^大家有興趣的話
8 Q0 C8 Q& F8 Y/ k7 J) Z) @8 g可以來看看和討論 0 H5 S, P8 b# r; o% f* O; e
http://gogojesseco.blogspot.com/
0 Y4 P, w/ E; P) i' y; [/ v! I- T7 ~* Y- h
以後可能會採取  先在chip123貼新文章6 g  @% g& D4 Y7 u3 ?' K/ x" M
慢慢整理到blog上的方式
5 f& \( x8 o6 b. y& o4 r, o因為chip123比較方便討論 =)/ g0 A, E1 T# O, u
blog編輯修改起來比較方便1 [' z* c* |# j
閱讀也比較集中   大家可以在這邊看到討論
8 |2 T2 _- e# x  N3 b6 Z然後在blog看到完整的文章 (類似BBS精華區的感覺)

評分

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

查看全部評分

16#
 樓主| 發表於 2009-7-15 17:07:03 | 只看該作者
隔了很長一段時間沒update
4 m  C$ U: P' w4 |之前程式碼走到 ./arch/arm/kernel/head.S 的 line 99 附近
  1.      99         ldr     r13, __switch_data              @ address to jump to after
    ' G3 Z, G! M( |* J5 h
  2.     100                                                 @ mmu has been enabled
    2 P" T3 ]1 |2 [5 U
  3.     101         adr     lr, __enable_mmu                @ return (PIC) address
    & s* m5 `: z7 T; b8 Q0 a
  4.     102         add     pc, r10, #PROCINFO_INITFUNC
複製代碼
line 99, 將__switch_data放到r13。(留作之後用)
0 ?; H- m) m8 J# n4 X; ?line 101, 將__enable_mmu的addr放到lr。(留作之後用)
# c# s4 E1 [$ e/ X* y* hline 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
    2 \/ v4 T5 P6 k1 q) c, P; E
  2. 374 __arm926_setup:6 F0 W# s& ]7 }  I: v
  3. 375         mov     r0, #08 E1 V: J$ b# B5 T# u& Q# L
  4. 376         mcr     p15, 0, r0, c7, c7              @ invalidate I,D caches on v4
      z% o- x1 I, m) L( b% _
  5. 377         mcr     p15, 0, r0, c7, c10, 4          @ drain write buffer on v4% D; I. n7 L+ M1 m; |2 S
  6. 378 #ifdef CONFIG_MMU7 {, e: Z6 t  v' o
  7. 379         mcr     p15, 0, r0, c8, c7              @ invalidate I,D TLBs on v45 P% v! G) Y2 b' f; x; j% V
  8. 380 #endif- G- C  `2 |; k. A* |- y

  9. 0 c1 J7 U% y5 K8 }  U: g5 Q
  10. 388         adr     r5, arm926_crval7 p. B+ a" Y; r3 F4 ^3 I
  11. 389         ldmia   r5, {r5, r6}1 f* r7 h0 Y+ Z! N0 O. `
  12. 390         mrc     p15, 0, r0, c1, c0              @ get control register v42 `: C( U: A* [! m. j1 O
  13. 391         bic     r0, r0, r5
    : N9 P8 [; d1 K5 k: v6 r
  14. 392         orr     r0, r0, r62 E# d. [6 \& g2 F7 R: n
  15. ! Y4 P) t2 |0 D2 \0 C
  16. 396         mov     pc, lr
    $ }. }5 H9 W. b) x' ~. m
  17. 397         .size   __arm926_setup, . - __arm926_setup
複製代碼
這邊的程式碼就跟CPU有很大的相依性,2 ^: U( ?$ q5 r
line 375~380, 主要就是invalidate CPU的I&D cache和清空write buffer。7 \/ R* Q  g3 H
line 388~392, 把cp15的設定讀出來,並且將一些預設狀態做好運算放到r0。(預設值從arm926_crval這邊可以取得。)
* j4 X3 r5 @2 W  f9 r9 |  U8 rline 396, 直接把pc跳到lr,因為我們之前已經將lr = enable_mmu,所以直接跳過去。
17#
 樓主| 發表於 2009-7-15 17:29:45 | 只看該作者
  1. 155 __enable_mmu:
    $ J4 ?1 b0 W& d
  2. 170         mov     r5, #(domain_val(DOMAIN_USER, DOMAIN_MANAGER) | \/ |2 `& ], C' ^/ u
  3. 171                       domain_val(DOMAIN_KERNEL, DOMAIN_MANAGER) | \
    - Q5 F. F& J$ L" H/ B+ I
  4. 172                       domain_val(DOMAIN_TABLE, DOMAIN_MANAGER) | \1 a; J% X4 V8 N$ _
  5. 173                       domain_val(DOMAIN_IO, DOMAIN_CLIENT))
    , W: v4 ]% q9 }4 Z, G$ g2 ~
  6. 174         mcr     p15, 0, r5, c3, c0, 0           @ load domain access register2 X: U+ w8 C1 L/ S
  7. 175         mcr     p15, 0, r4, c2, c0, 0           @ load page table pointer3 k5 g& o8 \, Q# t
  8. 176         b       __turn_mmu_on
    2 r0 t- C. p6 f3 D" r
  9. 177 ENDPROC(__enable_mmu)
複製代碼
line 170~174,設置好domain access。(domain access可以先當成設置access的權限,或許以後可以寫詳細的文章說明)
9 @- X( X" I" L; Q3 Kline 175~176,將create好的page table開頭丟給mmu。跳到turn_mmu_on
  1. 191 __turn_mmu_on:
    " a7 X5 F0 [: U8 L: n
  2. 192         mov     r0, r06 c) C5 Y- T9 x  J
  3. 193         mcr     p15, 0, r0, c1, c0, 0           @ write control reg
    + {: B7 i6 k; [7 a$ i% _  \
  4. 194         mrc     p15, 0, r3, c0, c0, 0           @ read id reg
    % D, N+ W- g+ ]2 ?7 n; o
  5. 195         mov     r3, r3
    ; e+ ~* c% T3 y$ A# M$ B
  6. 196         mov     r3, r3
    6 e( ], q) g0 r4 _
  7. 197         mov     pc, r13
    7 v6 L) a6 X* w1 ~+ O# s2 u
  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:
    6 S0 K$ u: K/ M  g' Y) m8 N
  2. 19         .long   __mmap_switched
    & D0 n, d% E0 u, ]
  3. 20         .long   __data_loc                      @ r49 k; j& _0 b$ s5 k) ?
  4. 21         .long   _data                           @ r5
    - h7 u0 O* E% d, f5 l$ P2 f5 l' m2 M
  5. 22         .long   __bss_start                     @ r6# [6 ^8 W0 m9 ^* i& Y! f4 r
  6. 23         .long   _end                            @ r7( k/ }, ~, y( R/ B, v- ^
  7. 24         .long   processor_id                    @ r44 \, M# R2 o% d6 p
  8. 25         .long   __machine_arch_type             @ r5
    & [# m& X, r7 L3 m
  9. 26         .long   __atags_pointer                 @ r6* K* t. s: R" `# @6 z$ x) L
  10. 27         .long   cr_alignment                    @ r7
    : [( @4 Y9 A) w4 @/ _" U4 J
  11. 28         .long   init_thread_union + THREAD_START_SP @ sp: P1 Q6 n) v! b
  12. 29! B4 q0 B5 `/ j: s5 x" Y
複製代碼
switch_data的第一行就是 __mmap_switched,所以我們直接看line 39。
18#
 樓主| 發表於 2009-7-15 17:30:00 | 只看該作者
  1. 39 __mmap_switched:
    / ]1 i% i( n# A$ n8 ^. V
  2. 40         adr     r3, __switch_data + 4
    5 Q. y8 ]6 c0 C. V
  3. 41
    ' {1 R. I0 v1 b+ x" @* V5 L
  4. 42         ldmia   r3!, {r4, r5, r6, r7}
    4 m# `; T" |+ B3 j, t
  5. 43         cmp     r4, r5                          @ Copy data segment if needed
    + b9 q6 s9 N( V- [
  6. 44 1:      cmpne   r5, r6/ Q3 Y  a- z6 @( Z
  7. 45         ldrne   fp, [r4], #4
    & ]6 @# W8 C9 H* |  d
  8. 46         strne   fp, [r5], #4' q: D' E% \* D/ @$ L% t. U1 p8 Y
  9. 47         bne     1b
    * @. N* A1 Y" J' d3 c. b9 W1 i
  10. 48
    ' H  r4 g! N/ v% J" P
  11. 49         mov     fp, #0                          @ Clear BSS (and zero fp)
    * S) ?$ V: \+ e# o# h" z
  12. 50 1:      cmp     r6, r7
    5 M. R, r' N6 I$ R
  13. 51         strcc   fp, [r6],#4; A& e( R* M% F% E
  14. 52         bcc     1b
    ( f& t" [1 v# b: _, k! Y$ F
  15. 53
    . }7 h) o* k# ]6 W, P; l" x- P
  16. 54         ldmia   r3, {r4, r5, r6, r7, sp}5 ~/ g6 y: C# n! N9 Z
  17. 55         str     r9, [r4]                        @ Save processor ID
    / G) Y7 E- j; j! y$ v9 k! m, p9 a$ H
  18. 56         str     r1, [r5]                        @ Save machine type' P0 V0 z. k$ X1 s
  19. 57         str     r2, [r6]                        @ Save atags pointer" @5 J- M0 ]. m0 s; s# R4 @+ L
  20. 58         bic     r4, r0, #CR_A                   @ Clear 'A' bit2 P3 V1 x4 A$ h6 C% N
  21. 59         stmia   r7, {r0, r4}                    @ Save control register values0 p' [8 s: E' D5 y+ G
  22. 60         b       start_kernel: n7 Q! r# n$ m9 G0 @
  23. 61 ENDPROC(__mmap_switched)
複製代碼
switch_data的第一行就是 __mmap_switched,所以我們直接看line 39。
7 w' C# E. H1 l1 g; a2 z; gline 39,將__data_loc的addr放到r3
! u8 o- d1 l8 \line 42,從r3的位址,連續讀取四筆資料到r4, r5, r6, r7/ E/ [% x8 ~' w2 q( m: s" H! x' A3 X: ]
line 43~47,看看data segment是不是需要搬動。' Y. Q- R0 B, @* P, H* c* n
line 49~52, clear BSS。6 }. Z  G' P  Y

0 o/ z* C& y9 V8 g, ?' Q2 b由於linux kernel在進入start_kernel前有一些前提必須要滿足:
  j9 U7 q7 n- [# U4 y% G* Tr0  = cp#15 control register1 |; f$ L/ Q. U7 g9 U" ^( L
r1  = machine ID
1 g% \) w) i2 f( Wr2  = atags pointer
, T' Z- Q' u% A: \# tr9  = processor ID
  |+ m- D8 h7 ]0 Y
- }% E1 B* \, p# ?' m; ^所以line 54~59就是在做這些準備。. `3 F  m3 z# y' V3 w
最後呢? 我們就跳到start_kernel了。(而且還是用b start_kernel,表示我們不會再回來了)/ z) Q* I) B7 G5 N& ?' M

! R2 H; I9 H% E3 C/ q( ?- S& l看一下start_kernel()在./init/main.c,終於跳出architecture specific的目錄,表示
1 F/ B8 H: ?5 g我們真正的開始linux kernel的初始化。# c, v5 E" `! D% q! t! t7 ?+ j- Q
像是 shedule init, console init, memory init, irq init等等都在start_kernel裡頭。+ L' {9 w$ ]' e
到這邊之後,應該就可以深入linux kernel中,各項比較跟hardware不那麼相關的軟體部分。

評分

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

查看全部評分

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

本版積分規則

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

GMT+8, 2024-5-21 11:08 AM , Processed in 0.144018 second(s), 18 queries .

Powered by Discuz! X3.2

© 2001-2013 Comsenz Inc.

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