printfの先をほんの少し掘ってみた

printfの先をほんの少し掘ってみた

printf の先を少しだけ掘ってみました。はい、少しだけ。
Clock Icon2024.08.31

こんにちは、高崎@アノテーション です。

はじめに

X にて下記のポストがありました。

https://x.com/s5ml/status/1820749346700411067

ちょっと興味があったので、ソースを取得して少し掘ってみたので記事にします。

動作環境

動作環境については、下記をベースとします。

  • Ubuntu 24.04 LTS
  • CPU x86_64
  • 下記のソースの printf で考えてみる
#include <stdio.h>
#include <stdlib.h>

int main()
{
  printf("Hello, world.\n");
  return 0;
}

printf を探る

Ubuntu 24.04 LTS の glibc のバージョンは下記で調べました。

wataru@ubuntu-24-04:~$ ldd --version
ldd (Ubuntu GLIBC 2.39-0ubuntu8.3) 2.39
Copyright (C) 2024 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
作者 Roland McGrath および Ulrich Drepper。

ということで、glibc 2.39 のソースを下記から取得します。

https://ftp.gnu.org/gnu/glibc/

glibc の printf

glibc のソースを展開し printf のコードを見ますと、

stdio-common/printf.c
    :
#undef printf

/* Write formatted output to stdout from the format string FORMAT.  */
/* VARARGS1 */
int
__printf (const char *format, ...)
{
  va_list arg;
  int done;

  va_start (arg, format);
  done = __vfprintf_internal (stdout, format, arg, 0);
  va_end (arg);

  return done;
}

#undef _IO_printf
ldbl_strong_alias (__printf, printf);
ldbl_strong_alias (__printf, _IO_printf);

関数自体は __ がついていますが、関数の後にある ldbl_strong_alias にて printf が __printf として定義されていますので実体はこれだと思います。

va_list 構造体、va_start、va_end のマクロは動的引数を扱う時のやり方ですので、解析は省略して、この動的引数を __vfprintf_internal 関数に渡してコールされますので、こちらを見ていきます。

__vfprintf_internal

定義とその実体はココにありました。

stdio-common/vfprintf-internal.c
    :
# define vfprintf	__vfprintf_internal
    :
/* The FILE-based function.  */
int
vfprintf (FILE *s, const CHAR_T *format, va_list ap, unsigned int mode_flags)
{
  /* Orient the stream.  */
#ifdef ORIENT
  ORIENT;
#endif

  /* Sanity check of arguments.  */
  ARGCHECK (s, format);

#ifdef ORIENT
  /* Check for correct orientation.  */
  if (_IO_vtable_offset (s) == 0
      && _IO_fwide (s, sizeof (CHAR_T) == 1 ? -1 : 1)
      != (sizeof (CHAR_T) == 1 ? -1 : 1))
    /* The stream is already oriented otherwise.  */
    return EOF;
#endif

  if (!_IO_need_lock (s))
    {
      struct Xprintf (buffer_to_file) wrap;
      Xprintf (buffer_to_file_init) (&wrap, s);
      Xprintf_buffer (&wrap.base, format, ap, mode_flags);
      return Xprintf (buffer_to_file_done) (&wrap);
    }

  int done;

  /* Lock stream.  */
  _IO_cleanup_region_start ((void (*) (void *)) &_IO_funlockfile, s);
  _IO_flockfile (s);

  /* Set up the wrapping buffer.  */
  struct Xprintf (buffer_to_file) wrap;
  Xprintf (buffer_to_file_init) (&wrap, s);

  /* Perform the printing operation on the buffer.  */
  Xprintf_buffer (&wrap.base, format, ap, mode_flags);
  done = Xprintf (buffer_to_file_done) (&wrap);

  /* Unlock the stream.  */
  _IO_funlockfile (s);
  _IO_cleanup_region_end (0);

  return done;
}

出力先のストリームがロック対象かどうかで処理が分かれていますが、やっていることは、

  • Xprintf(buffer_to_file) の構造体を定義
  • Xprintf(buffer_to_file_init) を実行
  • Xprintf_buffer を実行
  • Xprintf(buffer_to_file_done) を実行した結果を戻り値にして完了

Xprintf については下記で定義されています。

stdio-common/printf_buffer-char.h
    :
#define Xprintf(n) __printf_##n
    :

__printf_buffer_to_file という構造体が別途定義されているわけですね。

stdio-common/printf_buffer_to_file.h
    :
struct __printf_buffer_to_file
{
  struct __printf_buffer base;
  FILE *fp;

  /* Staging buffer.  Used if fp does not have any available buffer
     space.  */
  char stage[PRINTF_BUFFER_SIZE_TO_FILE_STAGE];
};
    :

詳細は省きますが、同じソースに __printf_buffer_to_file_init と __printf_buffer_to_file_done が定義されていました。

途中の Xprintf_buffer ですが、同じ vfprintf 関数と同じソースにありました。

