1. 程式人生 > >Linux最小系統移植之早期打印CONFIG_DEBUG_LL

Linux最小系統移植之早期打印CONFIG_DEBUG_LL

ace makefile add ssa ptr 終端 同時 虛擬 固件

一、幾個關鍵宏定義

  CONFIG_DEBUG_LL、 CONFIG_DEBUG_LL_INCLUDE

  容我慢慢道來, 首先要使能早期打印, menuconfig必須選中CONFIG_DEBUG_LL, 我們再慢慢梳理其他所以宏及代碼

/* linux-3.10.65/arch/arm/kernel/Makefile */
obj-$(CONFIG_DEBUG_LL)    += debug.o
obj-$(CONFIG_EARLY_PRINTK)    += early_printk.o

技術分享圖片

  我們選中“Kernel low-level debugging functions (read help!)” 在linux-3.10.65/arch/arm/Kconfig.debug 中就是DEBUG_LL

技術分享圖片

  在這個選項中發現還有個依賴的子菜單“Kernel low-level debugging port”, 裏面有一堆宏定義如AT91_DEBUG_LL_DBGU0、AT91_DEBUG_LL_DBGU1、DEBUG_BCM2835、DEBUG_ICEDCC,

實際上前面3個又依賴其他宏定義所以在menuconfig中只看到幾個子選項如下:

技術分享圖片

  這幾個子選項用來幹嘛呢? 一是代碼文件debug.S(obj-$(CONFIG_DEBUG_LL) += debug.o) 會根據子宏定義走不同的分支; 二是這個代碼裏會引用宏CONFIG_DEBUG_LL_INCLUDE,

這個宏在“linux-3.10.65/arch/arm/Kconfig.debug”中定義如下:

技術分享圖片

  看到沒? 子選項定義的DEBUG_BCM2835 在DEBUG_LL_INCLUDE中被依賴了, 也就是我們移植一個新平臺, 需要在子選項定義新的宏, 然後在這添加依賴這個新宏對應的文件, 我們現在就這麽做:

/* linux-3.10.65/arch/arm/Kconfig.debug */
choice
    prompt "Kernel low-level debugging port"
    depends on DEBUG_LL

+    config VEDIC_DEBUG_LL
+ bool "I just add a test macro" config AT91_DEBUG_LL_DBGU0 bool "Kernel low-level debugging on rm9200, 9260/9g20, 9261/9g10 and 9rl" depends on HAVE_AT91_DBGU0 config DEBUG_LL_INCLUDE string + default "debug/vedic.S" if VEDIC_DEBUG_LL default "debug/bcm2835.S" if DEBUG_BCM2835 default "debug/cns3xxx.S" if DEBUG_CNS3XXX

接下來我們在menuconfig選中VEDIC_DEBUG_LL宏和添加新文件vedic.S, 如下:

技術分享圖片

