printf 從用戶層到OS層之間的調用關係

瞭解printf函數的調用機制,我將以PowerPC爲例子介紹它到OS的調用過程,首先我們先來看看printf的函數的具體的實現,如下:
static char sprint_buf[1024];            //定義一個buf存儲空間來存放參數;
int printf(const char *fmt, ...)
{
 va_list args;
 int n;
 va_start(args, fmt);
 n = vsprintf(sprint_buf, fmt, args);           //主要用於輸出格式的匹配工作;
 va_end(args);
 if (console_ops.write)
     console_ops.write(sprint_buf, n);        //終端的操作,註冊和初始化串口;
 return n;
}
        從上面的函數來看,printf函數主要做的就是兩件事情,首先確定內容如何輸出,以何種格式輸出,然後初始化串口,通過串口將數據輸出到硬件層。

下面我們將來介紹它的調用過程,以幫助我們對printf函數的理解。
int vsprintf(char *buf, const char *fmt, va_list args)
{
 int len;
 unsigned long long num;
 int i, base;
 char * str;
 const char *s;
 int flags;                            /* flags to number() */
 int field_width;                  /* width of output field */
 int precision;                     /* min. # of digits for integers; max number of chars for from string */
 int qualifier;                      /* 'h', 'l', or 'L' for integer fields */
                                           /* 'z' support added 23/7/1999 S.H.    */
/* 'z' changed to 'Z' --davidm 1/25/99 */
for (str=buf ; *fmt ; ++fmt) {
  if (*fmt != '%') {
   *str++ = *fmt;
   continue;
  }
   
  /* process flags */
  flags = 0;
  repeat:
   ++fmt; /* this also skips first '%' */
   switch (*fmt) {
    case '-': flags |= LEFT; goto repeat;
    case '+': flags |= PLUS; goto repeat;
    case ' ': flags |= SPACE; goto repeat;
    case '#': flags |= SPECIAL; goto repeat;
    case '0': flags |= ZEROPAD; goto repeat;
    }
 
  /* get field width */
  field_width = -1;
  if ('0' <= *fmt && *fmt <= '9')
   field_width = skip_atoi(&fmt);
  else if (*fmt == '*') {
   ++fmt;
   /* it's the next argument */
   field_width = va_arg(args, int);
   if (field_width < 0) {
    field_width = -field_width;
    flags |= LEFT;
   }
  }

  /* get the precision */
  precision = -1;
  if (*fmt == '.') {
   ++fmt;
   if ('0' <= *fmt && *fmt <= '9')
    precision = skip_atoi(&fmt);
   else if (*fmt == '*') {
    ++fmt;
    /* it's the next argument */
    precision = va_arg(args, int);
   }
   if (precision < 0)
    precision = 0;
  }

  /* get the conversion qualifier */
  qualifier = -1;
  if (*fmt == 'l' && *(fmt + 1) == 'l') {
   qualifier = 'q';
   fmt += 2;
  } else if (*fmt == 'h' || *fmt == 'l' || *fmt == 'L'
   || *fmt == 'Z') {
   qualifier = *fmt;
   ++fmt;
  }

  /* default base */
  base = 10;

  switch (*fmt) {
  case 'c':
   if (!(flags & LEFT))
    while (--field_width > 0)
     *str++ = ' ';
   *str++ = (unsigned char) va_arg(args, int);
   while (--field_width > 0)
    *str++ = ' ';
   continue;

  case 's':
   s = va_arg(args, char *);
   if (!s)
    s = "<NULL>";

   len = strnlen(s, precision);

   if (!(flags & LEFT))
    while (len < field_width--)
     *str++ = ' ';
   for (i = 0; i < len; ++i)
    *str++ = *s++;
   while (len < field_width--)
    *str++ = ' ';
   continue;

  case 'p':
   if (field_width == -1) {
    field_width = 2*sizeof(void *);
    flags |= ZEROPAD;
   }
   str = number(str,
    (unsigned long) va_arg(args, void *), 16,
    field_width, precision, flags);
   continue;
  case 'n':
   if (qualifier == 'l') {
    long * ip = va_arg(args, long *);
    *ip = (str - buf);
   } else if (qualifier == 'Z') {
    size_t * ip = va_arg(args, size_t *);
    *ip = (str - buf);
   } else {
    int * ip = va_arg(args, int *);
    *ip = (str - buf);
   }
   continue;

  case '%':
   *str++ = '%';
   continue;

  /* integer number formats - set up the flags and "break" */
  case 'o':
   base = 8;
   break;

  case 'X':
   flags |= LARGE;
  case 'x':
   base = 16;
   break;

  case 'd':
  case 'i':
   flags |= SIGN;
  case 'u':
   break;

  default:
   *str++ = '%';
   if (*fmt)
    *str++ = *fmt;
   else
    --fmt;
   continue;
  }
  if (qualifier == 'l') {
   num = va_arg(args, unsigned long);
   if (flags & SIGN)
    num = (signed long) num;
  } else if (qualifier == 'q') {
   num = va_arg(args, unsigned long long);
   if (flags & SIGN)
    num = (signed long long) num;
  } else if (qualifier == 'Z') {
   num = va_arg(args, size_t);
  } else if (qualifier == 'h') {
   num = (unsigned short) va_arg(args, int);
   if (flags & SIGN)
    num = (signed short) num;
  } else {
   num = va_arg(args, unsigned int);
   if (flags & SIGN)
    num = (signed int) num;
  }
  str = number(str, num, base, field_width, precision, flags);
 }
 *str = '\0';
 return str-buf;
}

