200909241058Linux/MIPS 核心剖析(1)

Linux/MIPS 核心剖析
cvs -d :pserver:cvs@oss.sgi.com:/cvs login
cvs -d :pserver:cvs@oss.sgi.com:/cvs co linux
password:cvs
經典書籍:
    * "See Mips Run",by Dominic Sweetman
    * "See Mips Run"(中文版)www.xtrj.org/smr.htm
Jun Sun's mips porting guide: http://linux.junsun.net/porting-howto/
Debian Mips port: http://www.debian.org/ports/mips
3. mips kernel的一般介紹
    加電後, mips kernel 從系統固件程式(類似bios,可能燒在eprom,flash中)得到控制
    之後(head.S),
    初始化內核棧,調用 init_arch 初始化硬體平臺相關的代碼.
    init_arch(setup.c)首先監測使用的 CPU(通過MIPS CPU的CP0控制寄存器PRID)確定
    使用的指令集和
    一些 CPU 參數, 如 TLB 大小等.
    然後調用 prom_init 做一些底層參數初始化.
    prom_init 是和具體的硬體相關的.
    使用 MIPS CPU 的平臺多如牛毛, 所以大家在 arch/mips 下面可以看到很多的子目錄,
    每個子目錄是
    一個或者一系列相似的平臺.
    這裏的平臺差不多可以理解成一塊主板加上它的系統固件, 其中很多還包括一些專用的
    顯卡什麼的硬體
    (比如一些工作站).
    這些目錄的主要任務是:
    1. 提供底層板子上的一些重要資訊, 包括系統固件傳遞的參數, io的映射基底位址,
    記憶體的大小的
    分佈等.
        多數還包括提供早期的資訊輸入輸出介面(通常是一個簡單的串口驅動)以方便調試,
        因為pmon往往
        不提供鍵盤和顯示卡的支援.
    2. 底層中斷代碼,包括中斷控制器編程和中斷的分派,應答等
    3. pci 子系統底層代碼. 實現 pci 配置空間的讀寫,以及 pci 設備的中斷, IO/Mem
    空間的分配
    4. 其他, 特定的硬體.常見的有即時時鐘等
這裏關鍵是要理解這些硬體平臺和熟悉的x86不同之處.
    * item MIPS 不象 X86 有很標準的硬體軟體介面, 而是五花八門, 每個廠家有一套,
     因為它們很多是
    嵌入式系統或者專門的工作站.
        不象PC中,有了 BIOS 後用同一套的程式,就可以使用很多不同的主板和CPU.
        MIPS中的 'bios' 常用的有 pmon 和 yamon, 都是開放源代碼的軟體。
        很多開發板帶的固件功能和 PC BIOS 很不一樣,它們多數支援串口顯示, 或者網路
        下載和啟動,以及
        類 DEBUG 的調試介面, 但可能根本不支持顯卡和硬碟,沒有一般的基本'輸入輸出'
        功能.
    * PCI系統和位址空間,匯流排等問題.
        在 x86 中, IO 空間用專門的指令訪問, 而 PCI 設備的記憶體空間和實體記憶體空
        間是相同的, 也就
        是說, 在 CPU 看來實體記憶體從位址 0 開始的話, 在 PCI 設備看來也是一樣的.
        反之, PCI 設備的基底位址寄存器設定的 PCI 存儲位址, CPU 用相同的物理位址訪
        問就行了.
        而在 MIPS 中就很不一樣了, IO一般是 memory map 的, map到哪里就倚賴具體平臺了.
        而 PCI 設備的位址空間和 CPU 所見的實體記憶體位址空間往往也不一樣 (bus address &
        physical address).
        所以 mips kernel 的 iob/outb, 以及 bus_to_virt/virt_to_bus,phys_to_virt/virt_to_phys,
        ioremap 等就要小心考慮.
        這些問題有時間筆者會對這些問題做專門的說明.
        PCI 配置空間的讀寫和位址空間映射的處理通常都是每個平臺不一樣的.
        因為缺乏統一介面的 BIOS, 內核經常要自己做 PCI 設備的枚舉, 空間分配, 中斷分配.
    * 中斷系統.
        PC 中中斷控制器先是有 8259, 後來是 apic, 而 cpu 的中斷處理 386 之後好像也變
        化不大,相對統一.
        mips CPU 的中斷處理方式倒是比較一致, 但是主板上的控制器就亂七八糟了怎麼鑒別
        中斷源, 怎麼編程
        控制器等任務就得各自實現了.
        總的說來, MIPS CPU 的中斷處理方式體現了 RISC 的特點: 軟體做事多,硬體儘量精簡.
        編程控制器, 提供中斷控制介面, dispatch中系統這一部分原來很混亂, 大家各寫各的
        , 現在有人試圖
        寫一些比較統一的代碼(實際上就是原來x86上用的 controller/handler 抽象).
    * 存儲管理.
        MIPS 是典型的RISC結構, 它的存儲管理單元做的事情比象x86這種機器少得多.
        例如, 它的 tlb 是軟體管理的, cache常常是需要系統程式干預的.
        而且,過多的 CPU 和主板變種使得這一部分非常複雜, 容易出錯.
        存儲管理的代碼主要在 include/asm-mips 和 arch/mips/mm/ 目錄下.
    * 其他.
        如時間處理, r4k 以上的 MIPS CPU 提供 count/compare 寄存器, 每隔幾拍 count 增加
        , 到和 compare
        相等時發生時鐘中斷, 這可以用來提供系統的時鐘中斷.
        但很多板子自己也提供其他的可編程時鐘源.
        具體用什麼就取決於開發者了.
