为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);

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,976评论 6 513
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,249评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 167,449评论 0 360
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,433评论 1 296
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,460评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,132评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,721评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,641评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,180评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,267评论 3 339
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,408评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 36,076评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,767评论 3 332
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,255评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,386评论 1 271
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,764评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,413评论 2 358

推荐阅读更多精彩内容