從這個函數我們可以看出printf的格式符的重要性,以及它所支持的所有輸出格式,在這個函數中調用了兩個重要的函數,下面我們來介紹這倆個重要的函數函數的功能;
static char * number(char * str, unsigned long long num, int base, int size, int precision, int type)
{
 char c,sign,tmp[66];
 const char *digits="0123456789abcdefghijklmnopqrstuvwxyz";
 int i;
 if (type & LARGE)
  digits = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
 if (type & LEFT)
  type &= ~ZEROPAD;
 if (base < 2 || base > 36)
  return 0;
 c = (type & ZEROPAD) ? '0' : ' ';
 sign = 0;
 if (type & SIGN) {
  if ((signed long long)num < 0) {
   sign = '-';
   num = - (signed long long)num;
   size--;
  } else if (type & PLUS) {
   sign = '+';
   size--;
  } else if (type & SPACE) {
   sign = ' ';
   size--;
  }
 }
 if (type & SPECIAL) {
  if (base == 16)
   size -= 2;
  else if (base == 8)
   size--;
 }
 i = 0;
 if (num == 0)
  tmp[i++]='0';
 else while (num != 0) {
  tmp[i++] = digits[do_div(num, base)];
 }
 if (i > precision)
  precision = i;
 size -= precision;
 if (!(type&(ZEROPAD+LEFT)))
  while(size-->0)
   *str++ = ' ';
 if (sign)
  *str++ = sign;
 if (type & SPECIAL) {
  if (base==8)
   *str++ = '0';
  else if (base==16) {
   *str++ = '0';
   *str++ = digits[33];
  }
 }
 if (!(type & LEFT))
  while (size-- > 0)
   *str++ = c;
 while (i < precision--)
  *str++ = '0';
 while (i-- > 0)
  *str++ = tmp[i];
 while (size-- > 0)
  *str++ = ' ';
 return str;
}
        
        這個函數的功能主要是將數字字符串按2,8,16進制的形式輸出結果的處理,以及對輸出的結果正負的處理和空格的處理。同時也解決了數據的輸出對齊問題,從而讓數據根據我們想要的形式輸出結果,下面我們來介紹下該函數所用到的一些宏的定義,以便於幫助我們來理解該函數的功能。
#define ZEROPAD 1                             /* pad with zero */
#define SIGN         2                             /* unsigned/signed long */
#define PLUS         4                             /* show plus */
#define SPACE      8                             /* space if plus */
#define LEFT        16                            /* left justified */
#define SPECIAL  32                            /* 0x */
#define LARGE     64                            /* use 'ABCDEF' instead of 'abcdef' */
          對於這些宏我就不做過多的解釋了,只要將它們代入具體的宏的位置去就可以了,它們只是基本的宏替換的功能而已,沒有其它過多的用法了,看到這個宏定義,讓我想到了枚舉,其實我可以用枚舉的方法來定義這些宏定義,只是在初始化的時候有點區別,枚舉的第一個成員默認被分配爲0,之後每個成員要是沒有賦值它將會在前一個成員的基礎上加一,當讓枚舉和宏定義還是有區別的,宏定義是在編譯器編譯時對宏進行宏展開的工作,所以它不分配內存空間,而枚舉就有些不一樣了,他是結構類型,所以在爲它初始化後它會佔用內存空間,所以它會分配內存;還是繼續來說說另外一個重要的函數的功能吧!

