为linux kernel调试增加printf

在Linux内核调试的时候,最开始因为设备驱动没有初始化,串口也不能正常的访问,而内核好像也不能通过一般的Jlink调试,这个具体原因还不清楚,只是现象上看断点停掉之后就不会继续往下运行(好像和之前的一个bug有点类似呀),先不管这些,总之我们需要有一个可以观察内核运行情况的东西,嵌入式中最为常用的当然就是printf了,虽然耗时,但是简单易用,下面就给出一个即使设备驱动没有初始化也能开始打印的方法。

内核刚启动时候运行汇编代码的时候

在内核启动的初期,代码都是汇编代码,这时候用printf好像有点麻烦,但是内核代码其实也集成了一些调试代码,稍加修改就可以实现串口的打印,比如在head-common.S中有这么一个汇编函数的定义:

    arch/arm/kernel/head-common.S
    192 __error_p:
    193 #ifdef CONFIG_DEBUG_LL
    194     adr r0, str_p1
    195     bl  printascii
    196     mov r0, r9
    197     bl  printhex8
    198     adr r0, str_p2
    199     bl  printascii
    200     b   __error
    201 str_p1: .asciz  "\nError: unrecognized/unsupported processor variant (0x"
    202 str_p2: .asciz  ").\n"
    203     .align
    204 #endif
    205 ENDPROC(__error_p)

这个实际就调用了一个打印的代码,这个里面打印了一个hex8字符,还有一个字符串,我们来看看printascii是怎么实现的,这个函数是在debug.S中定义的

    arch/arm/kernel/debug.S
     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         ret lr
     94 ENDPROC(printascii)