init_arch 後是 loadmmu, 初始化 cache/tlb.
代碼在arch/mips/mm裏.
有人可能會問, 在 cache 和 tlb 之前 CPU 怎麼工作的?
在 x86 裏有實模式, 而 MIPS 沒有, 但它的位址空間是特殊的, 分成幾個不同的區域, 每個區
域中的位址在 CPU
裏的待遇是不一樣的, 系統剛上電時 CPU 從位址 bfc00000 開始, 那裏的位址既不用 tlb 也
不用 cache, 所以
 CPU 能工作而不管 cache 和 tlb 是什麼樣子.
當然, 這樣子效率是很低的, 所以 CPU 很快就開始進行 loadmmu.
因為 MIPS CPU 變種繁多, 所以代碼又臭又長.
主要不外是檢測 cache 大小, 選擇相應的 cache/tlb flush 過程, 還有一些 memcpy/memset
等的高效實現.
這裏還很容易出微妙的錯誤, 軟體管理 tlb 或者 cache 都不簡單, 要保證效率又要保證正確.
在開發初期常常先關掉 CPU 的 cache 以便排除 cache 問題.
MMU 初始化後, 系統就直接跳轉到 init/main.c 中的 start_kernel, 很快吧?
不過別高興 start_kernel 虛晃一槍, 又回到 arch/mips/kernel/setup.c, 調用 setup_arch
這回就是完成上
面說的各平臺相關的初始化了.
平臺相關的初始化完成之後 mips 內核和其他平臺的內核區別就不大了, 但也還有不少問題需
要關注.
如許多驅動程式可能因為倚賴 x86 的特殊屬性(如IO埠,自動的 cache 一致性維護,顯卡初始化
等)而不能直接
在MIPS下工作.
例如, 能直接(用現有的內核驅動)在 MIPS 下工作的網卡不是很多,筆者知道的有 intel eepro100,
AMD pcnet32, Tulip.
3com 的網卡好像大多不能用.
顯卡則由於 vga bios 的問題, 很少能直接使用.
(常見的顯卡都是為 x86 做的,它們常常帶著一塊 rom, 裏面含有 vga bios, PC 的 BIOS
的初始化過程中發現它
們的化就會先去執行它們以初始化顯卡, 然後才能很早地在螢幕上輸出資訊).
而 vga bios 裏面的代碼一般是 for x86, 不能直接在 mips CPU 上運行.
而且這些代碼裏常常有一些廠家相關的特定初始化, 沒有一個通用的代碼可以替換.
只有少數比較開放的廠家提供足夠的資料使得內核開發人員能夠跳過 vga bios 的執行直接初始化
他的顯卡, 如 matrox.
除此之外, 也可能有其他的內核代碼由於種種原因(不對齊訪問, unsigned/signed 等)不能使用,
如一些檔系統(xfs).
關於 linux-mips 內核的問題, 在 sgi 的 mailing list 搜索或者提問比較有希望獲得解決.
如果你足夠有錢,可以購買 montivista 的服務. http://www.mvista.com.
4. mips的異常處理
1.硬體
    mips CPU 的異常處理中, 硬體做的事情很少, 這也是 RISC 的特點.
    和 x86 系統相比, 有兩點大不一樣:
    * 硬體不負責具體鑒別異常, CPU 回應異常之後需要根據狀態寄存器等來確定究竟發生哪個異常.
        有時候硬體會做一點簡單分類, CPU能直接到某一類異常的處理入口.
    * 硬體通常不負責保存上下文.
       例如系統調用,所有的寄存器內容都要由軟體進行必要的保存.
       各種主板的中斷控制種類很多,需要根據中斷控制器和連線情況來編程.
