Linux最小系統移植之早期打印CONFIG_DEBUG_LL
一、幾個關鍵宏定義
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 #endiflinux-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 #endifdebug.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