static int skip_atoi(const char **s)
{
 int i, c;
 for (i = 0; '0' <= (c = **s) && c <= '9'; ++*s)
        i = i*10 + c - '0';
 return i;
}
看到這個函數我相信我們大家都很熟悉了,這個函數和我們之前調用的atoi的功能差不多,本函數主要是主要是將字符型數字轉化爲十進制數字的功能,它是通過一位一位的返回模式轉化的。

下面我們再來看看printf函數的另外的一個函數的功能,首先來看看console_ops 結構體;
struct console_ops console_ops;
/* Console operations */
struct console_ops {
 int (*open)(void);
 void (*write)(const char *buf, int len);
 void (*edit_cmdline)(char *buf, int len);
 void (*close)(void);
 void *data;
};

另外一個函數就是這個結構的write成員函數,下面我們來看看它的功能。
int serial_console_init(void)
{
 void *devp;
 int rc = -1;
 devp = serial_get_stdout_devp();
 if (devp == NULL)
  goto err_out;
 if (dt_is_compatible(devp, "ns16550") ||
     dt_is_compatible(devp, "pnpPNP,501"))
  rc = ns16550_console_init(devp, &serial_cd);
 else if (dt_is_compatible(devp, "marvell,mv64360-mpsc"))
  rc = mpsc_console_init(devp, &serial_cd);
 else if (dt_is_compatible(devp, "fsl,cpm1-scc-uart") ||
          dt_is_compatible(devp, "fsl,cpm1-smc-uart") ||
          dt_is_compatible(devp, "fsl,cpm2-scc-uart") ||
          dt_is_compatible(devp, "fsl,cpm2-smc-uart"))
  rc = cpm_console_init(devp, &serial_cd);
 else if (dt_is_compatible(devp, "fsl,mpc5200-psc-uart"))
  rc = mpc5200_psc_console_init(devp, &serial_cd);
 else if (dt_is_compatible(devp, "xlnx,opb-uartlite-1.00.b") ||
   dt_is_compatible(devp, "xlnx,xps-uartlite-1.00.a"))
  rc = uartlite_console_init(devp, &serial_cd);
 /* Add other serial console driver calls here */
 if (!rc) {
  console_ops.open = serial_open;
  console_ops.write = serial_write;
  console_ops.close = serial_close;
  console_ops.data = &serial_cd;
  if (serial_cd.getc)
   console_ops.edit_cmdline = serial_edit_cmdline;
  return 0;
 }
err_out:
 return -1;
}

當函數執行時,console_ops.write函數就相當於執行serial_write函數,我們來看看它又做了些什麼事情;
static void serial_write(const char *buf, int len)
{
 struct serial_console_data *scdp = console_ops.data;
 while (*buf != '\0')
     scdp->putc(*buf++);
}

由上面的console_ops結構體我們已經知道了它有一個data成員,下面我們來具體看看serial_console_data結構的具體內容;
struct serial_console_data {
 int (*open)(void);
 void (*putc)(unsigned char c);
 unsigned char (*getc)(void);
 u8 (*tstc)(void);
 void (*close)(void);
};