stdio-common/vfprintf-internal.c
    :
/* The buffer-based function itself.  */
void
Xprintf_buffer (struct Xprintf_buffer *buf, const CHAR_T *format,
		  va_list ap, unsigned int mode_flags)
{
  /* The character used as thousands separator.  */
  THOUSANDS_SEP_T thousands_sep = 0;
    :
    :
/* Write the literal text before the first format.  */
  Xprintf_buffer_write (buf, format,
			  lead_str_end - (const UCHAR_T *) format);
  if (Xprintf_buffer_has_failed (buf))
    return;
    :
    :
    :
      /* Process current format.  */
      while (1)
	{
    :
    :
      /* Write the following constant string.  */
      Xprintf_buffer_write (buf, (const CHAR_T *) end_of_spec,
			      f - end_of_spec);
    }
  while (*f != L_('\0') && !Xprintf_buffer_has_failed (buf));

 all_done:
  /* printf_positional performs cleanup under its all_done label, so
     vfprintf-process-arg.c uses it for this function and
     printf_positional below.  */
  return;

  /* Hand off processing for positional parameters.  */
do_positional:
  printf_positional (buf, format, readonly_format, ap, &ap_save,
		     nspecs_done, lead_str_end, work_buffer,
		     save_errno, grouping, thousands_sep, mode_flags);
}

途中、色々省きましたが、いわゆる %d %s といった書式文字列を解析して展開しているようです。

それでは次の Xprintf_buffer_write を見ていきます。

stdio-common/Xprintf_buffer_write.c
    :
void
Xprintf_buffer_write (struct Xprintf_buffer *buf,
                        const CHAR_T *s, size_t count)
{
  if (__glibc_unlikely (Xprintf_buffer_has_failed (buf)))
    return;

  while (count > 0)
    {
      if (buf->write_ptr == buf->write_end && !Xprintf_buffer_flush (buf))
        return;
      assert (buf->write_ptr != buf->write_end);
      size_t to_copy = buf->write_end - buf->write_ptr;
      if (to_copy > count)
        to_copy = count;
      MEMCPY (buf->write_ptr, s, to_copy);
      buf->write_ptr += to_copy;
      s += to_copy;
      count -= to_copy;
    }
}

単に、バッファにコピーしているようですね。

それでは __printf_buffer_to_file_done 関数を見てみます。

__printf_buffer_to_file_done

stdio-common/printf_buffer_to_file.c
    :
int
__printf_buffer_to_file_done (struct __printf_buffer_to_file *buf)
{
  if (__printf_buffer_has_failed (&buf->base))
    return -1;
  __printf_buffer_flush_to_file (buf);
  return __printf_buffer_done (&buf->base);
}

__printf_buffer_flush_to_file の実体は同じファイルにあります。

stdio-common/printf_buffer_to_file.c
    :
void
__printf_buffer_flush_to_file (struct __printf_buffer_to_file *buf)
{
  /* The bytes in the buffer are always consumed.  */
  buf->base.written += buf->base.write_ptr - buf->base.write_base;

  if (buf->base.write_end == array_end (buf->stage))
    {
      /* If the stage buffer is used, make a copy into the file.  The
         stage buffer is always consumed fully, even if just partially
         written, to ensure that the file stream has all the data.  */
      size_t count = buf->base.write_ptr - buf->stage;
      if ((size_t) _IO_sputn (buf->fp, buf->stage, count) != count)
        {
          __printf_buffer_mark_failed (&buf->base);
          return;
        }
      /* buf->fp may have a buffer now.  */
      __printf_buffer_to_file_switch (buf);
      return;
    }
  else if (buf->base.write_end == buf->stage + 1)
    {
      /* Special one-character buffer case.  This is used to avoid
         flush-only overflow below.  */
      if (buf->base.write_ptr == buf->base.write_end)
        {
          if (__overflow (buf->fp, (unsigned char) *buf->stage) == EOF)
            {
              __printf_buffer_mark_failed (&buf->base);
              return;
            }
          __printf_buffer_to_file_switch (buf);
        }
      /* Else there is nothing to write.  */
      return;
    }

  /* We have written directly into the buf->fp buffer.  */
  assert (buf->base.write_end == buf->fp->_IO_write_end);

  /* Mark the bytes as written.  */
  buf->fp->_IO_write_ptr = buf->base.write_ptr;

  if (buf->base.write_ptr == buf->base.write_end)
    {
      /* The buffer in buf->fp has been filled.  This should just call
         __overflow (buf->fp, EOF), but flush-only overflow is obscure
         and not always correctly implemented.  See bug 28949.  Be
         conservative and switch to a one-character buffer instead, to
         obtain one more character for a regular __overflow call.  */
      buf->base.write_ptr = buf->stage;
      buf->base.write_end = buf->stage + 1;
    }
  /* The bytes in the file stream were already marked as written above.  */

  buf->base.write_base = buf->base.write_ptr;
}