/* linux-3.10.65/.config */
+CONFIG_DEBUG_LL=y
+CONFIG_VEDIC_DEBUG_LL=y
+CONFIG_DEBUG_LL_INCLUDE="debug/vedic.S"

  那這個vedic.S該怎麽寫呢? 我們看同目錄下(linux-3.10.65/arch//arm/include/debug/)bcm2835.S文件寫了什麽:

/*
 * Debugging macro include header
 *
 * Copyright (C) 2010 Broadcom
 * Copyright (C) 1994-1999 Russell King
 * Moved from linux/arch/arm/kernel/debug.S by Ben Dooks
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 2 as
 * published by the Free Software Foundation.
 *
 */

#define BCM2835_DEBUG_PHYS 0x20201000
#define BCM2835_DEBUG_VIRT 0xf0201000

    .macro    addruart, rp, rv, tmp
    ldr    \rp, =BCM2835_DEBUG_PHYS
    ldr    \rv, =BCM2835_DEBUG_VIRT
    .endm

#include <asm/hardware/debug-pl01x.S>

  沒錯, 就是提供一個函數而已 -> addruart(rp, rv, tmp) , 其實就是返回參數 -- 串口物理地址和串口虛擬地址, 為什麽要提供虛擬地址呢? 因為在kernel C語言的第一個入口start_kernel()時, 匯編期間已經開啟了MMU, CPU取的都是

虛擬地址; 該函數只是返回地址而已, 如果是開啟MMU,返回虛擬地址還不夠, 還要事前構建好頁表, 不然根據虛擬地址也找不到物理地址。 至於具體在哪裏構建頁表, 待會說, 根據我目前的硬件平臺, 提供文件代碼如下:

/* linux-3.10.65/arch/arm/include/debug/vedic.S 
 * phy_addr is fixed of hardware, but virt_addr? Why 0xF5368000
 */

#define VEDIC_DEBUG_PHYS 0x70400000
#define VEDIC_DEBUG_VIRT 0xF5368000

    .macro    addruart, rp, rv, tmp
    ldr    \rp, =VEDIC_DEBUG_PHYS
    ldr    \rv, =VEDIC_DEBUG_VIRT
    .endm

 二、源碼分析

  為方便待會分批解釋功能我先貼出debug.S源碼:

技術分享圖片
  1 /*
  2  *  linux/arch/arm/kernel/debug.S
  3  *
  4  *  Copyright (C) 1994-1999 Russell King
  5  *
  6  * This program is free software; you can redistribute it and/or modify
  7  * it under the terms of the GNU General Public License version 2 as
  8  * published by the Free Software Foundation.
  9  *
 10  *  32-bit debugging code
 11  */
 12 #include <linux/linkage.h>
 13 #include <asm/assembler.h>
 14 
 15         .text
 16 
 17 /*
 18  * Some debugging routines (useful if you‘ve got MM problems and
 19  * printk isn‘t working).  For DEBUGGING ONLY!!!  Do not leave
 20  * references to these in a production kernel!
 21  */
 22 
 23 #if !defined(CONFIG_DEBUG_SEMIHOSTING)
 24 #include CONFIG_DEBUG_LL_INCLUDE
 25 #endif
 26 
 27 #ifdef CONFIG_MMU
 28         .macro    addruart_current, rx, tmp1, tmp2
 29         addruart    \tmp1, \tmp2, \rx
 30         mrc        p15, 0, \rx, c1, c0
 31         tst        \rx, #1
 32         moveq        \rx, \tmp1
 33         movne        \rx, \tmp2
 34         .endm
 35 
 36 #else /* !CONFIG_MMU */
 37         .macro    addruart_current, rx, tmp1, tmp2
 38         addruart    \rx, \tmp1
 39         .endm
 40 
 41 #endif /* CONFIG_MMU */
 42 
 43 /*
 44  * Useful debugging routines
 45  */
 46 ENTRY(printhex8)
 47         mov    r1, #8
 48         b    printhex
 49 ENDPROC(printhex8)
 50 
 51 ENTRY(printhex4)
 52         mov    r1, #4
 53         b    printhex
 54 ENDPROC(printhex4)
 55 
 56 ENTRY(printhex2)
 57         mov    r1, #2
 58 printhex:    adr    r2, hexbuf
 59         add    r3, r2, r1
 60         mov    r1, #0
 61         strb    r1, [r3]
 62 1:        and    r1, r0, #15
 63         mov    r0, r0, lsr #4
 64         cmp    r1, #10
 65         addlt    r1, r1, #0
 66         addge    r1, r1, #a - 10
 67         strb    r1, [r3, #-1]!
 68         teq    r3, r2
 69         bne    1b
 70         mov    r0, r2
 71         b    printascii
 72 ENDPROC(printhex2)
 73 
 74 hexbuf:        .space 16
 75 
 76         .ltorg
 77 
 78 #ifndef CONFIG_DEBUG_SEMIHOSTING
 79 
 80 ENTRY(printascii)
 81         addruart_current r3, r1, r2
 82         b    2f
 83 1:        waituart r2, r3
 84         senduart r1, r3
 85         busyuart r2, r3
 86         teq    r1, #\n
 87         moveq    r1, #\r
 88         beq    1b
 89 2:        teq    r0, #0
 90         ldrneb    r1, [r0], #1
 91         teqne    r1, #0
 92         bne    1b
 93         mov    pc, lr
 94 ENDPROC(printascii)
 95 
 96 ENTRY(printch)
 97         addruart_current r3, r1, r2
 98         mov    r1, r0
 99         mov    r0, #0
100         b    1b
101 ENDPROC(printch)
102 
103 #ifdef CONFIG_MMU
104 ENTRY(debug_ll_addr)
105         addruart r2, r3, ip
106         str    r2, [r0]
107         str    r3, [r1]
108         mov    pc, lr
109 ENDPROC(debug_ll_addr)
110 #endif
111 
112 #else
113 
114 ENTRY(printascii)
115         mov    r1, r0
116         mov    r0, #0x04        @ SYS_WRITE0
117     ARM(    svc    #0x123456    )
118     THUMB(    svc    #0xab        )
119         mov    pc, lr
120 ENDPROC(printascii)
121 
122 ENTRY(printch)
123         adr    r1, hexbuf
124         strb    r0, [r1]
125         mov    r0, #0x03        @ SYS_WRITEC
126     ARM(    svc    #0x123456    )
127     THUMB(    svc    #0xab        )
128         mov    pc, lr
129 ENDPROC(printch)
130 
131 ENTRY(debug_ll_addr)
132         mov    r2, #0
133         str    r2, [r0]
134         str    r2, [r1]
135         mov    pc, lr
136 ENDPROC(debug_ll_addr)
137 
138 #endif
linux-3.10.65/arch/arm/kernel/debug.S
具體分析如下:
1
. #if !defined(CONFIG_DEBUG_SEMIHOSTING) #include CONFIG_DEBUG_LL_INCLUDE #endif ---> 因為沒有定義CONFIG_DEBUG_SEMIHOSTING, 所以包含了CONFIG_DEBUG_LL_INCLUDE=linux-3.10.65/arch/arm/include/debug/vedic.S, 也即提供addruart()功能 2. #ifdef CONFIG_MMU .macro addruart_current, rx, tmp1, tmp2 addruart \tmp1, \tmp2, \rx mrc p15, 0, \rx, c1, c0 tst \rx, #1 moveq \rx, \tmp1 movne \rx, \tmp2 .endm #else /* !CONFIG_MMU */ .macro addruart_current, rx, tmp1, tmp2 addruart \rx, \tmp1 .endm #endif /* CONFIG_MMU */ ---> 註意參數的位置!addruart()第一個參數是返回物理地址, 第二個是返回虛擬地址, 第三個不用 addruart_current(rx, tmp1, tmp2) rx是返回串口地址,至於是物理地址還是虛擬地址做了判斷, 如果沒有開MMU, 當然rx是物理地址,所以直接 將rx作為addruart()第一個參數, 如果使能MMU可以直接返回虛擬地址, 但開發者可能為保險起見, 判斷p15協處理器硬件是否真的開啟MMU, 如果開啟就返回虛擬地址rx=tmp2, 不然返回物理地址rx=tmp1 3. ENTRY(printascii) addruart_current r3, r1, r2 b 2f /* 先跳轉到2,把要打印的值放置r1 */ 1: waituart r2, r3 senduart r1, r3 busyuart r2, r3 teq r1, #\n moveq r1, #\r beq 1b 2: teq r0, #0 ldrneb r1, [r0], #1 /* r0存放的是要打印字符內存地址, 該指令是加載r0地址上的值,取一個byte到r1, 同時r0偏移1個byte */ teqne r1, #0 bne 1b mov pc, lr ENDPROC(printascii) ENTRY(printch) addruart_current r3, r1, r2 mov r1, r0 mov r0, #0 b 1b ENDPROC(printch) ---> 這段匯編提供兩個函數功能, 打印字符串printascii(), 和打印字符printch(), 這裏需要再提供waituart() senduart() busyuart() 所以我們要在vedic.S提供這三個函數 4. #ifdef CONFIG_MMU ENTRY(debug_ll_addr) addruart r2, r3, ip str r2, [r0] str r3, [r1] mov pc, lr ENDPROC(debug_ll_addr) #endif ---> 這個函數其實就是將串口物理地址賦值給參數0, 虛擬地址賦值給參數1, 其C定義的含義為: #ifdef CONFIG_DEBUG_LL extern void debug_ll_addr(unsigned long *paddr, unsigned long *vaddr); extern void debug_ll_io_init(void); #else static inline void debug_ll_io_init(void) {} #endif 可以理解debug_ll_addr()等效於addruart(), 那誰用這個功能呢? 在linux-3.10.65/arch/arm/mmu.c: #ifdef CONFIG_DEBUG_LL void __init debug_ll_io_init(void) { struct map_desc map; debug_ll_addr(&map.pfn, &map.virtual); if (!map.pfn || !map.virtual) return; map.pfn = __phys_to_pfn(map.pfn); map.virtual &= PAGE_MASK; map.length = PAGE_SIZE; map.type = MT_DEVICE; create_mapping(&map, false); } #endif 沒錯, debug_ll_io_init()就是構建串口虛擬地址和物理地址頁表, 一般放在 machine_desc.map_io: static void __init zynq_map_io(void) { debug_ll_io_init(); zynq_scu_map_io(); } MACHINE_START(XILINX_EP107, "Xilinx Zynq Platform") .smp = smp_ops(zynq_smp_ops), .map_io = zynq_map_io, .init_irq = irqchip_init, .init_machine = zynq_init_machine, .init_time = zynq_timer_init, .dt_compat = zynq_dt_match, .restart = zynq_system_reset, MACHINE_END 這個在: start_kernel() -> setup_arch() -> paging_init() -> devicemaps_init() ->mdesc->map_io() 從這裏可以看出, 要使用printascii(), printch()功能必須在setup_arch()執行之後,因為之前頁表都沒有建立訪問虛擬地址壓根找不到物理地址 很明顯跑到start_kernel()時已經開啟MMU了,我就想在start_kernel()時就立馬打印log出來, 當操作虛擬地址時由於頁表沒建立串口的映射導致系統Oops 這是非常不好的用戶體驗!串口雖然是device, 應該歸屬devicemaps_init(), 但由於它的特殊性, 我們希望這塊映射越早越好, 因此稍微新的內核版本 都把這塊映射放置在匯編階段: linux-3.10.65/arch/arm/kernel/head.s __create_page_tables: #ifdef CONFIG_DEBUG_LL #if !defined(CONFIG_DEBUG_ICEDCC) && !defined(CONFIG_DEBUG_SEMIHOSTING) /* * Map in IO space for serial debugging. * This allows debug messages to be output * via a serial console before paging_init. */ addruart r7, r3, r0 mov r3, r3, lsr #SECTION_SHIFT mov r3, r3, lsl #PMD_ORDER add r0, r4, r3 mov r3, r7, lsr #SECTION_SHIFT ldr r7, [r10, #PROCINFO_IO_MMUFLAGS] @ io_mmuflags orr r3, r7, r3, lsl #SECTION_SHIFT #ifdef CONFIG_ARM_LPAE mov r7, #1 << (54 - 32) @ XN #ifdef CONFIG_CPU_ENDIAN_BE8 str r7, [r0], #4 str r3, [r0], #4 #else str r3, [r0], #4 str r7, [r0], #4 #endif #else orr r3, r3, #PMD_SECT_XN str r3, [r0], #4 #endif #else /* CONFIG_DEBUG_ICEDCC || CONFIG_DEBUG_SEMIHOSTING */ /* we don‘t need any serial debugging mappings */ ldr r7, [r10, #PROCINFO_IO_MMUFLAGS] @ io_mmuflags #endif 其他平臺串口需要特殊映射的..... #endif Now,匯編階段映射好頁表, 然後跳轉到start_kernel()就可以立馬使用printascii(), 同時mdesc->map_io也不需要調用debug_ll_io_init()
技術分享圖片
/* linux-3.10.65/arch/arm/include/debug/vedic.S 
 * phy_addr is uart base addr which fixed by hardware, but virt_addr? Why 0xF5368000
 */

#define VEDIC_DEBUG_PHYS 0x70400000
#define VEDIC_DEBUG_VIRT 0xF5368000

    .macro    addruart, rp, rv, tmp
    ldr    \rp, =VEDIC_DEBUG_PHYS
    ldr    \rv, =VEDIC_DEBUG_VIRT
    .endm

    .macro    senduart,rd,rx
    and    \rd,\rd,#0xFF
    str    \rd, [\rx, #0x00]    /* tx_reg is offset 0x00 */
    .endm

    .macro    waituart,rd,rx
1:    ldr    \rd, [\rx, #0x0C]    /* fifo_reg is offset 0x0C */
    mov    \rd,\rd,lsr #8
    and    \rd,\rd,#0x7F
    teq    \rd, #0x00
    bne    1b
    .endm

    .macro    busyuart,rd,rx
2:    ldr    \rd, [\rx, #0x0C]
    mov    \rd,\rd,lsr #8
    and    \rd,\rd,#0x7F
    teq    \rd, #0x00
    bne    2b
    .endm
完整的vedic.S源碼

三、測試驗證

/* linux-3.10.65/include/generated/autoconf.h */
#define CONFIG_DEBUG_LL_INCLUDE "debug/vedic.S"

技術分享圖片

技術分享圖片

四、壓縮鏡像 zImage 使能打印

  上面的調試都是解壓後Image的log, 如果固件是壓縮zImage呢? 如果想打印log? 我們先看一下log所在文件:

/* linux-3.10.65/arch/arm/boot/compressed/misc.c */


static void putstr(const char *ptr)
{
    char c;

    while ((c = *ptr++) != \0) {
        if (c == \n)
            putc(\r);
        putc(c);
    }

    flush();
}


void decompress_kernel(unsigned long output_start, unsigned long free_mem_ptr_p,
        unsigned long free_mem_ptr_end_p,
        int arch_id)
{
    int ret;

    output_data        = (unsigned char *)output_start;
    free_mem_ptr        = free_mem_ptr_p;
    free_mem_end_ptr    = free_mem_ptr_end_p;
    __machine_arch_type    = arch_id;

    arch_decomp_setup();

    putstr("Uncompressing Linux...");
    ret = do_decompress(input_data, input_data_end - input_data,
                output_data, error);
    if (ret)
        error("decompressor returned an error");
    else
        putstr(" done, booting the kernel.\n");
}

  看樣子最終靠putc()實現, misc.c開頭包含一個宏“CONFIG_UNCOMPRESS_INCLUDE”, 定義在:

/* linux-3.10.65/arch/arm/Kconfig.debug */

/* Here I delete ARCH_MULTIPLATFORM since my menuconfig does not declare
config DEBUG_UNCOMPRESS
    bool
    default y if ARCH_MULTIPLATFORM && DEBUG_LL &&              !DEBUG_OMAP2PLUS_UART &&              !DEBUG_TEGRA_UART

config UNCOMPRESS_INCLUDE
    string
    default "debug/uncompress.h" if ARCH_MULTIPLATFORM
    default "mach/uncompress.h"
*/

config DEBUG_UNCOMPRESS 
    bool
    default y if DEBUG_LL &&              !DEBUG_OMAP2PLUS_UART &&              !DEBUG_TEGRA_UART

config UNCOMPRESS_INCLUDE 
    string
    default "debug/uncompress.h"

所以:
CONFIG_DEBUG_UNCOMPRESS=y
CONFIG_UNCOMPRESS_INCLUDE=linux-3.10.65/arch/arm/include/debug/uncompress.h
技術分享圖片
#ifdef CONFIG_DEBUG_UNCOMPRESS
extern void putc(int c);
#else
static inline void putc(int c) {}
#endif
static inline void flush(void) {}
static inline void arch_decomp_setup(void) {}
uncompress.h源碼

/* linux-3.10.65/arch/arm/boot/compressed/Makefile */
ifeq ($(CONFIG_DEBUG_UNCOMPRESS),y)
OBJS    += debug.o


而linux-3.10.65/arch/arm/boot/compressed/debug.S:
技術分享圖片
#include <linux/linkage.h>
#include <asm/assembler.h>

#ifndef CONFIG_DEBUG_SEMIHOSTING

#include CONFIG_DEBUG_LL_INCLUDE

ENTRY(putc)
    addruart r1, r2, r3
    waituart r3, r1
    senduart r0, r1
    busyuart r3, r1
    mov     pc, lr
ENDPROC(putc)

#else

ENTRY(putc)
    adr    r1, 1f
    ldmia    r1, {r2, r3}
    add    r2, r2, r1
    ldr    r1, [r2, r3]
    strb    r0, [r1]
    mov    r0, #0x03        @ SYS_WRITEC
   ARM(    svc    #0x123456    )
 THUMB(    svc    #0xab        )
    mov    pc, lr
    .align    2
1:    .word    _GLOBAL_OFFSET_TABLE_ - .
    .word    semi_writec_buf(GOT)
ENDPROC(putc)

    .bss
    .global    semi_writec_buf
    .type   semi_writec_buf, %object
semi_writec_buf:
    .space    4
    .size    semi_writec_buf, 4

#endif
debug.S源碼

  核心思想就是選中DEBUG_UNCOMPRESS, 使其編譯linux-3.10.65/arch/arm/boot/compressed/debug.S, 裏面會提供putc()實現,

然後用戶可以在misc.c刪掉“#include CONFIG_UNCOMPRESS_INCLUDE”, 添加申明代碼“extern void putc(int c);”, 或者選中

UNCOMPRESS_INCLUDE=linux-3.10.65/arch/arm/include/debug/uncompress.h 幫我們申明

  我的開機log如下:

技術分享圖片

五、其他

  說起早期打印不得不提CONFIG_EARLY_PRINTK, 這個可以理解為對printch()進一步的封裝, 同時歸屬於console框架, 具備緩存數據等後期console的所有功能, 而且實測發現用early_printk在串口終端CRT顯示會比直接使用printch()好,

這就是為何會出現CONFIG_EARLY_PRINTK的緣故吧, 不然前期用printascii(), 後期直接用printk就行了

  具體請參考另一篇博文:


Linux最小系統移植之早期打印CONFIG_DEBUG_LL