看到這個結構體就是一個操作串口的操作集合,我們只要調用它就可以操作串口了,serial_write函數主要是操作它的putc函數來輸出內容到串口,下面看看putc函數,通過查找得知它又調用其它函數;
int cpm_console_init(void *devp, struct serial_console_data *scdp)
{
 void *vreg[2];
 u32 reg[2];
 int is_smc = 0, is_cpm2 = 0;
 void *parent, *muram;
 void *muram_addr;
 unsigned long muram_offset, muram_size;

 if (dt_is_compatible(devp, "fsl,cpm1-smc-uart")) {
  is_smc = 1;
 } else if (dt_is_compatible(devp, "fsl,cpm2-scc-uart")) {
  is_cpm2 = 1;
 } else if (dt_is_compatible(devp, "fsl,cpm2-smc-uart")) {
  is_cpm2 = 1;
  is_smc = 1;
 }

 if (is_smc) {
  enable_port = smc_enable_port;
  disable_port = smc_disable_port;
 } else {
  enable_port = scc_enable_port;
  disable_port = scc_disable_port;
 }

 if (is_cpm2)
  do_cmd = cpm2_cmd;
 else
  do_cmd = cpm1_cmd;

 if (getprop(devp, "fsl,cpm-command", &cpm_cmd, 4) < 4)
  return -1;

 if (dt_get_virtual_reg(devp, vreg, 2) < 2)
  return -1;

 if (is_smc)
  smc = vreg[0];
 else
  scc = vreg[0];

 param = vreg[1];

 parent = get_parent(devp);
 if (!parent)
  return -1;

 if (dt_get_virtual_reg(parent, &cpcr, 1) < 1)
  return -1;

 muram = finddevice("/soc/cpm/muram/data");
 if (!muram)
  return -1;

 /* For bootwrapper-compatible device trees, we assume that the first
  * entry has at least 128 bytes, and that #address-cells/#data-cells
  * is one for both parent and child.
  */

 if (dt_get_virtual_reg(muram, &muram_addr, 1) < 1)
  return -1;

 if (getprop(muram, "reg", reg, 8) < 8)
  return -1;

 muram_offset = reg[0];
 muram_size = reg[1];

 /* Store the buffer descriptors at the end of the first muram chunk.
  * For SMC ports on CPM2-based platforms, relocate the parameter RAM
  * just before the buffer descriptors.
  */

 cbd_offset = muram_offset + muram_size - 2 * sizeof(struct cpm_bd);

 if (is_cpm2 && is_smc) {
  u16 *smc_base = (u16 *)param;
  u16 pram_offset;

  pram_offset = cbd_offset - 64;
  pram_offset = _ALIGN_DOWN(pram_offset, 64);

  disable_port();
  out_be16(smc_base, pram_offset);
  param = muram_addr - muram_offset + pram_offset;
 }

 cbd_addr = muram_addr - muram_offset + cbd_offset;

 scdp->open = cpm_serial_open;
 scdp->putc = cpm_serial_putc;
 scdp->getc = cpm_serial_getc;
 scdp->tstc = cpm_serial_tstc;

 return 0;
}

這個函數的具體內容就不看了,就去看看cpm_serial_putc函數的具體實現,來看看它實現什麼功能;
static void cpm_serial_putc(unsigned char c)
{
 while (tbdf->sc & 0x8000)
 barrier();      //執行空操作;
 sync();
 tbdf->addr[0] = c; 
 eieio();         //上下文同步;
 tbdf->sc |= 0x8000;
}
針對barrier函數和eieio函數的具體實現做一些說明:
static inline void barrier(void)
{
     asm volatile("" : : : "memory");     //執行空指令,但是消耗沒存空間;      
}

static inline void eieio(void)
{
     __asm__ __volatile__ ("eieio" : : : "memory");     //同上
}
        eieio 是上下文同步指令。“上下文同步”指的是:處理器內核包含着多個獨立的執行單元,所以它能夠並行的執行多個指令並且是亂序的。上下文同步指令用於需要嚴格秩序的地方,進行強制嚴格的指令順序。eieio代表“強制按順序執行IO”。在執行過程中,加載/存儲單元等待前一個訪問結束之後再開始運行加載/存儲指令。eieio的目的就是爲了防止執行過程中的隨意加載和存儲。

我們再來看看tbdf具體是什麼結構,看看的結構體的具體作用;
static struct cpm_bd *tbdf, *rbdf;
struct cpm_bd {
 u16 sc;                    /* Status and Control */
 u16 len;                  /* Data length in buffer */
 u8 *addr;                /* Buffer address in host memory */
};
        
        從該結構體的可以看出,putc最後存儲的內容其實是存在了buff空間裏了,但是疑問也同時產生了tbdf結構體是在什麼時候被初始化的,初始化值是什麼,帶着疑問我們繼續來往下看看;