__printf_buffer から始まる関数はバッファへの操作を行っているので出力に関わる関数を探すと _IO_sputn (buf->fp, buf->stage, count) の箇所が該当しそうですね。

_IO_sputn は libio/libioP.h で define されていましたが、定義されている先を少し記載しておきます。

libio/libioP.h
#define _IO_sputn(__fp, __s, __n) _IO_XSPUTN (__fp, __s, __n)
    ↓↓↓
#define _IO_XSPUTN(FP, DATA, N) JUMP2 (__xsputn, FP, DATA, N)
    ↓↓↓
#define JUMP2(FUNC, THIS, X1, X2) (_IO_JUMPS_FUNC(THIS)->FUNC) (THIS, X1, X2)
    ↓↓↓
    :

と、まぁこんな感じでしたので xsputn イベントが呼ばれるようで、出力先のファイルストリームによってテーブルを定義しているように見えます。

dup2 を使って出力先をリダイレクトしていない限り、printf の出力先のファイルストリーム は stdout になりますが、stdout の定義から、このテーブルの定義をたどってみたいと思います。

stdout の定義をたどる

libio/stdio.c
    :
#include "libioP.h"
#include "stdio.h"

#undef stdin
#undef stdout
#undef stderr
FILE *stdin = (FILE *) &_IO_2_1_stdin_;
FILE *stdout = (FILE *) &_IO_2_1_stdout_;
FILE *stderr = (FILE *) &_IO_2_1_stderr_;
libio/stdfiles.c
    :
#include "libioP.h"

