以下內容主要摘錄自《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; }