以下内容主要摘录自《Linux安全体系分析与编程》
1、基本原理
(1)在UBOOT里设置console=ttySAC0或者console=tty1内核就会根据命令行参数来找到对应的硬件操作函数,并将信息通过对应的硬件终端打印出来!
2、printk及控制台的日志级别
函数printk的使用方法和printf相似,用于内核打印消息。printk根据日志级别(loglevel)对消息进行分类。
相似的在android中有使用Log函数进行调试信息的打印。
日志级别用宏定义,日志级别宏展开为一个字符串,在编译时由预处理器将它和消息文本拼接成一个字符串,因此printk 函数中日志级别宏和格式字符串间不能有逗号。
下面是两个printk的例子,一个用于打印调试信息,另一个用于打印临界条件信息。
printk(KERN_DEBUG "Here I am: %s:%i/n", _ _FILE_ _, _ _LINE_ _); printk(KERN_CRIT "I'm trashed; giving up on %p/n", ptr);printk的日志级别定义如下(在linux2.6和3.14/include/linux/kernel.h中):
#defineKERN_EMERG"<0>"/*紧急事件消息,系统崩溃之前提示,表示系统不可用*/ #defineKERN_ALERT"<1>"/*报告消息,表示必须立即采取措施*/ #defineKERN_CRIT"<2>"/*临界条件,通常涉及严重的硬件或软件操作失败*/ #defineKERN_ERR"<3>"/*错误条件,驱动程序常用KERN_ERR来报告硬件的错误*/ #defineKERN_WARNING"<4>"/*警告条件,对可能出现问题的情况进行警告*/ #defineKERN_NOTICE"<5>"/*正常但又重要的条件,用于提醒。常用于与安全相关的消息*/ #defineKERN_INFO"<6>"/*提示信息,如驱动程序启动时,打印硬件信息*/ #defineKERN_DEBUG"<7>"/*调试级别的消息*/ extern int console_printk[]; #define console_loglevel (console_printk[0]) #define default_message_loglevel (console_printk[1]) #define minimum_console_loglevel (console_printk[2]) #define default_console_loglevel (console_printk[3])
/*没有定义日志级别的printk使用下面的默认级别*/ #define DEFAULT_MESSAGE_LOGLEVEL 4 /* KERN_WARNING 警告条件*/
/* 显示比这个级别更重发的消息*/ #define MINIMUM_CONSOLE_LOGLEVEL 1 /*可以使用的最小日志级别*/ #define DEFAULT_CONSOLE_LOGLEVEL 7 /*比KERN_DEBUG 更重要的消息都被打印*/ int console_printk[4] = { DEFAULT_CONSOLE_LOGLEVEL,/*控制台日志级别,优先级高于该值的消息将在控制台显示*/ /*默认消息日志级别,printk没定义优先级时,打印这个优先级以上的消息*/ DEFAULT_MESSAGE_LOGLEVEL, /*最小控制台日志级别,控制台日志级别可被设置的最小值(最高优先级)*/ MINIMUM_CONSOLE_LOGLEVEL, DEFAULT_CONSOLE_LOGLEVEL,/* 默认的控制台日志级别*/ };
#cat /proc/sys/kernel/printk 7 4 1 7
# echo "4 4 1 7" > /proc/sys/kernel/printk这里有个问题需要说一下,在一些开发板提供的内核源码printk的默认打印级别都是未经修改的,例如飞凌的开发板提供的内核,在实际使用时有一些驱动会输出一些调试信息,在实际使用时这些信息会影响正常的用户使用,比如飞凌开发板连接网线后串口终端会定时输出以下内容:
eth0: link down eth0: link up, 100Mbps, full-duplex, lpa 0x4DE1以上信息并不是错误,只是在打印调试信息,说明网络正常连接,但这样会干扰串口输入,所以在编译内核时,修改printk.c中的调试级别如下即可解决这个问题:
int console_printk[4] = { DEFAULT_MESSAGE_LOGLEVEL,/*控制台日志级别,优先级高于该值的消息将在控制台显示*/ /*默认消息日志级别,printk没定义优先级时,打印这个优先级以上的消息*/ DEFAULT_MESSAGE_LOGLEVEL,/*最小控制台日志级别,控制台日志级别可被设置的最小值(最高优先级)*/ MINIMUM_CONSOLE_LOGLEVEL, DEFAULT_CONSOLE_LOGLEVEL,/* 默认的控制台日志级别*/ };
环形缓冲区__log_buf在使用之前就是已定义好的全局变量,缓冲区的长度为1 << CONFIG_LOG_ BUF_SHIFT。变量CONFIG_LOG_BUF_SHIFT在内核编译时由配置文件定义,对于i386平台,其值定义如下(在 linux2.6/arch/i386/defconfig中):
CONFIG_LOG_BUF_SHIFT=18
在内核编译时,编译器根据配置文件的设置,产生如下的宏定义:
#define CONFIG_LOG_BUF_SHIFT 18
环形缓冲区__log_buf定义如下(在linux2.6/kernel/printk.c中,在linux3.14/kernel/printk/printk.c中):
#define __LOG_BUF_LEN(1 << CONFIG_LOG_BUF_SHIFT) //定义环形缓冲区的长度,i386平台为 static char __log_buf[__LOG_BUF_LEN]; //printk的环形缓冲区 static char *log_buf = __log_buf; static int log_buf_len = __LOG_BUF_LEN; /*互斥锁logbuf_lock保护log_buf、log_start、log_end、con_start和logged_chars */ static DEFINE_SPINLOCK(logbuf_lock);
为了指明环形缓冲区__log_buf字符读取位置,定义了下面的位置变量:
static unsigned long log_start;/*系统调用syslog读取的下一个字符*/
static unsigned long con_start;/*送到控制台的下一个字符*/
static unsigned long log_end;/*最近已写字符序号加1 */
static unsigned long logged_chars; /*自从上一次read+clear 操作以来产生的字符数*/
任何地方的内核调用都可以调用函数printk打印调试、安全、提示和错误消息。函数printk尝试得到控制台信号量(console_sem),如果得到,就将信息输出到环形缓冲区__log_buf中,然后函数release_console_sem()在释放信号 量之前把环形缓冲区中的消息送到控制台,调用控制台驱动程序显示打印的信息。如果没得到信号量,就只将信息输出到环形缓冲区后返回。
函数printk列出如下(在linux2.6/kernel/printk.c中,在linux3.14/kernel/printk/printk.c中):
asmlinkage int printk(const char *fmt, ...) { va_list args; int r; va_start(args, fmt); r = vprintk(fmt, args); va_end(args); return r; } asmlinkage int vprintk(const char *fmt, va_list args) { unsigned long flags; int printed_len; char *p; static char printk_buf[1024]; static int log_level_unknown = 1; preempt_disable(); //关闭内核抢占 if (unlikely(oops_in_progress) && printk_cpu == smp_processor_id()) /*如果在printk运行时,这个CPU发生崩溃, 确信不能死锁,10秒1次初始化锁logbuf_lock和console_sem,留时间 给控制台打印完全的oops信息*/ zap_locks(); local_irq_save(flags); //存储本地中断标识 lockdep_off(); spin_lock(&logbuf_lock); printk_cpu = smp_processor_id(); /*将输出信息发送到临时缓冲区printk_buf */ printed_len = vscnprintf(printk_buf, sizeof(printk_buf), fmt, args); /*拷贝printk_buf数据到循环缓冲区,如果调用者没提供合适的日志级别,插入默认值*/ for (p = printk_buf; *p; p++) { if (log_level_unknown) { /* log_level_unknown signals the start of a new line */ if (printk_time) { int loglev_char; char tbuf[50], *tp; unsigned tlen; unsigned long long t; unsigned long nanosec_rem; /*在时间输出之前强制输出日志级别*/ if (p[0] == '<' && p[1] >='0' && p[1] <= '7' && p[2] == '>') { loglev_char = p[1]; //获取日志级别字符 p += 3; printed_len -= 3; } else { loglev_char = default_message_loglevel + '0'; } t = printk_clock();//返回当前时钟,以ns为单位 nanosec_rem = do_div(t, 1000000000); tlen = sprintf(tbuf, "<%c>[%5lu.%06lu] ", loglev_char, (unsigned long)t, nanosec_rem/1000);//写入格式化后的日志级别和时间 for (tp = tbuf; tp < tbuf + tlen; tp++) emit_log_char(*tp); //将日志级别和时间字符输出到循环缓冲区 printed_len += tlen; } else { if (p[0] != '<' || p[1] < '0' || p[1] > '7' || p[2] != '>') { emit_log_char('<'); emit_log_char(default_message_loglevel + '0'); //输出字符到循环缓冲区 emit_log_char('>'); printed_len += 3; } } log_level_unknown = 0; if (!*p) break; } emit_log_char(*p);//将其他printk_buf数据输出到循环缓冲区 if (*p == '/n') log_level_unknown = 1; } if (!down_trylock(&console_sem)) { /*拥有控制台驱动程序,降低spinlock并让release_console_sem()打印字符 */ console_locked = 1; printk_cpu = UINT_MAX; spin_unlock(&logbuf_lock); /*如果CPU准备好,控制台就输出字符。函数cpu_online检测CPU是否在线, 函数have_callable_console()检测是否 有注册的控制台启动时就可以使用*/ if (cpu_online(smp_processor_id()) || have_callable_console()) { console_may_schedule = 0; release_console_sem(); } else { /*释放锁避免刷新缓冲区*/ console_locked = 0; up(&console_sem); } lockdep_on(); local_irq_restore(flags); //恢复本地中断标识 } else { /*如果其他进程拥有这个驱动程序,本线程降低spinlock, 允许信号量持有者运行并调用控制台驱动程序输出字符*/ printk_cpu = UINT_MAX; spin_unlock(&logbuf_lock); lockdep_on(); local_irq_restore(flags); //恢复本地中断标识 } preempt_enable(); //开启抢占机制 return printed_len; }
void release_console_sem(void) { unsigned long flags; unsigned long _con_start, _log_end; unsigned long wake_klogd = 0; for ( ; ; ) { spin_lock_irqsave(&logbuf_lock, flags); wake_klogd |= log_start - log_end; if (con_start == log_end) break;/* 没有需要打印的数据*/ _con_start = con_start; _log_end = log_end; con_start = log_end;/* Flush */ spin_unlock_irqrestore(&logbuf_lock, flags); //调用控制台driver的write函数写入到控制台 call_console_drivers(_con_start, _log_end); } console_locked = 0; console_may_schedule = 0; up(&console_sem); spin_unlock_irqrestore(&logbuf_lock, flags); if (wake_klogd && !oops_in_progress && waitqueue_active(&log_wait)) wake_up_interruptible(&log_wait);//唤醒在等待队列上的进程 }函数_call_console_drivers将缓冲区中从start到end - 1的数据输出到控制台进行显示。在输出数据到控制台之前,它检查消息的日志级别。只有日志级别小于控制台日志级别console_loglevel的消 息,才能交给控制台驱动程序进行显示。
static void _call_console_drivers(unsigned long start, unsigned long end, int msg_log_level) { //日志级别小于控制台日志级别的消息才能输出到控制台 if ((msg_log_level < console_loglevel || ignore_loglevel) && console_drivers && start != end) { if ((start & LOG_BUF_MASK) > (end & LOG_BUF_MASK)) { /* 调用控制台驱动程序的写操作函数 */ __call_console_drivers(start & LOG_BUF_MASK, log_buf_len); __call_console_drivers(0, end & LOG_BUF_MASK); } else { __call_console_drivers(start, end); } } }
static void __call_console_drivers(unsigned long start, unsigned long end) { struct console *con; for (con = console_drivers; con; con = con->next) { if ((con->flags & CON_ENABLED) && con->write && (cpu_online(smp_processor_id()) || (con->flags & CON_ANYTIME))) con->write(con, &LOG_BUF(start), end - start); //调用驱动程序的写操作函数 } }4、sys_syslog系统调用
asmlinkage long sys_syslog(int type, char __user * buf, int len) { return do_syslog(type, buf, len); } int do_syslog(int type, char __user *buf, int len) { unsigned long i, j, limit, count; int do_clear = 0; char c; int error = 0; error = security_syslog(type); //检查是否调用这个函数的权限 if (error) return error; switch (type) { case 0:/* 关闭日志 */ break; case 1:/* 打开日志*/ break; case 2:/*读取日志信息*/ error = -EINVAL; if (!buf || len < 0) goto out; error = 0; if (!len) goto out; if (!access_ok(VERIFY_WRITE, buf, len)) { //验证是否有写的权限 error = -EFAULT; goto out; } //当log_start - log_end为0时,表示环形缓冲区无数据可读,把当前进程放入 等待队列log_wait error = wait_event_interruptible(log_wait, (log_start - log_end)); if (error) goto out; i = 0; spin_lock_irq(&logbuf_lock); while (!error && (log_start != log_end) && i < len) { c = LOG_BUF(log_start); //从环形缓冲区得到读取位置log_start log_start++; spin_unlock_irq(&logbuf_lock); error = __put_user(c,buf); //将c地址的字符传递到用户空间的buf中 buf++; i++; cond_resched(); //条件调度,让其他进程有运行时间 spin_lock_irq(&logbuf_lock); } spin_unlock_irq(&logbuf_lock); if (!error) error = i; break; case 4:/* 读/清除上一次内核消息*/ do_clear = 1; /* FALL THRU */ case 3:/*读取上一次内核消息*/ error = -EINVAL; if (!buf || len < 0) goto out; error = 0; if (!len) //读取长度为0 goto out; if (!access_ok(VERIFY_WRITE, buf, len)) { //验证有写权限 error = -EFAULT; goto out; } count = len; if (count > log_buf_len) count = log_buf_len; spin_lock_irq(&logbuf_lock); if (count > logged_chars) // logged_chars是上次读/清除以来产生的日志字符数 count = logged_chars; if (do_clear) logged_chars = 0; limit = log_end; /* __put_user() 可以睡眠,当__put_user睡眠时,printk()可能覆盖写正在 拷贝到用户空间的消息,因此,这些消息被反方向拷贝,将buf覆盖部分的数据重写到buf的起始位置*/ for (i = 0; i < count && !error; i++) { //读取count个字符 j = limit-1-i; if (j + log_buf_len < log_end) break; c = LOG_BUF(j); //从环形缓冲区得到读取位置j spin_unlock_irq(&logbuf_lock); //将c位置的字符传递到用户空间的buf中,如果发生错误,将发生错误的c位置给error error = __put_user(c,&buf[count-1-i]); cond_resched(); spin_lock_irq(&logbuf_lock); } spin_unlock_irq(&logbuf_lock); if (error) break; error = i; if (i != count) { //表示__put_user没有拷贝完成 int offset = count-error; /* 拷贝期间缓冲区溢出,纠正用户空间缓冲区*/ for (i = 0; i < error; i++) { if (__get_user(c,&buf[i+offset]) || __put_user(c,&buf[i])) { //将覆盖部分的数据 重写到buf的起始位置 error = -EFAULT; break; } cond_resched(); } } break; case 5:/* 清除环形缓冲区*/ logged_chars = 0; break; case 6:/*关闭向控制台输出消息*/ console_loglevel = minimum_console_loglevel; break; case 7:/*开启向控制台输出消息*/ console_loglevel = default_console_loglevel; break; case 8:/* 设置打印到控制台的日志级别*/ error = -EINVAL; if (len < 1 || len > 8) goto out; if (len < minimum_console_loglevel) len = minimum_console_loglevel; console_loglevel = len; error = 0; break; case 9:/* 得到日志消息所占缓冲区的大小*/ error = log_end - log_start; break; case 10:/*返回环形缓冲区的大小*/ error = log_buf_len; break; default: error = -EINVAL; break; } out: return error; }