这个函数里面调用了waituart, senduart, busyuart几个函数,这几个函数是平台相关的,再继续找一下我们看到,他们的定义是在arch/arm/include/debug/stm32.S中实现的,

    arch/arm/include/debug/stm32.S
     12 #if defined(CONFIG_ARCH_STM32F7)
     13 # define STM32_USART_SR     (0x1C)      /* Interrupt&Status Register */
     14 # define STM32_USART_DR     (0x28)      /* Transmit Data Register */
     15 #else
     16 # define STM32_USART_SR     (0x00)      /* Status Register */
     17 # define STM32_USART_DR     (0x04)      /* Data Register */
     18 #endif
     19
     20 #define STM32_USART_SR_TXE  (1 << 7)    /* Transmitter Empty */
     21
     22     .macro  addruart, rp, rv, tmp
     23     ldr \rp, =CONFIG_DEBUG_UART_PHYS    @ phys address
     24     ldr \rv, =CONFIG_DEBUG_UART_VIRT    @ virt address
     25     .endm
     26
     27     .macro  senduart,rd,rx
     28     strb    \rd, [\rx, #(STM32_USART_DR)]   @ Write to Data Register
     29     .endm
     30
     31     .macro  waituart,rd,rx
     32 1001:   ldr \rd, [\rx, #(STM32_USART_SR)]   @ Read Status Register
     33     tst \rd, #STM32_USART_SR_TXE    @ TXE = 1 when TDR shifted
     34     beq 1001b               @ branch if TXE = 0
     35     .endm
     36
     37     .macro  busyuart,rd,rx
     38 1001:   ldr \rd, [\rx, #(STM32_USART_SR)]   @ Read Status Register
     39     tst \rd, #STM32_USART_SR_TXE    @ TXE = 0 when TDR has data
     40     beq 1001b               @ branch if TXE = 0
     41     .endm

这里面定义了和uart相关的几个宏,这个其实也比较简单,一个是uart的状态寄存器,用来轮询uart数据是否发送完成,一个是数据寄存器,用来向uart发送数据,比如移植到RT1050之后,我们就可以通过修改寄存器的地址和状态位的位移实现一个简单的串口。
注意,如果需要使用汇编阶段的串口需要定义CONFIG_DEBUG_LL_INCLUDE这个宏,比如:

#define CONFIG_DEBUG_LL_INCLUDE "debug/stm32.S"

这样相应的汇编文件会包含到debug.S中,从而实现调试信息的打印。
当然还有以下两个宏需要定义

    2542 CONFIG_DEBUG_UART_PHYS=0x40184000
    2543 CONFIG_DEBUG_UART_VIRT=0x40184000
    2544 CONFIG_DEBUG_UNCOMPRESS=y
    2545 CONFIG_UNCOMPRESS_INCLUDE="debug/uncompress.h"
    2546 CONFIG_EARLY_PRINTK=y

完成以上修改之后就可以实现汇编阶段的打印了。

C语言阶段的打印

汇编阶段的打印是比较简单的,当程序可以运行c语言代码的时候,就应该祭出我们常用的调试利器printf了,在源代码中我们发现内核也贴心的实现了相应的函数,不过里面的Printf并不是把信息打印到串口上,而是保存在一个char buffer里面,当串口初始化完成之后再一股脑打印出来。这个对我们的调试毫无用处呀,毕竟很多错误都是在串口初始化之前出现的,难道要盲调?当然不是,我们可以将这个printf稍微修改一下,加点代码就可以实现数据到串口的打印,我们首先看看printf的具体实现。

    kernel/printk/printk.c
    1974 asmlinkage __visible int printk(const char *fmt, ...)
    1975 {
    1976     va_list args;
    1977     int r;
    1978
    1979     va_start(args, fmt);
    1980     r = vprintk_func(fmt, args);
    1981     va_end(args);
    1982
    1983     return r;
    1984 }
    1985 EXPORT_SYMBOL(printk);

以上是printk的定义,原谅我没法继续往下追,因为vprintk_func = this_cpu_read(printk_func);这个一大堆的宏定义,看起来还和线程相关的,我们的目的是在初始化的时候调用这个函数,和线程没有一毛钱关系呀,看来此路不通。
偶然间想到还有一个early_printf,我们来看看这个东西是怎么一回事

    1980 #ifdef CONFIG_EARLY_PRINTK
    1981 struct console *early_console;
    1982
    1983 asmlinkage __visible void early_printk(const char *fmt, ...)
    1984 {
    1985     va_list ap;
    1986     char buf[512];
    1987     int n;
    1988
    1989     if (!early_console)
    1990         return;
    1991
    1992     va_start(ap, fmt);
    1993     n = vscnprintf(buf, sizeof(buf), fmt, ap);
    1994     va_end(ap);
    1995
    2010     early_console->write(early_console, buf, n);
    2011 }
    2012 #endif

这个就是early_printk的实现,要使用这个函数需要定义CONFIG_EARLY_PRINTK这个宏,这个的可改造性比较强,因为里面定义了char buf,并且对各个变量进行了处理,就是这个函数在哪里调用的我还不是特别清楚。另外early_console在哪里定义的也要研究一下。
不过n = vscnprintf(buf, sizeof(buf), fmt, ap);这个我们可以利用一下,把他用来解析printk中的格式字符串,然后将结果依次通过串口发送出去,为了简单起见,我直接用固定地址访问串口的寄存器,毕竟调试完了就可以删掉了呀。

    1895 asmlinkage __visible int printk(const char *fmt, ...)
    1896 {
    1897     printk_func_t vprintk_func;
    1898     va_list args;
    1899     int r;
    1900     int n;
    1901     char buf[512];
    1902
    1903     va_start(args, fmt);
    1904
    1905     /*
    1906      * If a caller overrides the per_cpu printk_func, then it needs
    1907      * to disable preemption when calling printk(). Otherwise
    1908      * the printk_func should be set to the default. No need to
    1909      * disable preemption here.
    1910      */
    1911     vprintk_func = this_cpu_read(printk_func);
    1912     r = vprintk_func(fmt, args);
    1913     n = vscnprintf(buf, sizeof(buf), fmt, args);
    1914     va_end(args);
    1915
    1916     ////////// DEBUG by Major ////////////////
    1917     int *uart_data;
    1918     int *uart_status;
    1919     int temp;
    1920     int i;
    1921     uart_data = (int*)0x4018401c;
    1922     uart_status = (int*)0x40184014;
    1923     for(i = 0; i< n; i++){
    1924         while(!((*uart_status) & (1<<23)));
    1925         temp = (*uart_data)&0xffffff00;
    1926         *uart_data = (int)(temp|(buf[i]));
    1927     }
    1928     while(!((*uart_status) & (1<<23)));
    1929     temp = (*uart_data)&0xffffff00;
    1930     *uart_data = (int)(temp|'\r');
    1931     ////////// DEBUG by Major ////////////////
    1932
    1933
    1934     return r;
    1935 }
    1936 EXPORT_SYMBOL(printk);

以上就是改造之后的函数,经过改造之后就可以在串口上未初始化的时候打印一些必要的调试信息,并且支持格式化字符串的打印。


本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可。

发表新评论