2.kernel實現
    * 處理程式什麼時候安裝?
    traps_init(arch/mips/kernel/traps.c,setup_arch 之後 start_kernel 調用)
    ...
    /* Copy the generic exception handler code to it's final destination. */
    memcpy((void *)(KSEG0 + 0x80), &except_vec1_generic, 0x80);
    memcpy((void *)(KSEG0 + 0x100), &except_vec2_generic, 0x80);
    memcpy((void *)(KSEG0 + 0x180), &except_vec3_generic, 0x80);
    flush_icache_range(KSEG0 + 0x80, KSEG0 + 0x200);
    /*
     * Setup default vectors
     */
    for (i = 0; i <= 31; i++)
        set_except_vector(i, handle_reserved);
    ...
    * 裝的什麼?
    except_vec3_generic(head.S) (除了TLB refill例外都用這個入口):
    /* General exception vector R4000 version. */
    NESTED(except_vec3_r4000, 0, sp)
    .set noat
    mfc0 k1, CP_CAUSE
    andi k1, k1, 0x7c /* 從cause寄存器取出異常號 */
    li     k0, 31<<2      beq  k1, k0, handle_vced /* 如果是vced,處理之*/
    li    k0, 14><<2       beq  k1, k0, handle_vcei /* 如果是vcei,處理之*/
    /* 這兩個異常是和cache相關的,cache出了問題,不能再在這個cached的位置處理啦 */
    la    k0, exception_handlers /* 取出異常處理程式表 */
    addu     k0, k0, k1     lw   k0, (k0) /*處理函數*/
    nop      jr      k0    /*運行異常處理函數*/
    nop
    那個異常處理程式表是如何初始化的呢?
    在 traps_init 中, 大家會看到 set_exception_vector(i,handler) 這樣的代碼,
    填的就是這張表啦.
    可是,如果你用 souce insigh 之類的東西去找那個 handler, 往往就落空了, 怎麼
    沒有 handle_ri,
    handle_tlbl..._ 不著急, 只不過是一個小 trick, 還記得 x86 中斷處理的
    handler 代碼嗎?
    它們是用宏生成的:
    entry.S      ...
    #define BUILD_HANDLER(exception,handler,clear,verbose)
    .align 5;
    NESTED(handle_##exception, PT_SIZE, sp);
    .set noat;
    SAVE_ALL; /* 保存現場,切換棧(如必要)*/
    __BUILD_clear_##clear(exception); /* 關中斷 */
    .set at;
    __BUILD_##verbose(exception);
    jal do_##handler; /*幹活*/
    move a0, sp;
    ret_from_exception; /*回去*/
    nop;
    END(handle_##exception)         /*生成處理函數*/
    BUILD_HANDLER(adel,ade,ade,silent)       /* #4 */
    BUILD_HANDLER(ades,ade,ade,silent)      /* #5 */
    BUILD_HANDLER(ibe,ibe,cli,verbose)        /* #6 */
    BUILD_HANDLER(dbe,dbe,cli,silent)         /* #7 */
    BUILD_HANDLER(bp,bp,sti,silent)          /* #9 */
    認真追究下去, 這裏的一些宏是很重要的, 象SAVE_ALL(include/asm/stackframe.h),
    異常處理要
    高效, 正確, 這裏要非常小心.
    這是因為硬體做的事情實在太少了.
    別的暫時先不說了, 下面我們來看外設中斷(它是一種特殊的異常).
    entry.S 並沒有用 BUILD_HANDLER 生成中斷處理函數, 因為它是平臺相關的就以筆者
    的板子為例,
    在 arch/mips/algor/p6032/kernel/ 中(標準內核沒有)增加了 p6032IRQ.S 這個彙編檔,
     裏面定義了
    一個 p6032IRQ 的函數, 它負責鑒別中斷源, 調用相應的中斷控制器處理代碼,
    而在同目錄的 irq.c->init_IRQ 中調用 set_except_vector(0,p6032IRQ) 填表(所有
    的中斷都引發異
    常 0, 並在 cause 寄存器中設置具體中斷原因).
    下面列出這兩個文件以便解說:
    p6032IRQ.s
    algor p6032(筆者用的開發板)中斷安排如下:
    MIPS IRQ       Source
    *     --------  ------
    *          0   Software (ignored)
    *          1   Software (ignored)
    *          2   bonito interrupt (hw0)
    *          3   i8259A interrupt (hw1)
    *          4   Hardware (ignored)
    *          5   Debug Switch
    *          6   Hardware (ignored)
    *            7    R4k timer (what we use)
    .text
    .set noreorder
    .set noat
    .align 5
    NESTED(p6032IRQ, PT_SIZE, sp)
    SAVE_ALL /* 保存現場,切換堆疊(if usermode -> kernel mode)*/
    CLI        /* 關中斷,mips有多種方法禁止回應中斷,CLI用清status相應位的方法,如下:
                Move to kernel mode and disable interrupts.
                Set cp0 enable bit as sign that we're running
                on the kernel stack */
    #define CLI
    mfc0 t0,CP0_STATUS;
    li     t1,ST0_CU0|0x1f;
    or      t0,t1;
    xori t0,0x1f;
    mtc0 t0,CP0_STATUS
    .set    at
    mfc0 s0, CP0_CAUSE
    /* get irq mask,中斷回應時cause寄存器指示哪類中斷發生
    注意, MIPS CPU 只區分 8 個中斷源, 並沒有象 PC 中從匯流排讀取中斷向量號, 所以每類中斷的
    代碼要自己設法定位中斷
     */
    /* 挨個檢查可能的中斷原因 */
    /* First we check for r4k counter/timer IRQ. */
    andi a0, s0, CAUSEF_IP7
    beq a0, zero, 1f
    andi a0, s0, CAUSEF_IP3         # delay slot, check 8259 interrupt
    /* Wheee, a timer interrupt. */
    li   a0, 63
    jal do_IRQ
    move a1, sp
    j    ret_from_irq
    nop                  # delay slot
    1:    beqz a0,1f
    andi a0, s0, CAUSEF_IP2
    /* Wheee, i8259A interrupt. */
    /* p6032也使用8259來處理一些pc style的設備*/
    jal   i8259A_irqdispatch      /* 調用8259控制器的中斷分派代碼*/
    move a0, sp              # delay slot
    j    ret_from_irq
    nop                  # delay slot
    1:    beq a0, zero, 1f
    andi a0, s0, CAUSEF_IP5
    /* Wheee, bonito interrupt. */
    /* bonito是6032板的北橋,它提供了一個中斷控制器*/
    jal   bonito_irqdispatch
    move a0, sp              # delay slot
    j    ret_from_irq
    nop                  # delay slot
    1:    beqz   a0,1f
    nop
    /* Wheee, a debug interrupt. */
    jal p6032_debug_interrupt
    move a0, sp              # delay slot
    j      ret_from_irq
    nop                      # delay slot
    1:
    /* Here by mistake? This is possible, what can happen
     * is that by the time we take the exception the IRQ
     * pin goes low, so just leave if this is the case.
     */
    j      ret_from_irq
    nop
    END(p6032IRQ)
    irq.c部分代碼如下:
    p6032中斷共有四類:
    begin{enumerate}
        item timer中斷,單獨處理
        item debug中斷,單獨處理
        item 8259中斷,由8259控制器代碼處理
        item bonito中斷由bonito控制器代碼處理
    end{enumerate}
/* now mips kernel is using the same abstraction as x86 kernel,
  that is, all irq in the system are described in an struct
  array: irq_desc[]. Each item of a specific item records
  all the information about this irq,including status,action,
  and the controller that handle it etc. Below is the controller
  structure for bonito irqs,we can easily guess its functionality
  from its names.*/
hw_irq_controller bonito_irq_controller = {
   "bonito_irq",
   bonito_irq_startup,
   bonito_irq_shutdown,
   bonito_irq_enable,
   bonito_irq_disable,
   bonito_irq_ack,
   bonito_irq_end,
   NULL                 /* no affinity stuff for UP */
};
void
bonito_irq_init(u32 irq_base)
{
   extern irq_desc_t irq_desc[];
   u32 i;
   for (i= irq_base; i< P6032INT_END; i++) {
      irq_desc[i].status = IRQ_DISABLED;
      irq_desc[i].action = NULL;
      irq_desc[i].depth = 1;
      irq_desc[i].handler = &bonito_irq_controller;
   }
   bonito_irq_base = irq_base;
}
/* 中斷初始化,核心的資料結構就是irq_desc[]陣列
  它的每個元素對應一個中斷,記錄該中斷的控制器類型,處理函數,狀態等
  關於這些可以參見對x86中斷的分析*/
void __init init_IRQ(void)
{
   Bonito;
   /*
    * Mask out all interrupt by writing "1" to all bit position in
    * the interrupt reset reg.
   */
  BONITO_INTEDGE = BONITO_ICU_SYSTEMERR | BONITO_ICU_MASTERERR
      | BONITO_ICU_RETRYERR | BONITO_ICU_MBOXES;
  BONITO_INTPOL = (1 << (P6032INT_UART1-16))
      | (1 << (P6032INT_ISANMI-16))
      | (1 << (P6032INT_ISAIRQ-16))
      | (1 << (P6032INT_UART0-16));
  BONITO_INTSTEER = 0;
  BONITO_INTENCLR = ~0;
  /* init all controllers */
  init_generic_irq();
  init_i8259_irqs();
  bonito_irq_init(16);
  BONITO_INTSTEER |= 1 << (P6032INT_ISAIRQ-16);
  BONITO_INTENSET = 1 << (P6032INT_ISAIRQ-16);
  /* hook up the first-level interrupt handler */
  set_except_vector(0, p6032IRQ);
  ...
}
/*p6032IRQ發現一個bonito中斷後調用這個*/
asmlinkage void
bonito_irqdispatch(struct pt_regs *regs)
{
  Bonito;
  int irq;
  unsigned long int_status;
  int i;
  /* Get pending sources, masked by current enables */
     /* 到底是哪個中斷呢?從主板寄存器讀*/
     int_status = BONITO_INTISR & BONITO_INTEN & ~(1 << (P6032INT_ISAIRQ-16))
          ;
     /* Scan all pending interrupt bits and execute appropriate actions */
     for (i=0; i<32 && int_status; i++) {
          if (int_status & 1<                 irq = i + 16; /* 0-15 assigned to 8259int,16-48 bonito*/
                      /* Clear bit to optimise loop exit */
                      int_status &= ~(1<
                do_IRQ(irq,regs);
          }
     }
     return;
}
  8259控制器的代碼類似,不再列出.
  更高層一點的通用irq代碼在arch/mips/kernel/irq.c arch/mips/kernel/i8259.c
  總之,p6032上一個中斷的過程是:
   1.外設發出中斷,通過北橋在cpu中斷引腳上(mips CPU有多個中斷引腳)引起異常
   2. cpu自動跳轉到0x80000180的通用異常入口,根據cause寄存器查表找到中斷
      處理函數入口p6032IRQ
   3. p6032IRQ保存上下文,識別中斷類別,把中斷轉交給相應的中斷控制器
   4. 中斷控制器的代碼進一步識別出具體的中斷號,做出相應的應答並調用
      中斷處理do_irq
  現在還有不少平臺沒有使用這種irq_desc[],controller,action的代碼,閱讀的時候
  可能要注意.
  下麵把include/asm-mips/stackframe.h
  對著注解一下,希望能說清楚一些.
  (因為時間關係,筆者寫的文檔將主要以這種檔注解為主,加上筆者認為有用的背景知識
或者分析.)
/*
 一些背景知識
 一.mips彙編有個約定(後來也有些變化,我們不管,o32,n32),32個通用寄存器不是一視同仁
 的,而是分成下列部分:
    寄存器號                  符號名              用途
      0             始終為0          看起來象浪費,其實很有用
      1             at       保留給彙編器使用
      2-3            v0,v1       函數返回值
      4-7            a0-a3      前頭幾個函數參數
      8-15            t0-t7     臨時寄存器,子過程可以不保存就使用
      24-25            t8,t9     同上
      16-23            s0-s7      寄存器變數,子過程要使用它必須先保存
                             然後在退出前恢復以保留調用者需要的值
      26,27            k0,k1      保留給異常處理函數使用
      28             gp        global pointer;用於方便存取全局或者靜態變數
      29             sp       stack pointer
      30             s8/fp      第9個寄存器變數;子過程可以用它做frame pointer
      31              ra      返回地址
  硬體上這些寄存器並沒有區別(除了0號),區分的目的是為了不同的編譯器產生的代碼
  可以通用
 二. r4k MIPS CPU中和異常相關的控制寄存器(這些寄存器由輔助運算器cp0控制,有獨立的
存取方法)有:
    1.status 狀態寄存器
    31 28 27 26 25 24           16 15          876 543 2 1 0
   ------------------------------------------------------------------
   | cu0-3|RP|FR|RE| Diag Status| IM7-IM0 |KX|SX|UX|KSU|ERL|EXL|IE|
------------------------------------------------------------------
其中KSU,ERL,EXL,IE位在這裏很重要:
  KSU: 模式位元 00 -kernel 01--Supervisor 10--User
  ERL: error level,0->normal,1->error
  EXL: exception level,0->normal,1->exception,異常發生是EXL自動置1
  IE: interrupt Enable, 0 -> disable interrupt,1->enable interrupt
  (IM位則可以用於enbale/disable具體某個中斷,ERL||EXL=1 也使得中斷不能回應)
 系統所處的模式由KSU,ERL,EXL決定:
   User mode: KSU = 10 && EXL=0 && ERL=0
   Supervisor mode(never used): KSU=01 && EXL=0 && ERL=0
   Kernel mode: KSU=00 || EXL=1 || ERL=1
 2.cause寄存器
  31 30 29 28 27           16 15          876           2 1 0
 ----------------------------------------------------------------
 |BD|0 | CE | 0            | IP7 - IP0 |0|Exc code | 0 |
 ----------------------------------------------------------------
 異常發生時cause被自動設置
 其中:
   BD指示最近發生的異常指令是否在delay slot中
   CE發生coprocessor unusable異常時的coprocessor編號(mips有4個cp)
   IP: interrupt pending, 1->pending,0->no interrupt,CPU有6個中斷
      引腳,加上兩個軟體中斷(最高兩個)
   Exc code:異常類型,所有的外設中斷為0,系統調用為8,...
 3.EPC
    對一般的異常,EPC包含:
     . 導致異常的指令位址(virtual)
     or. if 異常在delay slot指令發生,該指令前面那個跳轉指令的位址
    當EXL=1時,處理器不寫EPC
 4.和存儲相關的:
   context,BadVaddr,Xcontext,ECC,CacheErr,ErrorEPC
   以後再說
 一般異常處理程式都是先保存一些寄存器,然後清除EXL以便嵌套異常,
 清除KSU保持核心態,IE位元看情況而定;處理完後恢復一些保存內容以及CPU狀態
   */
/* SAVE_ALL 保存所有的寄存器,分成幾個部分,方便不同的需求選用*/
/*保存AT寄存器,sp是棧頂PT_R1是at寄存器在pt_regs結構的偏移量
  .set xxx是彙編指示,告訴彙編器要幹什麼,不要幹什麼,或改變狀態
 */
#define SAVE_AT                            \
          .set push;              \
          .set noat;             \
          sw    $1, PT_R1(sp);       \
          .set pop
/*保存臨時寄存器,以及hi,lo寄存器(用於乘法部件保存64位結果)
 可以看到mfhi(取hi寄存器的值)後並沒有立即保存,這是因為
 流水線中,mfhi的結果一般一拍不能出來,如果下一條指令就想
 用v1則會導致硬體停一拍,這種情況下讓無關的指令先做可以提高
 效率.下面還有許多類似的例子
 */
#define SAVE_TEMP                            \
          mfhi v1;                 \
          sw    $8, PT_R8(sp);       \
          sw    $9, PT_R9(sp);       \
          sw    v1, PT_HI(sp);       \
          mflo v1;                 \
          sw    $10,PT_R10(sp);          \
          sw    $11, PT_R11(sp);         \
          sw    v1, PT_LO(sp);         \
          sw    $12, PT_R12(sp);         \
          sw    $13, PT_R13(sp);         \
          sw    $14, PT_R14(sp);         \
          sw    $15, PT_R15(sp);         \
          sw    $24, PT_R24(sp)
/* s0-s8 */
#define SAVE_STATIC                            \
          sw     $16, PT_R16(sp);          \
          sw     $17, PT_R17(sp);          \
          sw     $18, PT_R18(sp);          \
          sw     $19, PT_R19(sp);          \
          sw     $20, PT_R20(sp);          \
          sw     $21, PT_R21(sp);          \
          sw     $22, PT_R22(sp);          \
          sw     $23, PT_R23(sp);          \
          sw     $30, PT_R30(sp)
#define __str2(x) #x
#define __str(x) __str2(x)
/*ok,下面對這個宏有冗長的注解*/
#define save_static_function(symbol)                       \
__asm__ (                                        \
     ".globl\t" #symbol "\n\t"                        \
     ".align\t2\n\t"                            \
     ".type\t" #symbol ", @function\n\t"                 \
     ".ent\t" #symbol ", 0\n"                        \
     #symbol":\n\t"                                \
     ".frame\t$29, 0, $31\n\t"                        \
     "sw\t$16,"__str(PT_R16)"($29)\t\t\t# save_static_function\n\t" \
     "sw\t$17,"__str(PT_R17)"($29)\n\t"                    \
     "sw\t$18,"__str(PT_R18)"($29)\n\t"                    \
     "sw\t$19,"__str(PT_R19)"($29)\n\t"                    \
     "sw\t$20,"__str(PT_R20)"($29)\n\t"                    \
     "sw\t$21,"__str(PT_R21)"($29)\n\t"                    \
     "sw\t$22,"__str(PT_R22)"($29)\n\t"                    \
     "sw\t$23,"__str(PT_R23)"($29)\n\t"                    \
     "sw\t$30,"__str(PT_R30)"($29)\n\t"                    \
     ".end\t" #symbol "\n\t"                         \
     ".size\t" #symbol",. - " #symbol)
/* Used in declaration of save_static functions. */
#define static_unused static __attribute__((unused))
/*以下這一段涉及比較微妙的問題,沒有興趣可以跳過*/
/* save_static_function宏是一個令人迷惑的東西,它定義了一個彙編函數,保存s0-s8
  可是這個函數沒有返回!實際上,它只是一個函數的一部分:
  在arch/mips/kernel/signal.c中有:
     save_static_function(sys_rt_sigsuspend);
     static_unused int
     _sys_rt_sigsuspend(struct pt_regs regs)
     {
           sigset_t *unewset, saveset, newset;
                size_t sigsetsize;
  這裏用save_static_function定義了sys_rt_sigsuspend,而實際上如果
  你調用sys_rt_sigsuspend的話,它保存完s0-s8後,接著就調用_sys_rt_sigsuspend!
  看它鏈結後的反彙編片段:
    80108cc8 :
    80108cc8:        afb00058       sw    $s0,88($sp)
    80108ccc:        afb1005c      sw    $s1,92($sp)
    80108cd0:        afb20060       sw    $s2,96($sp)
    80108cd4:        afb30064       sw    $s3,100($sp)
    80108cd8:        afb40068       sw    $s4,104($sp)
    80108cdc:        afb5006c       sw   $s5,108($sp)
    80108ce0:        afb60070       sw    $s6,112($sp)
    80108ce4:        afb70074       sw    $s7,116($sp)
    80108ce8:        afbe0090       sw   $s8,144($sp)
    80108cec <_sys_rt_sigsuspend>:
    80108cec:        27bdffc8      addiu $sp,$sp,-56
    80108cf0:        8fa80064      lw    $t0,100($sp)
    80108cf4:        24030010       li  $v1,16
    80108cf8:        afbf0034      sw    $ra,52($sp)
    80108cfc:       afb00030       sw    $s0,48($sp) ---> notice
   80108d00:         afa40038      sw    $a0,56($sp)
   80108d04:         afa5003c      sw    $a1,60($sp)
   80108d08:         afa60040      sw    $a2,64($sp)
   ...
 用到save_static_function的地方共有4處:
       signal.c:save_static_function(sys_sigsuspend);
       signal.c:save_static_function(sys_rt_sigsuspend);
       syscall.c:save_static_function(sys_fork);
       syscall.c:save_static_function(sys_clone);
 我們知道s0-s8如果在子過程用到,編譯器本來就會保存/恢復它的(如上面的s0),
 那為何要搞這個花招呢?筆者分析之後得出如下結論:
(警告:以下某些內容是筆者的推測,可能不完全正確)
 先看看syscall的處理,syscall也是mips的一種異常,異常號為8.上次我們說
 了一般異常是如何工作的,但在handle_sys並非用BUILD_HANDLER生成,而是在
 scall_o23.S中定義,因為它又有其特殊之處.
       1.缺省情況它只用了SAVE_SOME,並沒有保存at,t*,s*等寄存器,因為syscall
       是由應用程式調用的,不象中斷,任何時候都可以發生,所以一般編譯器就可以
       保證不會丟資料了(at,t*的值應該已經無效,s*的值會被函數保存恢復).
        這樣可以提高系統調用的效率
       2.它還得和用戶空間打交道(取參數,送資料)
 還有個別系統調用需要在特定的時候手工保存s*寄存器,如上面的幾個.為什麼呢?
 對sigsuspend來說,它將使進程在內核中睡眠等待信號到來,信號來了之後將直接
 先回到進程的信號處理代碼,而信號處理代碼可能希望看到當前進程的寄存器
 (sigcontext),這是通過內核棧中的pt_regs結構獲得的,所以內核必需把s*寄存器
 保存到pt_regs中.對於fork的情況,則似乎是為了滿足vfork的要求.(vfork時,子進程
 不拷貝頁表(即和父進程完全共用記憶體),注意,連copy-on-write都沒有!父進程掛起
 一直到子進程不再使用它的資源(exec或者exit)).fork 系統調用使用ret_from_fork
 返回,其中調用到了RESTORE_ALL_AND_RET(entry.S),需要恢復s*.
 這裏還有一個很容易混亂的地方: 在scall_o32.S和entry.S中有幾個函數(彙編)是同名
 的,如restore_all,sig_return等.總體來說scall_o32.S中是對滿足o32(old 32bit)彙編
 約定的系統調用處理,可以避免保存s*,而entry.S中是通用的,保存/恢復所由寄存器
 scall_o32.S中也有一些情況需要保存靜態寄存器s*,此時它就會到ret_from_syscall
而不是本檔中的o32_ret_from_syscall返回了,兩者的差別就是恢復的寄存器數目
不同.scall_o32.S中一些錯誤處理直接用ret_from_syscall返回,筆者懷疑會導致s*寄存器
被破壞,有機會請各路高手指教.
好了,說了一通系統調用,無非是想讓大家明白內核中寄存器的保存恢復過程,以及
為了少做些無用功所做的努力.下面看為什麼要save_static_function:為了避免
s0寄存器的破壞.
  如果我們使用
    sys_rt_sigsuspend()
    { ..
      save_static;
      ...
    }
 會有什麼問題呢,請看,
 Nasty degree - 3 days of tracking.
 The symptom was pthread cannot be created. In the end the caller will
 get a BUS error.
 What exactly happened has to do with how registers are saved. Below
 attached is the beginning part of sys_sigsuspend() function. It is easy
 to see that s0 is saved into stack frame AFTER its modified. Next time
 when process returns to userland, the s0 reg will be wrong!
 So the bug is either
 1) that we need to save s0 register in SAVE_SOME and not save it in
 save_static; or that
 2) we fix compiler so that it does not use s0 register in that case (it
       does the same thing for sys_rt_sigsuspend)
 I am sure Ralf will have something to say about it. :-) In any case, I
 attached a patch for 1) fix.
sys_sigsuspend(struct pt_regs regs)
{
     8008e280: 27bdffc0         addiu $sp,$sp,-64
     8008e284: afb00030          sw     $s0,48($sp)
            sigset_t *uset, saveset, newset;
             save_static(®s);
     8008e288: 27b00040           addiu $s0,$sp,64 /* save_static時
                                       s0已經破壞*/
     8008e28c: afbf003c         sw     $ra,60($sp)
     8008e290:    afb20038       sw     $s2,56($sp)
     8008e294:    afb10034       sw     $s1,52($sp)
     8008e298:    afa40040       sw     $a0,64($sp)
     8008e29c:    afa50044       sw     $a1,68($sp)
     8008e2a0:    afa60048       sw     $a2,72($sp)
     8008e2a4:    afa7004c      sw     $a3,76($sp)
     8008e2a8:    ae100058       sw     $s0,88($s0)
     8008e2ac:    ae11005c       sw     $s1,92($s0)
#ifdef CONFIG_SMP
# define GET_SAVED_SP                              \
          mfc0 k0, CP0_CONTEXT;                    \
          lui k1, %hi(kernelsp);              \
          srl k0, k0, 23;                 \
         sll k0, k0, 2;                 \
          addu k1, k0;                     \
          lw     k1, %lo(kernelsp)(k1);
#else
# define GET_SAVED_SP                              \
/*實際上就是k1 = kernelsp, kernelsp保存當前進程的內核棧指標 */
         lui   k1, %hi(kernelsp);            \
         lw     k1, %lo(kernelsp)(k1);
#endif
/*判斷當前運行態,設置棧頂sp
 保存寄存器--參數a0-a3:4-7,返回值v0-v1:2-3,25,28,31以及一些控制寄存器,
 */
#define SAVE_SOME                                  \
         .set push;                  \
         .set reorder;                \
         mfc0 k0, CP0_STATUS;                     \
         sll k0, 3; /* extract cu0 bit */ \
         .set noreorder;                \
         bltz k0, 8f;                \
          move k1, sp;                  \
         .set reorder;                \
         /* Called from user mode, new stack. */ \
          GET_SAVED_SP                          \
8:                                 \
         move k0, sp;                   \
         subu sp, k1, PT_SIZE;                \
         sw     k0, PT_R29(sp);             \
          sw $3, PT_R3(sp);                \
         sw     $0, PT_R0(sp);            \
         mfc0 v1, CP0_STATUS;                     \
         sw     $2, PT_R2(sp);             \
         sw     v1, PT_STATUS(sp);               \
         sw     $4, PT_R4(sp);             \
         mfc0 v1, CP0_CAUSE;                     \
         sw     $5, PT_R5(sp);             \
         sw     v1, PT_CAUSE(sp);               \
         sw     $6, PT_R6(sp);             \
         mfc0 v1, CP0_EPC;                    \
         sw     $7, PT_R7(sp);             \
         sw     v1, PT_EPC(sp);              \
         sw     $25, PT_R25(sp);             \
         sw     $28, PT_R28(sp);             \
         sw     $31, PT_R31(sp);             \
         ori $28, sp, 0x1fff;        \
         xori $28, 0x1fff;          \
         .set pop
#define SAVE_ALL                            \
         SAVE_SOME;                       \
         SAVE_AT;                     \
         SAVE_TEMP;                       \
         SAVE_STATIC
#define RESTORE_AT                            \
         .set push;             \
         .set noat;            \
         lw   $1, PT_R1(sp);            \
         .set pop;
#define RESTORE_TEMP                            \
         lw   $24, PT_LO(sp);             \
         lw   $8, PT_R8(sp);            \
         lw   $9, PT_R9(sp);            \
         mtlo $24;                \
         lw   $24, PT_HI(sp);           \
         lw   $10,PT_R10(sp);             \
         lw   $11, PT_R11(sp);            \
         mthi $24;                \
         lw   $12, PT_R12(sp);            \
         lw   $13, PT_R13(sp);            \
         lw   $14, PT_R14(sp);            \
         lw   $15, PT_R15(sp);            \
         lw   $24, PT_R24(sp)
#define RESTORE_STATIC                            \
         lw   $16, PT_R16(sp);            \
         lw   $17, PT_R17(sp);            \
         lw   $18, PT_R18(sp);            \
         lw   $19, PT_R19(sp);            \
          lw    $20, PT_R20(sp);           \
          lw    $21, PT_R21(sp);           \
          lw    $22,
沒有上一則|日誌首頁|沒有下一則
回應





Powered by Xuite
    沒有新回應!
關鍵字