printf()等系統庫函數是如何實現的?
比如printf(),fopen()等,既然這些函數是庫函數,那麼他們應該是由更基本的單元組成的,那麼第一個庫函數應該是不依賴於其他庫函數構成的
但是基本元素(運算符,控制流)是如何直接/間接實現printf()這樣的功能的呢?這些庫函數的源碼如何查看呢?(這個是屬於哪個領域的內容?如果您願意告訴這些函數的源碼,或者實現的原理我在哪裡/哪本書里可以看到,定當感激不盡)
強行答題,我在我最近完成的玩具內核 LastAvenger/OS67 · GitHub 里也實現了 printf,題主既然沒有限定是哪個系統哪個庫的實現,那我就講講我是怎麼實現 printf 的。
完整展開可以寫半本書了……我挑重點講。
倒著說吧,依次實現:- putchar()
- puts()
- sys_puts()
- 用戶級別的 puts()
- sprintf()
- printf()
uint16_t *vga_mem = 0xb800;
vga_mem[0][0] = "A" | ((0 &<&< 4)| 4 )&<&<8;
這兩句就能在屏幕的第一行第一列輸出一個黑底紅字的字母 『A』,更多的資料還請參考
Text UI - OSDev Wiki 在此基礎上, 處理一下游標位置和特殊字元,就可以實現一個 getchar 了: OS67/drv/vga.c:getchar()。接著也可以很容易地實現 puts:對一個字元串的每個元素都 putchar 一次就好了。
現在我們可以自由地列印字元串了,但只是在內核中,用戶程序對內存地址 0xb800 是沒有讀寫許可權的。所以我們要實現一個系統調用 sys_write,讓用戶傳遞一個字元串給內核,由內核再列印出來。
為什麼不是叫 sys_puts 呢?因為 sys_wirte 不止可以往屏幕寫入,往文件,往管道寫入都是可以的,往屏幕寫字串,在這個內核里被實現為往設備 con 里寫入字串,當然在這裡這不是重點,所以我們還是把它叫做 sys_puts 好了。從內核的角度看,sys_puts 是個沒有參數也沒有返回值的函數,因為內核和用戶程序並不能簡單地通過 C 語言的調用約定來傳遞參數:它們不能共用一個棧。
所以 sys_puts 是這樣做的:它直接讀取用戶進程的用戶用戶棧,從上面取出參數,這裡至少得有兩個參數:字串指針地址 和 字元串長度。 檢查 字串地址 + 字串長度 是否還在該進程的空間內,這是必要的安全檢查。
檢查完後就可以把字元串交給 puts 了,再在該進程的中斷上下文中的 eax 中保存返回值。然後從系統調用返回。(在執行 int 0x80 和離開系統調用的時候內核做了很多工作,此處就不展開了,建議參考 xv6)在用戶程序看來,調用系統調用其實和調用普通函數並無二致,我們可以生成 C 函數到系統調用的介面:global puts
puts:
mov eax, SYS_PUTS ; 系統調用號
int 0x80
ret
而在 C 的頭文件的加入這樣的聲明:
int puts(char *addr, uint32_t n);
只要引用這個頭文件並鏈接入上面那段彙編代碼,就可以在用戶程序像使用普通函數一樣使用 puts 了。
如何實現 printf 呢?首先要處理的是對不定長參數的處理:C 語言有語法來實現這個:
void printk(const char *fmt, ...);
在 printf 和 puts 之間,我加了個函數 sprintf 作為中間層,而 printf 只是負責把 sprintf 的輸入傳遞給 puts 而已。
來說說 sprintf。
C 的默認調用約定是從右到左壓入參數,並由調用者清除堆棧,所以被調用者並不知道它有多少個參數要處理。 對於被調用者 printf,它唯一能確定的是第一個參數肯定是字元串指針,而這個參數的地址在往上就是可能存在的第二個參數(x86 棧從高地址向低地址增長),printf 把字元串地址和第二個參數的地址傳給 了 sprintf (第二個參數地址用宏 va_start 生成)。sprintf 只管在字元串中尋找控制字元串(%s %d %x 等),遇到一個 %d 則從棧里取出一個整數,遇到 %s 則取出一個字串指針,然後把指針往上移動一個 sizeof (用宏 va_arg,這裡和普通 sizeof 的不同在於要考慮棧的地址對齊),對於取出來的參數,把它轉換成字元串(用整數用 itoa,浮點數用 gcvt,想辦法插入原字元串就好了。
和參數相關的三個宏如下:#define va_start(ap, v) (ap = (va_list)v + _INTSIZEOF(v))
#define va_arg(ap,t) ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )
#define va_end(ap) (ap = (char*) 0)
OS67/libs/vsprint.c : sprinf 類似的實現
OS67/usr/uio.c: printf 實現標準庫函數依賴的是 system call。
很多答案都提到是調用write(),但實際上我們在程序中使用的write()僅僅是一個符合POSIX標準的入口(有時被稱作stub或者wrapper),是由libc提供的,不是真正底層的系統調用。
在Linux系統下,真正的系統調用是sys_write()。SYSCALL_DEFINE3(write, unsigned int, fd, const char __user *, buf,
size_t, count)
{
struct fd f = fdget_pos(fd);
ssize_t ret = -EBADF;
if (f.file) {
loff_t pos = file_pos_read(f.file);
ret = vfs_write(f.file, buf, count, pos);
if (ret &>= 0)
file_pos_write(f.file, pos);
fdput_pos(f);
}
return ret;
}
新的Linux內核使用了一系列宏來把POSIX定義的介面跟實際的系統調用對應起來。上面的代碼實際上就是sys_write()的定義。
我們可以看到經過簡單的封裝後sys_write()又調用了vfs_write()。ssize_t vfs_write(struct file *file, const char __user *buf, size_t count,
loff_t *pos)
{
ssize_t ret;
if (!(file-&>f_mode FMODE_WRITE))
return -EBADF;
if (!(file-&>f_mode FMODE_CAN_WRITE))
return -EINVAL;
if (unlikely(!access_ok(VERIFY_READ, buf, count)))
return -EFAULT;
ret = rw_verify_area(WRITE, file, pos, count);
if (ret &>= 0) {
count = ret;
file_start_write(file);
if (file-&>f_op-&>write)
ret = file-&>f_op-&>write(file, buf, count, pos);
else if (file-&>f_op-&>aio_write)
ret = do_sync_write(file, buf, count, pos);
else
ret = new_sync_write(file, buf, count, pos);
if (ret &> 0) {
fsnotify_modify(file);
add_wchar(current, ret);
}
inc_syscw(current);
file_end_write(file);
}
return ret;
}
在這裡,vfs_write()會繼續根據寫入的文件類型來調用驅動提供的底層函數,這些函數將真正的完成寫入操作。
至於從printf()到達write()的過程在stackexchange : Where the printf() Rubber Meets the Road有詳細的介紹。如果是在 UNIX 下,printf 實際上最後調用的就是 write 這個 syscall 。它就是往 stdout 這個文件裡面寫你要寫的那些東西,當然了,首先要把那些參數都格式化成一個字元串。
HoHo..我來說個蛋疼的方法...喏....首先呢...你要知道printf是在哪個dll里的...當然咯..一般是在runtime dll里..比如我這個就在MSVCR110D里...隨便寫個小程序..掛上windbg...
x MSVCR110D!*printf*
之後發現有很多類似的函數喲..
然後隨便找到你想看的函數...0fa99450 MSVCR110D!_fwprintf_l (&
0faa02d0 MSVCR110D!_vfprintf_s_l (&) )0fa994b0 MSVCR110D!_fwprintf_p_l (& )0fab2f20 MSVCR110D!swprintf (& )0fb163d0 MSVCR110D!_cwprintf_l (& ) 0fa9e150 MSVCR110D!_snwprintf_s_l (&
0fb178d0 MSVCR110D!_cwprintf_p_l (&) )0fa9d820 MSVCR110D!_scprintf (& )0faa05f0 MSVCR110D!vfprintf_s (& )0faa00f0 MSVCR110D!vprintf_helper (&
uf MSVCR110D!printf
然後...怎麼實現的...隨便看...
0:000&> uf MSVCR110D!printf
MSVCR110D!printf:0fa9ba20 55 push ebp
0fa9ba21 8bec mov ebp,esp0fa9ba23 6afe push 0FFFFFFFEh0fa9ba25 68487bb90f push offset MSVCR110D!_CT??_R0?AVlogic_errorstd+0x538 (0fb97b48)0fa9ba2a 68a0cab50f push offset MSVCR110D!_except_handler4 (0fb5caa0)0fa9ba2f 64a100000000 mov eax,dword ptr fs:[00000000h]0fa9ba35 50 push eax0fa9ba36 83c4e4 add esp,0FFFFFFE4h0fa9ba39 53 push ebx0fa9ba3a 56 push esi0fa9ba3b 57 push edi
0fa9ba3c a160b4ba0f mov eax,dword ptr [MSVCR110D!__security_cookie (0fbab460)]0fa9ba41 3145f8 xor dword ptr [ebp-8],eax0fa9ba44 33c5 xor eax,ebp0fa9ba46 50 push eax0fa9ba47 8d45f0 lea eax,[ebp-10h]0fa9ba4a 64a300000000 mov dword ptr fs:[00000000h],eax0fa9ba50 837d0800 cmp dword ptr [ebp+8],00fa9ba54 7409 je MSVCR110D!printf+0x3f (0fa9ba5f).......
這門課叫——操作系統
怎麼搞你搜知乎vs裡面有windows下msvcrt的源碼,可以稍微參考 最後肯定要用console系列的api 最後要跑到csrss里什麼的
printf要調用vprintf,把可變參數轉換成va_list。
然後調用vfprintf,標準輸出也是文件,合在一起便於處理。
之后里面就開始解析格式化字元串了,毫無疑問會用到atoi之類的轉換函數,和posix write(windows下是別的api)。
posix的函數都是系統調用,你如果調用了posix api,反編譯之後會看到一個int80,而不是通常的call(而windows下會把中斷包裝成dll)。這部分請見《CSAPP》,或者《程序員的自我修養》。去看CSAPP吧~
http://www.cs.cmu.edu/~213printf的原理,實際上正如蕭大所說,是操作系統的範疇.要說清這個函數的原理,大致需要明白庫函數/系統調用/中斷 的概念和原理.
1.理清兩大關係:庫函數/系統調用
首先printf和fopen等函數,屬於C語言的庫函數,任何人都可以調用;而open/wirte等函數屬於system call. 它們的關係如下:
printf是不能和內核直接打交道的,這個需要藉助system call來實現.其中一個完整的操作系統,並不包括庫函數.
2.你所不知道的事兒----中斷-彙編-bios
可能你接著要問?系統調用是如何來實現講字元輸出到屏幕或者文件的?
回想一下計算機的關鍵部件CPU,如果學過彙編的話,你可能記得由mov/add等指令,這些指令完成基本運算和數據傳輸. 如果考量到這個層次的話,你應該明白了.Printf對應的指令本質實際上是將一些數據搬運到顯存的某些位置,而屏幕是IO,IO操作是需要藉助中斷處理程序來進行的.上一段,實際上是說的系統調用write.
3.繼續提幾個問題
如何讓printf輸出到一個磁碟文件而不是屏幕上?
為何頻繁調用printf會導致程序運行"緩慢"?或者說很多OJ上,如果不小心寫了輸出函數,會導致超時?其他:[1]詳細查看printf的源代碼,請看這裡:GNU Project Archives[2]其他問題:參見操作系統設計與實現《C標準庫》,glibc源碼等~慢慢看。。。。
整個gcc都是開源的,所以printf之類的庫函數可以在gcc源碼里找,更底層的system call,整個Linux都是開源的,看源碼看源碼,至於Windows,只能看看MSDN了,
一個naive的printf是C新手的分水嶺吧
痛點在參數變長,然後怎麼實現這種邊長的參數,本質還是玩指針和stack,函數傳參的時候參數的壓棧順序是什麼自己動手寫printf -- 庫函數printf的實現
踏踏實實學好C語言
並不需要深厚的OS學問, printf的很多邏輯任務都是在user space完成的,gdb跟蹤到關鍵的文件啃代碼就是了。我記得JOS裡面lib里同樣有類似printf函數的實現也可以看看。又被大神叫來了~~~
這類偏向於系統底層的問題好像大家都會,但大家又都不會。其實現原理都能侃侃而談,但是它們的原理都簡單到不能用來 Pretend To Be The Letter Just After A(PBLAA)。
後來發現一個問題,你買了一個手機,認為手機就應該這樣,然後,你要做一個手機,或者研究手機的電路。。。難。。。這和一個普通(沒有鄙視的意思)程序員寫著寫著#include&int printf(FILE *stream, const char *format, va_list arg)
{
int i; //聲明一個變數,-_-
//下面申請一個空間,用來保存將帶有%d,%s之類展開之後的字元串
//例子:("Hello %d 0x%x
", 1 ,10) --&> "Hello 1 0x0a
"
char *s = malloc(4096);
//對於例子里的轉換:
//s是轉換之後字元串的指針
//format指向的是"Hello %d 0x%x
"
//arg是參數列表:1,10(就是從下標為1開始到最後的參數)
i = vsprintf(s, format, arg);
i = fwrite(s, 1, i, stream); //這是個系統調用,這個再去學習,這裡假設成將字元
//串拷貝到數組stream中
free(s); //釋放空間
return i;
}
下面是vsprintf的一個簡單實現
int vsprintf(char *s, const char *format, va_list arg)
{
UCHAR c, *t = s, *p, flag_left, flag_zero /* , flag_sign, flag_space */;
UCHAR temp[32], *q;
temp[31] = " ";
int field_min, field_max, i;
long l;
static char hextable_X[16] = "0123456789ABCDEF";
static char hextable_x[16] = "0123456789abcdef";
for (;;) {
c = *format++;
if (c != "%") {
put1char:
*t++ = c;
if (c)
continue;
return t - (UCHAR *) s - 1;
}
flag_left = flag_zero = /* flag_sign = flag_space = flag_another = */ 0;
c = *format++;
for (;;) {
if (c == "-")
flag_left = 1;
else if (c == "0")
flag_zero = 1;
else
break;
c = *format++;
}
field_min = 0;
if ("1" &<= c c &<= "9") {
format--;
field_min = (int) strtoul0(format, 10, c);
c = *format++;
} else if (c == "*") {
field_min = va_arg(arg, int);
c = *format++;
}
field_max = INT_MAX;
if (c == ".") {
c = *format++;
if ("1" &<= c c &<= "9") {
format--;
field_min = (int) strtoul0(format, 10, c);
c = *format++;
} else if (c == "*") {
field_max = va_arg(arg, int);
c = *format++;
}
}
if (c == "s") {
if (field_max != INT_MAX)
goto mikan;
p = va_arg(arg, char *);
l = strlen(p);
if (*p) {
c = " ";
copy_p2t:
if (flag_left == 0) {
while (l &< field_min) {
*t++ = c;
field_min--;
}
}
do {
*t++ = *p++;
} while (*p);
}
while (l &< field_min) {
*t++ = " ";
field_min--;
}
continue;
}
if (c == "l") {
c = *format++;
if (c != "d" c != "x" c != "u") {
format--;
goto mikan;
}
}
if (c == "u") {
l = va_arg(arg, UINT);
goto printf_u;
}
if (c == "d") {
printf_d:
l = va_arg(arg, long);
if (l &< 0) {
*t++ = "-";
l = - l;
field_min--;
}
printf_u:
if (field_max != INT_MAX)
goto mikan;
if (field_min &<= 0)
field_min = 1;
p = setdec(temp[31], l);
printf_x2:
c = " ";
l = temp[31] - p;
if (flag_zero)
c = "0";
goto copy_p2t;
}
if (c == "i")
goto printf_d;
if (c == "%")
goto put1char;
if (c == "x") {
q = hextable_x;
printf_x:
l = va_arg(arg, UINT);
p = temp[31];
do {
*--p = q[l 0x0f];
} while ((*(UINT *) l) &>&>= 4);
goto printf_x2;
}
if (c == "X") {
q = hextable_X;
goto printf_x;
}
if (c == "p") {
i = (int) va_arg(arg, void *);
p = temp[31];
for (l = 0; l &< 8; l++) {
*--p = hextable_X[i 0x0f];
i &>&>= 4;
}
goto copy_p2t;
}
if (c == "o") {
l = va_arg(arg, UINT);
p = temp[31];
do {
*--p = hextable_x[l 0x07];
} while ((*(UINT *) l) &>&>= 3);
goto printf_x2;
}
if (c == "f") {
if (field_max == INT_MAX)
field_max = 6;
/* for ese */
if (field_min &< field_max + 2)
field_min = field_max + 2;
do {
*t++ = "#";
} while (--field_min);
continue;
}
if(c == "c"){
if (field_max == INT_MAX)
field_max = 6;
/* for ese */
if (field_min &< field_max + 2)
field_min = field_max + 2;
do {
*t++ = "C";
} while (--field_min);
continue;
}
mikan:
for(;;);
}
}
這大概就是printf實現的基本原理,如果你想運行,添加下面的代碼
typedef unsigned char UCHAR;
typedef unsigned int UINT;
#define UINT_MAX (+0xffffffff)
#define ULONG_MAX UINT_MAX
static int prefix(int c)
{
signed char base = 0;
if ("a" &<= c c &<= "z")
c += "A" - "a";
if (c == "B")
base = 2;
if (c == "D")
base = 10;
if (c == "O")
base = 8;
if (c == "X")
base = 16;
return base;
}
unsigned long strtoul0(const unsigned char **ps, int base, unsigned char *errflag)
{
const unsigned char *s = *ps;
unsigned long val = 0, max;
int digit;
if (base == 0) {
base += 10;
if (*s == "0") {
base = prefix(*(s + 1));
if (base == 0)
base += 8; /* base = 8; */
}
}
if (*s == "0") {
if (base == prefix(*(s + 1)))
s += 2;
}
max = ULONG_MAX / base;
*errflag = 0;
for (;;) {
digit = 99;
if ("0" &<= *s *s &<= "9")
digit = *s - "0";
if ("A" &<= *s *s &<= "Z")
digit = *s - ("A" - 10);
if ("a" &<= *s *s &<= "z")
digit = *s - ("a" - 10);
if (digit &>= base)
break;
if (val &> max)
goto err;
val *= base;
if (ULONG_MAX - val &< (unsigned long) digit) {
err:
*errflag = 1;
val = ULONG_MAX;
} else
val += digit;
s++;
}
*ps = s;
return val;
}
static UCHAR *setdec(UCHAR *s, UINT ui)
{
do {
*--s = (ui % 10) + "0";
} while (ui /= 10);
return s;
}
int write(int fd, const void *buf, int count)
{
int res = 0;
asm(
"movl $4, %%eax
"
"movl %1, %%ebx
"
"movl %2, %%ecx
"
"movl %3, %%edx
"
"int $0x80
"
"movl %%eax, %0
"
:"=m"(res)
:"m"(fd), "m"(buf), "m"(count)
);
return res;
}
(知乎的編輯器略噁心)
printf()是用于格式化輸出的,會根據參數做各種賞心悅目的格式化,支持任意個參數(參考`man stdarg`)不過最終會調用write()實現寫字元到屏幕的功能,後者通過0x80號中斷陷入內核。(參考貼出的代碼,網上很多,我是參考《程序員的自我修養》給出的)可以看看《程序員的自我修養》,看完就懂了。http://www.douban.com/doubanapp/dispatch?uri=/book/3775842/另外注意區分標準庫和系統調用
從一個printf()函數足以窺探一下整個計算機體系的秘密。
首先推薦南七技校網路課 Linux內核分析 - 網易雲課堂 這門課講的很好,擼下來大有幫助
如果題主很貧窮(這門課內部價也不低),可以在網上搜羅這門課的上課筆記。
題主的這個問題可以在系統調用過程 - 20135124 - 博客園 領會。
基本上串聯起來就是 高級語言——系統API(運行庫)——中斷——系統調用——硬體資源
為什麼說此問題足以窺探整個計算機體系的秘密。
說到系統調用得說內核態吧,說到切換得說到中斷吧,說到中斷得說到向量表吧,說到向量表得說到BIOS吧(Linux中系統跟bios的關係其實不是特別大),說到BIOS得說到ROM吧,說到ROM得說到觸發器吧,說到觸發器得說到邏輯門吧,說到邏輯門得說到晶體管吧,說到晶體管得說到不純半導體吧。。。。。。硬體方面,南北橋結構、匯流排、埠定址、訪問控制方式。。。。。。更不用說高級語言層面的東西了。
即能由小知大、又可以由大見小。
printf本身是標準庫函數,底層實現的一定是利用system call
windows上有個標準設備,std,printf在根源上是,通過windows API GetStdHandle 來得到這個設備,然後通過WriteFile API來寫這個設備。。。printf也好,putchar 也好,他只是向一個stdout緩衝區來寫數據(這是設備系統無關的),這是C語言做的事。。。真正達到發送條件是,他是調用WriteFile來將stdout發送出去,這才是設備系統有關的,WriteFile函數是系統的,到其他環境上比如linux 會變成write,甚至到單片機上是while(ch) UARTA=*(ch++);這樣。。。設備無關的函數可以不變。。。 還有系統 系統 API是怎麼調用的,拿linux來說,linux我其實不是很熟,API都是在系統中已經完成的函數,當你調用API,他的第一步是發出CPU軟中斷和中斷號,每個API會對應一個軟中斷,軟中斷後CPU經過一系列處理最終會進入系統模式,並通過中斷號去查向量表,找到函數指針的位置,然後調用,系統這些核心API並不能算是個函數庫,他一開始就跟隨系統裝到內存里了。。。
軟體上最根本的是向顯存地址寫數據屏幕就顯示出字元了,進行這個操作的是操作系統內核,printf函數調用system call讓內核把數據寫到顯存。
推薦閱讀:
※4G內存,安裝win7 32位流暢,還是安裝win7 64位好?
※Windows 7 有哪些優化的方法?
※CCIE,和思科認證?
※中國的超級計算機被用來幹什麼了?
※一個50萬人口的小城鎮在一天之內能產生超過10TB的對外網路流量嗎?