static int cpm_serial_open(void)
{
 disable_port();
 out_8(&param->rfcr, 0x10);
 out_8(&param->tfcr, 0x10);
 out_be16(&param->mrblr, 1);
 out_be16(&param->maxidl, 0);
 out_be16(&param->brkec, 0);
 out_be16(&param->brkln, 0);
 out_be16(&param->brkcr, 0);
 
 rbdf = cbd_addr;
 rbdf->addr = (u8 *)rbdf - 1;
 rbdf->sc = 0xa000;
 rbdf->len = 1;

 tbdf = rbdf + 1;
 tbdf->addr = (u8 *)rbdf - 2;
 tbdf->sc = 0x2000;
 tbdf->len = 1;
 
 sync();
 out_be16(&param->rbase, cbd_offset);
 out_be16(&param->tbase, cbd_offset + sizeof(struct cpm_bd));
 do_cmd(CPM_CMD_INIT_RX_TX);
 enable_port();
 return 0;
}

         從這個函數我們可以看出它對tbdf的初始化工作,我們可以根據它的初始化內容結合putc函數來理解數據的輸出功能,do_cmd函數出現過多次,我們來看看這個函數,再看這個函數之前我先來看看幾個宏定義;

#define CPM_CMD_STOP_TX     4
#define CPM_CMD_RESTART_TX  6
#define CPM_CMD_INIT_RX_TX  0

下面來看看do_cmd函數的實現,根據函數的查找我們可以看到do_cmd函數其實是在cpm_console_init函數裏面被初始化的,實際上它是調用cpm2_cmd(do_cmd = cpm2_cmd)函數;
static void cpm2_cmd(int op)
{
 while (in_be32(cpcr) & 0x10000)
  ;
 out_be32(cpcr, op | cpm_cmd | 0x10000);
 while (in_be32(cpcr) & 0x10000)
  ;
}
         針對這個函數我們來看看它調用的函數的功能,由於它內嵌彙編,所以我們要看看它的彙編指令的意思,結合它一起來理解該函數的功能,先來看看函數本身吧;
static inline unsigned in_be32(const volatile unsigned *addr)
{
 unsigned ret;
 __asm__ __volatile__("lwz%U1%X1 %0,%1; twi 0,%0,0; isync"
        : "=r" (ret) : "m" (*addr));
 return ret;
}

static inline unsigned in_be32(const volatile unsigned *addr)
{
 unsigned ret;
 __asm__ __volatile__("lwz%U1%X1 %0,%1; twi 0,%0,0; isync"
        : "=r" (ret) : "m" (*addr));
 return ret;
}
         我們可以看到其實這兩個函數本身都是在調用匯編指令,由於我們分析的PowerPC架構的代碼,所以我們只能去找它的指令集看看,看是否有lwz指令,看它是什麼意思了;
           lwz rD,d(rA) ;EA=(rA|0)+d,從EA處讀取4個字節的數,並加載到rD。

         從上面的彙編指令我們可以看出,其實就是加載字節的指令,將EA的字節存放到rD中去。針對上面的彙編部分我們做一些簡單的解釋,

 lwz%U1%X1 %0,%1;   將%1的內容加載到%0中去;
 twi 0,%0,0;                這是內核的寄存器的配置,具體執行什麼就需要根據具體的芯片指令手冊進行查找了,這裏不作具體介紹;
 isync                        這個是同步數據的指令;
 : "=r" (ret)             “=r”表示以寄存器變量的形式輸出數據,然後將數據給ret變量,它對應上面指令的%0;
 : "m" (*addr)        “m” 表示輸入的是內存中的變量,它對應的是%1,也就是將*addr替換%1;

        我們就先介紹到這裏,就不分析細節方面的函數了,如果要深入的理解它可以從細節方面的着手,本文主要是分析printf函數的OS的基本的調用過程,是爲讓初學者對它有個大概的認識,以便於後續的學習,當然本文跟多的是我個人的理解,所以中間肯定存在理解錯誤的地方;在後續工作中我將會深入的去了解它的具體的過程,並且會寫成文檔收集起來以供有需要的人學習。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章