#ifdef _IO_MTSAFE_IO
# define DEF_STDFILE(NAME, FD, CHAIN, FLAGS) \
  static _IO_lock_t _IO_stdfile_##FD##_lock = _IO_lock_initializer; \
  static struct _IO_wide_data _IO_wide_data_##FD \
    = { ._wide_vtable = &_IO_wfile_jumps }; \
  struct _IO_FILE_plus NAME \
    = {FILEBUF_LITERAL(CHAIN, FLAGS, FD, &_IO_wide_data_##FD), \
       &_IO_file_jumps};
#else
    :
#endif

DEF_STDFILE(_IO_2_1_stdin_, 0, 0, _IO_NO_WRITES);
DEF_STDFILE(_IO_2_1_stdout_, 1, &_IO_2_1_stdin_, _IO_NO_READS);
DEF_STDFILE(_IO_2_1_stderr_, 2, &_IO_2_1_stdout_, _IO_NO_READS+_IO_UNBUFFERED);

_IO_MTSAFE_IO は stdio がマルチスレッドに適用させる場合のコンパイルオプション見えましたが、Linux 用ですのでマルチスレッドに適用する、と判断して記載しています。

参考:https://sourceware.org/pipermail/libc-alpha/2017-March/079022.html

上記から各イベントでどのように実行されるか、が必要ですが、どうやら _IO_file_jumps を見れば良さそうです。

libio/libioP.h
extern const struct _IO_jump_t __io_vtables[] attribute_hidden;
    :
#define _IO_file_jumps                   (__io_vtables[IO_FILE_JUMPS])
libio/vtables.c
const struct _IO_jump_t __io_vtables[] attribute_relro =
{
  /* _IO_str_jumps  */
  [IO_STR_JUMPS] =
  {
     :
    中略
     :
  },
  /* _IO_wstr_jumps  */
  [IO_WSTR_JUMPS] = {
     :
    中略
     :
  },
  /* _IO_file_jumps  */
  [IO_FILE_JUMPS] = {
     :
    中略
     :
    JUMP_INIT (xsputn, _IO_file_xsputn),
     :
    中略
     :
  },

もう少し掘ってみる

というわけで _IO_file_xsputn を探します。

libio/fileops.c
    :
versioned_symbol (libc, _IO_new_file_xsputn, _IO_file_xsputn, GLIBC_2_1);

_IO_new_file_xsputn にシンボルが当たっているようですが、同じファイルにありました。

libio/fileops.c
size_t
_IO_new_file_xsputn (FILE *f, const void *data, size_t n)
{
  const char *s = (const char *) data;
  size_t to_do = n;
  int must_flush = 0;
  size_t count = 0;

  if (n <= 0)
    return 0;
  /* This is an optimized implementation.
     If the amount to be written straddles a block boundary
     (or the filebuf is unbuffered), use sys_write directly. */

  /* First figure out how much space is available in the buffer. */
  if ((f->_flags & _IO_LINE_BUF) && (f->_flags & _IO_CURRENTLY_PUTTING))
    {
      count = f->_IO_buf_end - f->_IO_write_ptr;
      if (count >= n)
	{
	  const char *p;
	  for (p = s + n; p > s; )
	    {
	      if (*--p == '\n')
		{
		  count = p - s + 1;
		  must_flush = 1;
		  break;
		}
	    }
	}
    }
  else if (f->_IO_write_end > f->_IO_write_ptr)
    count = f->_IO_write_end - f->_IO_write_ptr; /* Space available. */

  /* Then fill the buffer. */
  if (count > 0)
    {
      if (count > to_do)
	count = to_do;
      f->_IO_write_ptr = __mempcpy (f->_IO_write_ptr, s, count);
      s += count;
      to_do -= count;
    }
  if (to_do + must_flush > 0)
    {
      size_t block_size, do_write;
      /* Next flush the (full) buffer. */
      if (_IO_OVERFLOW (f, EOF) == EOF)
	/* If nothing else has to be written we must not signal the
	   caller that everything has been written.  */
	return to_do == 0 ? EOF : n - to_do;

      /* Try to maintain alignment: write a whole number of blocks.  */
      block_size = f->_IO_buf_end - f->_IO_buf_base;
      do_write = to_do - (block_size >= 128 ? to_do % block_size : 0);

      if (do_write)
	{
	  count = new_do_write (f, s, do_write);
	  to_do -= count;
	  if (count < do_write)
	    return n - to_do;
	}

      /* Now write out the remainder.  Normally, this will fit in the
	 buffer, but it's somewhat messier for line-buffered files,
	 so we let _IO_default_xsputn handle the general case. */
      if (to_do)
	to_do -= _IO_default_xsputn (f, s+do_write, to_do);
    }
  return n - to_do;
}

new_do_write が書き込みを行ってそうなのですが同じファイルにありました。

libio/fileops.c
    :
static size_t
new_do_write (FILE *fp, const char *data, size_t to_do)
{
  size_t count;
  if (fp->_flags & _IO_IS_APPENDING)
    /* On a system without a proper O_APPEND implementation,
       you would need to sys_seek(0, SEEK_END) here, but is
       not needed nor desirable for Unix- or Posix-like systems.
       Instead, just indicate that offset (before and after) is
       unpredictable. */
    fp->_offset = _IO_pos_BAD;
  else if (fp->_IO_read_end != fp->_IO_write_base)
    {
      off64_t new_pos
	= _IO_SYSSEEK (fp, fp->_IO_write_base - fp->_IO_read_end, 1);
      if (new_pos == _IO_pos_BAD)
	return 0;
      fp->_offset = new_pos;
    }
  count = _IO_SYSWRITE (fp, data, to_do);
  if (fp->_cur_column && count)
    fp->_cur_column = _IO_adjust_column (fp->_cur_column - 1, data, count) + 1;
  _IO_setg (fp, fp->_IO_buf_base, fp->_IO_buf_base, fp->_IO_buf_base);
  fp->_IO_write_base = fp->_IO_write_ptr = fp->_IO_buf_base;
  fp->_IO_write_end = (fp->_mode <= 0
		       && (fp->_flags & (_IO_LINE_BUF | _IO_UNBUFFERED))
		       ? fp->_IO_buf_base : fp->_IO_buf_end);
  return count;
}

_IO_SYSWRITE は先ほどの xputn といったものと同じようなものですね。

libio/libioP.h
#define _IO_SYSWRITE(FP, DATA, LEN) JUMP2 (__write, FP, DATA, LEN)

stdout の定義を改めて確認します。

libio/libioP.h
    :
    JUMP_FIELD(_IO_write_t, __write);
    :
libio/vtables.c
    :
    JUMP_INIT (write, _IO_new_file_write),
    :

_IO_new_file_write が実体っぽいですね。

libio/fileops.c
ssize_t
_IO_new_file_write (FILE *f, const void *data, ssize_t n)
{
  ssize_t to_do = n;
  while (to_do > 0)
    {
      ssize_t count = (__builtin_expect (f->_flags2
                                         & _IO_FLAGS2_NOTCANCEL, 0)
			   ? __write_nocancel (f->_fileno, data, to_do)
			   : __write (f->_fileno, data, to_do));
      if (count < 0)
	{
	  f->_flags |= _IO_ERR_SEEN;
	  break;
	}
      to_do -= count;
      data = (void *) ((char *) data + count);
    }
  n -= to_do;
  if (f->_offset >= 0)
    f->_offset += n;
  return n;
}

__write の実体はソース内に定義がなさそうでしたが、

sysdeps/unix/syscalls.list
    :
write		-	write		Ci:ibU	__libc_write	__write write
    :

と、いうわけで

予想通りというかなんというか printf がコールされた先は write のシステムコールでした。

ちょっと検算してみます。

冒頭の C をビルドして strace でトレースしてみますと…

wataru@ubuntu-24-04:~$ strace ./a.out
execve("./a.out", ["./a.out"], 0x7ffc36aabe10 /* 24 vars */) = 0
    :
   中略
    :
write(1, "Hello, world.\n", 14Hello, world.
)         = 14
exit_group(0)                           = ?
+++ exited with 0 +++

ファイルディスクリプタ 1(=stdout)write していましたね。

Ubuntu 24.04 LTS ベースで考えているのですが、tty で自身が居るターミナルが何処なのかがわかるので見てみますと、

wataru@ubuntu-24-04:~$ tty
/dev/pts/0

要するに、最初に上げた C のソースは

wataru@ubuntu-24-04:~$ echo -n -e "Hello, world.\n" > /dev/pts/0
Hello, world.

としても良いわけです。

カーネルを掘る

さて、今度はカーネルをほんの少し掘ってみます。

カーネルのバージョンは、

wataru@ubuntu-24-04:~$ uname -r
6.8.0-41-generic

6.8.0 ということで、

https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/snapshot/linux-6.8.tar.gz

から取得して展開します。

write から見る

CPU を x86_64 としましたので、システムコールについては、

arch/x86/entry/syscalls/syscall_64.tbl
    :
0	common	read			sys_read
1	common	write			sys_write
2	common	open			sys_open
3	common	close			sys_close
4	common	stat			sys_newstat
5	common	fstat			sys_newfstat
6	common	lstat			sys_newlstat
7	common	poll			sys_poll
    :

このファイルをビルドのときに展開して syscalls_64.h を作成して arch/x86/entry/syscall_64.c へ展開する、みたいな流れになっていたかと思いますが、カーネルのドライバは何を探せば良いでしょうか。

先ほどの tty コマンドの結果から /dev/pts で始まっているので UNIX98 PTY 形式のドライバから少しカーネルを掘っていきます。

カーネルから探す

UNIX98 PTY のドライバのソースは drivers/tty/pty.c にありました。

ここの write イベントのソースを見てみます。

drivers/tty/pty.c
    :
static ssize_t pty_write(struct tty_struct *tty, const u8 *buf, size_t c)
{
	struct tty_struct *to = tty->link;

	if (tty->flow.stopped || !c)
		return 0;

	return tty_insert_flip_string_and_push_buffer(to->port, buf, c);
}

drivers/tty/tty_buffer.c
    :
int tty_insert_flip_string_and_push_buffer(struct tty_port *port,
					   const u8 *chars, size_t size)
{
	struct tty_bufhead *buf = &port->buf;
	unsigned long flags;

	spin_lock_irqsave(&port->lock, flags);
	size = tty_insert_flip_string(port, chars, size);
	if (size)
		tty_flip_buffer_commit(buf->tail);
	spin_unlock_irqrestore(&port->lock, flags);

	queue_work(system_unbound_wq, &buf->work);

	return size;
}
    :

同じファイルでワークキューの登録を探すと、

drivers/tty/tty_buffer.c
    :
static void flush_to_ldisc(struct work_struct *work)
{
	struct tty_port *port = container_of(work, struct tty_port, buf.work);
	struct tty_bufhead *buf = &port->buf;

	mutex_lock(&buf->lock);

	while (1) {
		struct tty_buffer *head = buf->head;
		struct tty_buffer *next;
		size_t count, rcvd;

		/* Ldisc or user is trying to gain exclusive access */
		if (atomic_read(&buf->priority))
			break;

		/* paired w/ release in __tty_buffer_request_room();
		 * ensures commit value read is not stale if the head
		 * is advancing to the next buffer
		 */
		next = smp_load_acquire(&head->next);
		/* paired w/ release in __tty_buffer_request_room() or in
		 * tty_buffer_flush(); ensures we see the committed buffer data
		 */
		count = smp_load_acquire(&head->commit) - head->read;
		if (!count) {
			if (next == NULL)
				break;
			buf->head = next;
			tty_buffer_free(port, head);
			continue;
		}

		rcvd = receive_buf(port, head, count);
		head->read += rcvd;
		if (rcvd < count)
			lookahead_bufs(port, head);
		if (!rcvd)
			break;

		if (need_resched())
			cond_resched();
	}

	mutex_unlock(&buf->lock);

}
    :

結局 ldisc(ラインディシプリン)に引き渡していました。

https://docs.kernel.org/driver-api/tty/tty_ldisc.html を見ると、デフォルトの ldisc は N_TTY ですので、ソースは drivers/tty/n_tty.c と仮定して探しますと、

drivers/tty/n_tty.c
    :
static void n_tty_receive_buf(struct tty_struct *tty, const u8 *cp,
			      const u8 *fp, size_t count)
{
	n_tty_receive_buf_common(tty, cp, fp, count, false);
}
    ↓↓↓
static size_t
n_tty_receive_buf_common(struct tty_struct *tty, const u8 *cp, const u8 *fp,
			 size_t count, bool flow)
{
	struct n_tty_data *ldata = tty->disc_data;
	size_t n, rcvd = 0;
	int room, overflow;

	down_read(&tty->termios_rwsem);

	do {
		/*
		 * When PARMRK is set, each input char may take up to 3 chars
		 * in the read buf; reduce the buffer space avail by 3x
		 *
		 * If we are doing input canonicalization, and there are no
		 * pending newlines, let characters through without limit, so
		 * that erase characters will be handled.  Other excess
		 * characters will be beeped.
		 *
		 * paired with store in *_copy_from_read_buf() -- guarantees
		 * the consumer has loaded the data in read_buf up to the new
		 * read_tail (so this producer will not overwrite unread data)
		 */
		size_t tail = smp_load_acquire(&ldata->read_tail);

		room = N_TTY_BUF_SIZE - (ldata->read_head - tail);
		if (I_PARMRK(tty))
			room = DIV_ROUND_UP(room, 3);
		room--;
		if (room <= 0) {
			overflow = ldata->icanon && ldata->canon_head == tail;
			if (overflow && room < 0)
				ldata->read_head--;
			room = overflow;
			WRITE_ONCE(ldata->no_room, flow && !room);
		} else
			overflow = 0;

		n = min_t(size_t, count, room);
		if (!n)
			break;

		/* ignore parity errors if handling overflow */
		if (!overflow || !fp || *fp != TTY_PARITY)
			__receive_buf(tty, cp, fp, n);

		cp += n;
		if (fp)
			fp += n;
		count -= n;
		rcvd += n;
	} while (!test_bit(TTY_LDISC_CHANGING, &tty->flags));

	tty->receive_room = room;

	/* Unthrottle if handling overflow on pty */
	if (tty->driver->type == TTY_DRIVER_TYPE_PTY) {
		if (overflow) {
			tty_set_flow_change(tty, TTY_UNTHROTTLE_SAFE);
			tty_unthrottle_safe(tty);
			__tty_set_flow_change(tty, 0);
		}
	} else
		n_tty_check_throttle(tty);

	if (unlikely(ldata->no_room)) {
		/*
		 * Barrier here is to ensure to read the latest read_tail in
		 * chars_in_buffer() and to make sure that read_tail is not loaded
		 * before ldata->no_room is set.
		 */
		smp_mb();
		if (!chars_in_buffer(tty))
			n_tty_kick_worker(tty);
	}

	up_read(&tty->termios_rwsem);

	return rcvd;
}
    ↓↓↓
static void __receive_buf(struct tty_struct *tty, const u8 *cp, const u8 *fp,
			  size_t count)
{
	struct n_tty_data *ldata = tty->disc_data;
	bool preops = I_ISTRIP(tty) || (I_IUCLC(tty) && L_IEXTEN(tty));
	size_t la_count = min(ldata->lookahead_count, count);

	if (ldata->real_raw)
		n_tty_receive_buf_real_raw(tty, cp, count);
	else if (ldata->raw || (L_EXTPROC(tty) && !preops))
		n_tty_receive_buf_raw(tty, cp, fp, count);
	else if (tty->closing && !L_EXTPROC(tty)) {
		if (la_count > 0) {
			n_tty_receive_buf_closing(tty, cp, fp, la_count, true);
			cp += la_count;
			if (fp)
				fp += la_count;
			count -= la_count;
		}
		if (count > 0)
			n_tty_receive_buf_closing(tty, cp, fp, count, false);
	} else {
		if (la_count > 0) {
			n_tty_receive_buf_standard(tty, cp, fp, la_count, true);
			cp += la_count;
			if (fp)
				fp += la_count;
			count -= la_count;
		}
		if (count > 0)
			n_tty_receive_buf_standard(tty, cp, fp, count, false);

		flush_echoes(tty);
		if (tty->ops->flush_chars)
			tty->ops->flush_chars(tty);
	}

	ldata->lookahead_count -= la_count;

	if (ldata->icanon && !L_EXTPROC(tty))
		return;

	/* publish read_head to consumer */
	smp_store_release(&ldata->commit_head, ldata->read_head);

	if (read_cnt(ldata)) {
		kill_fasync(&tty->fasync, SIGIO, POLL_IN);
		wake_up_interruptible_poll(&tty->read_wait, EPOLLIN | EPOLLRDNORM);
	}
}
    ↓↓↓
static void flush_echoes(struct tty_struct *tty)
{
	struct n_tty_data *ldata = tty->disc_data;

	if ((!L_ECHO(tty) && !L_ECHONL(tty)) ||
	    ldata->echo_commit == ldata->echo_head)
		return;

	mutex_lock(&ldata->output_lock);
	ldata->echo_commit = ldata->echo_head;
	__process_echoes(tty);
	mutex_unlock(&ldata->output_lock);
}
    ↓↓↓
static size_t __process_echoes(struct tty_struct *tty)
{
	struct n_tty_data *ldata = tty->disc_data;
	int	space, old_space;
	size_t tail;
	u8 c;

	old_space = space = tty_write_room(tty);

	tail = ldata->echo_tail;
	while (MASK(ldata->echo_commit) != MASK(tail)) {
		c = echo_buf(ldata, tail);
		if (c == ECHO_OP_START) {
			int ret = n_tty_process_echo_ops(tty, &tail, space);
			if (ret == -ENODATA)
				goto not_yet_stored;
			if (ret < 0)
				break;
			space = ret;
		} else {
			if (O_OPOST(tty)) {
				int retval = do_output_char(c, tty, space);
				if (retval < 0)
					break;
				space -= retval;
			} else {
				if (!space)
					break;
				tty_put_char(tty, c);
				space -= 1;
			}
			tail += 1;
		}
	}

	/* If the echo buffer is nearly full (so that the possibility exists
	 * of echo overrun before the next commit), then discard enough
	 * data at the tail to prevent a subsequent overrun */
	while (ldata->echo_commit > tail &&
	       ldata->echo_commit - tail >= ECHO_DISCARD_WATERMARK) {
		if (echo_buf(ldata, tail) == ECHO_OP_START) {
			if (echo_buf(ldata, tail + 1) == ECHO_OP_ERASE_TAB)
				tail += 3;
			else
				tail += 2;
		} else
			tail++;
	}

 not_yet_stored:
	ldata->echo_tail = tail;
	return old_space - space;
}
    ↓↓↓
static int do_output_char(u8 c, struct tty_struct *tty, int space)
{
	struct n_tty_data *ldata = tty->disc_data;
	int	spaces;

	if (!space)
		return -1;

	switch (c) {
	case '\n':
		if (O_ONLRET(tty))
			ldata->column = 0;
		if (O_ONLCR(tty)) {
			if (space < 2)
				return -1;
			ldata->canon_column = ldata->column = 0;
			tty->ops->write(tty, "\r\n", 2);
			return 2;
		}
		ldata->canon_column = ldata->column;
		break;
	case '\r':
		if (O_ONOCR(tty) && ldata->column == 0)
			return 0;
		if (O_OCRNL(tty)) {
			c = '\n';
			if (O_ONLRET(tty))
				ldata->canon_column = ldata->column = 0;
			break;
		}
		ldata->canon_column = ldata->column = 0;
		break;
	case '\t':
		spaces = 8 - (ldata->column & 7);
		if (O_TABDLY(tty) == XTABS) {
			if (space < spaces)
				return -1;
			ldata->column += spaces;
			tty->ops->write(tty, "        ", spaces);
			return spaces;
		}
		ldata->column += spaces;
		break;
	case '\b':
		if (ldata->column > 0)
			ldata->column--;
		break;
	default:
		if (!iscntrl(c)) {
			if (O_OLCUC(tty))
				c = toupper(c);
			if (!is_continuation(c, tty))
				ldata->column++;
		}
		break;
	}

	tty_put_char(tty, c);
	return 1;
}

tty_put_char は drivers/tty/tty_io.c で定義されていました。

drivers/tty/tty_io.c
int tty_put_char(struct tty_struct *tty, u8 ch)
{
	if (tty->ops->put_char)
		return tty->ops->put_char(tty, ch);
	return tty->ops->write(tty, &ch, 1);
}
EXPORT_SYMBOL_GPL(tty_put_char);

put_char オペレーション関数は、大別すると以下の2つが多いかと思います。

  • シリアル出力であれば uart_put_char(drivers/tty/serial/serial_core.c)
  • VT100 様式コンソール出力であれば con_put_char(drivers/tty/vt/vt.c)

put_char オペレーションを掘る

今回は VT100 を確認します。

drivers/tty/vt/vt.c
static int con_put_char(struct tty_struct *tty, u8 ch)
{
	return do_con_write(tty, &ch, 1);
}
    ↓↓↓
static int do_con_write(struct tty_struct *tty, const u8 *buf, int count)
{
	struct vc_draw_region draw = {
		.x = -1,
	};
	int c, tc, n = 0;
	unsigned int currcons;
	struct vc_data *vc = tty->driver_data;
	struct vt_notifier_param param;
	bool rescan;

	if (in_interrupt())
		return count;

	console_lock();
	currcons = vc->vc_num;
	if (!vc_cons_allocated(currcons)) {
		/* could this happen? */
		pr_warn_once("con_write: tty %d not allocated\n", currcons+1);
		console_unlock();
		return 0;
	}

	/* undraw cursor first */
	if (con_is_fg(vc))
		hide_cursor(vc);

	param.vc = vc;

	while (!tty->flow.stopped && count) {
		u8 orig = *buf;
		buf++;
		n++;
		count--;
rescan_last_byte:
		c = orig;
		rescan = false;

		tc = vc_translate(vc, &c, &rescan);
		if (tc == -1)
			continue;

		param.c = tc;
		if (atomic_notifier_call_chain(&vt_notifier_list, VT_PREWRITE,
					&param) == NOTIFY_STOP)
			continue;

		if (vc_is_control(vc, tc, c)) {
			con_flush(vc, &draw);
			do_con_trol(tty, vc, orig);
			continue;
		}

		if (vc_con_write_normal(vc, tc, c, &draw) < 0)
			continue;

		if (rescan)
			goto rescan_last_byte;
	}
	con_flush(vc, &draw);
	console_conditional_schedule();
	notify_update(vc);
	console_unlock();
	return n;
}
    ↓↓↓
static void con_flush(struct vc_data *vc, struct vc_draw_region *draw)
{
	if (draw->x < 0)
		return;

	vc->vc_sw->con_putcs(vc, (u16 *)draw->from,
			(u16 *)draw->to - (u16 *)draw->from, vc->state.y,
			draw->x);
	draw->x = -1;
}

con_putcs についてはいくつか可能性があるのですが、ここではフレームバッファへ直接書き込んでいる fbcon_putcs(drivers/video/fbdev/core/fbcon.c)を見てみます。

drivers/video/fbdev/core/fbcon.c
static void fbcon_putcs(struct vc_data *vc, const u16 *s, unsigned int count,
			unsigned int ypos, unsigned int xpos)
{
	struct fb_info *info = fbcon_info_from_console(vc->vc_num);
	struct fbcon_display *p = &fb_display[vc->vc_num];
	struct fbcon_ops *ops = info->fbcon_par;

	if (!fbcon_is_inactive(vc, info))
		ops->putcs(vc, info, s, count, real_y(p, ypos), xpos,
			   get_color(vc, info, scr_readw(s), 1),
			   get_color(vc, info, scr_readw(s), 0));
}

ここから先はフレームバッファへのキャラクター書き込みオペレーション処理に行くのですが、フレームバッファの機能に応じて書き込みます。

キャラクター書き込み処理を掘る

一例で drivers/video/fbdev/core/bitblit.c にある bit_putcs を見てみます。

drivers/video/fbdev/core/bitblit.c
static void bit_putcs(struct vc_data *vc, struct fb_info *info,
		      const unsigned short *s, int count, int yy, int xx,
		      int fg, int bg)
{
	struct fb_image image;
	u32 width = DIV_ROUND_UP(vc->vc_font.width, 8);
	u32 cellsize = width * vc->vc_font.height;
	u32 maxcnt = info->pixmap.size/cellsize;
	u32 scan_align = info->pixmap.scan_align - 1;
	u32 buf_align = info->pixmap.buf_align - 1;
	u32 mod = vc->vc_font.width % 8, cnt, pitch, size;
	u32 attribute = get_attribute(info, scr_readw(s));
	u8 *dst, *buf = NULL;

	image.fg_color = fg;
	image.bg_color = bg;
	image.dx = xx * vc->vc_font.width;
	image.dy = yy * vc->vc_font.height;
	image.height = vc->vc_font.height;
	image.depth = 1;

	if (attribute) {
		buf = kmalloc(cellsize, GFP_ATOMIC);
		if (!buf)
			return;
	}

	while (count) {
		if (count > maxcnt)
			cnt = maxcnt;
		else
			cnt = count;

		image.width = vc->vc_font.width * cnt;
		pitch = DIV_ROUND_UP(image.width, 8) + scan_align;
		pitch &= ~scan_align;
		size = pitch * image.height + buf_align;
		size &= ~buf_align;
		dst = fb_get_buffer_offset(info, &info->pixmap, size);
		image.data = dst;

		if (!mod)
			bit_putcs_aligned(vc, info, s, attribute, cnt, pitch,
					  width, cellsize, &image, buf, dst);
		else
			bit_putcs_unaligned(vc, info, s, attribute, cnt,
					    pitch, width, cellsize, &image,
					    buf, dst);

		image.dx += cnt * vc->vc_font.width;
		count -= cnt;
		s += cnt;
	}

	/* buf is always NULL except when in monochrome mode, so in this case
	   it's a gain to check buf against NULL even though kfree() handles
	   NULL pointers just fine */
	if (unlikely(buf))
		kfree(buf);

}

この例ですと vc のメンバーに vc_font というメンバーも居たので、フォントのデータを取得しつつ、一文字ずつ整形して画像バッファへ送られるようです。[1]

コンソールの出力先は色々条件があるのでまだ中途半端ではありますが、掘るのはこれくらいにしておきます。

おわりに

今回は C で構築されたプログラムが printf 関数を実行する時の先を探検してみました。

printf 一つ取ってもなかなかの探検だったかと思います。

C言語ちょっと難し過ぎない?

C は複雑な実装を求められていて難し過ぎると思いますが、先人たちの叡智の結晶を有り難く頂戴し、今後もプログラミングに勤しみたいと思います。

アノテーション株式会社について

アノテーション株式会社はクラスメソッドグループのオペレーション専門特化企業です。

サポート・運用・開発保守・情シス・バックオフィスの専門チームが、最新 IT テクノロジー、高い技術力、蓄積されたノウハウをフル活用し、お客様の課題解決を行っています。

当社は様々な職種でメンバーを募集しています。

「オペレーション・エクセレンス」と「らしく働く、らしく生きる」を共に実現するカルチャー・しくみ・働き方にご興味がある方は、アノテーション株式会社 採用サイトをぜひご覧ください。

脚注
  1. ASCII 文字として考えていますが日本語が入ってくると色々変わってくるかもしれません ↩︎

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.