koturnの日記

転職したい社会人2年生の技術系日記

タブ番号,バッファ番号,ウィンドウ番号,ウィンドウIDの相互変換

はじめに

先日,「Vimで既に対象バッファを開いているウィンドウがあるとき,そのウィンドウに移動する」「Vimのタブ番号,ウィンドウ番号,ウィンドウID,バッファ番号の一覧情報を表示する」の2記事で,タブ番号,バッファ番号,ウィンドウ番号,ウィンドウIDを取り扱った. そして,Vim scriptの組み込み関数で,これらの相互変換を行った. しかし,単純に変換する関数が用意されているわけではなく,そこそこ工夫が必要だったので,この忘備録として,この記事にまとめようと思う.

Vimのタブ番号,ウィンドウ番号,ウィンドウID,バッファ番号について

Vimのタブ番号,ウィンドウ番号,ウィンドウID,バッファ番号について,簡単にまとめる.

種類 説明
タブ番号 :tabnew 等でオープンされたタブに振られる番号.左から1, 2, 3, ...と振られ,タブ消去,タブ移動を行うと振り直される
ウィンドウ番号 1つのタブにおけるウィンドウの番号.ウィンドウの移動,消去で振り直される
ウィンドウID タブに関係無く,1つのウィンドウに一意に割り当てられる番号.
バッファ番号 バッファに関連付けられた番号.バッファを削除しても振り直しは行われないため,IDとして扱うことも可能.

ウィンドウIDについては,Vim8.0からの機能であり,thincaさんのVim 8.0 Advent Calendar 7 日目 ウィンドウ IDという記事で解説されている.

相互変換表

本題の相互変換について,簡単に表にまとめた. 変換先が一意ではなく,複数あるものは「(複数)」と表記してある.

個別の詳細は,次の章で紹介する.

変換元 変換先 手法 制限
bufnr winnr(複数) buf_winnr() / win_findbuf()win_id2tabwin() から逆引き辞書作成 前者は同一タブ最初のウィンドウのみ
bufnr winid(複数) bufwinid() / win_findbuf() 前者は同一タブ最初のウィンドウのみ
bufnr tabnr(複数) tabpagebuflist() から逆引き辞書作成
winnr bufnr winbufnr() / win_getid()win_findbuf() 前者は同一タブのみ
winnr winid win_getid()
winnr tabnr 無意味であるため省略
winid bufnr win_findbuf() から逆引き辞書を作成
winid winnr win_id2tabwin()
winid tabnr win_id2win() / win_id2tabwin() 前者は同一タブのみ
tabnr bufnr(複数) tabpagebuflist()
tabnr winnr(複数) tabpagewinnr()
tabnr winid(複数) tabpagewinnr()win_getid()

個々の変換手法

bufnr から winnr (複数)

指定したバッファ番号のバッファを開いているウィンドウの番号を得る.

buf_winnr() だと同一タブ内における若い番号のウィンドウ番号を1つしか取得することができない. 複数個取得しようと思うと, win_findbuf()win_id2tabwin() で逆引き辞書を作成しておく必要がある.

まず,ウィンドウ番号は個々のタブで1から始まるので,インプットとしてはバッファ番号の他にタブ番号も受け取るものとする. (タブ番号は省略可能で,省略時はカレントタブの番号を受け取ったものとする)

function! s:create_bufnr2tabwin_dict() abort " {{{
  let bufnr2tabwin_dict = {}
  for bnr in filter(range(1, bufnr('$')), 'bufexists(v:val)')
    let bufnr2tabwin_dict[bnr] = map(win_findbuf(bnr), 'win_id2tabwin(v:val)')
  endfor
  return bufnr2tabwin_dict
endfunction " }}}

function! s:bufnr2winnr(bnr, ...) abort " {{{
  if bufexists(a:bnr)
    throw 'E86 Buffer ' . a:bnr . 'does not exist'
  endif
  let tnr = a:0 > 0 ? a:1 : tabpagenr()
  return map(filter(s:create_bufnr2tabwin_dict()[a:bnr], 'v:val[0] == tnr'), 'v:val[1]')
endfunction " }}}

bufnr から winid (複数)

指定したバッファ番号のバッファを開いているウィンドウのIDを得る.

win_findbuf() を呼び出すだけでよい.

function! s:bufnr2winid(bnr) abort " {{{
  return win_findbuf(a:bnr)
endfunction " }}}

bufnr から tabnr (複数)

指定したバッファ番号のバッファを開いているウィンドウが存在するタブページの番号を得る.

tabpagebuflist() でtabnrからbufnrのリストが得られるので,逆引き辞書を作成する.

function! s:create_bufnr2tabnr_dict() abort " {{{
  let bufnr2tabnr_dict = {}
  for tnr in range(1, tabpagenr('$'))
    for bnr in tabpagebuflist(tnr)
      let bufnr2tabnr_dict[bnr] = has_key(bufnr2tabnr_dict, bnr) ? add(bufnr2tabnr_dict[bnr], tnr) : [tnr]
    endfor
  endfor
  for val in values(bufnr2tabnr_dict)
    call uniq(sort(val))
  endfor
  return bufnr2tabnr_dict
endfunction " }}}

function! s:bufnr2tabnr(bnr) abort " {{{
  return s:create_bufnr2tabnr_dict()[a:bnr]
function " }}}

bufnr から winnrの事例のように,win_findbuf()win_id2tabwin() で逆引き辞書を作ってもよい.

function! s:create_bufnr2tabwin_dict() abort " {{{
  let bufnr2tabwin_dict = {}
  for bnr in filter(range(1, bufnr('$')), 'bufexists(v:val)')
    let bufnr2tabwin_dict[bnr] = map(win_findbuf(bnr), 'win_id2tabwin(v:val)')
  endfor
  return bufnr2tabwin_dict
endfunction " }}}

function! s:bufnr2winnr02(bnr) abort " {{{
  if bufexists(a:bnr)
    throw 'E86 Buffer ' . a:bnr . 'does not exist'
  endif
  return map(s:create_bufnr2tabwin_dict()[a:bnr], 'v:val[0]')
endfunction " }}}

winnr から bufnr

指定したウィンドウ番号のウィンドウが開いているバッファの番号を得る.

まず,ウィンドウ番号からウィンドウIDに変換し,後述するウィンドウIDからバッファ番号の辞書を作成する手法を利用する.

function! s:create_winid2bufnr_dict() abort " {{{
  let winid2bufnr_dict = {}
  for bnr in range(1, bufnr('$'))
    for wid in win_findbuf(bnr)
      let winid2bufnr_dict[wid] = bnr
    endfor
  endfor
  return winid2bufnr_dict
endfunction " }}}

function! s:winid2tabnr(wid, ...) abort " {{{
  return a:0 > 0 ? s:create_winid2bufnr_dict()[win_getid(a:wid, tnr)] : winbufnr(a:wid)
endfunction " }}}

winnr から winid

指定したウィンドウ番号のウィンドウIDを得る.

当然,タブを指定する必要はあるよねという話. win_getid() を呼び出すだけ(タブ番号を省略すると,カレントタブのウィンドウ番号からウィンドウIDを引くことになる). 関数を呼び出すだけなので,自作関数を定義する必要はないが,とりあえず作っておく.

function! s:winnr2winid(wnr, ...) abort " {{{
  return a:0 > 0 ? win_getid(a:wnr, a:1) : win_getid(a:wnr)
endfunction " }}}

winid から tabnr

例えば,「番号2のウィンドウ」を持つタブのリストを作成することはできるが意味が無いので割愛. それは,2個以上のウィンドウに分割されているタブの検索にしかならない.

winid から bufnr

指定したウィンドウIDのウィンドウで開かれているバッファの番号を得る.

直接取得できる関数は無いため, winid2bufnr_dict() を利用して,winidからbufnrの逆引き辞書を作成する. (ループして一致したらreturnというスタンスでもよいが,他への転用がしんどいので辞書を作成する)

function! s:create_winid2bufnr_dict() abort " {{{
  let winid2bufnr_dict = {}
  for bnr in range(1, bufnr('$'))
    for wid in win_findbuf(bnr)
      let winid2bufnr_dict[wid] = bnr
    endfor
  endfor
  return winid2bufnr_dict
endfunction " }}}

function! s:winid2bufnr(wid) abort " {{{
  return s:create_winid2bufnr_dict()[a:wid]
endfunction " }}}

winid から winnr

指定したウィンドウIDのウィンドウの番号を得る.

win_id2tabwin() を呼び出すだけでよい.

function! s:winid2tabnr(wid) abort " {{{
  return win_id2tabwin(a:wid)[1]
endfunction " }}}

winid から tabnr

指定したウィンドウIDのウィンドウを持つタブページの番号を得る.

win_id2win() は同一タブ内のウィンドウしか検索対象にしないため,1つ前と同じく win_id2tabwin() を利用する. そもそも,ウィンドウ番号はタブと共に無いとあまり意味がないので, win_id2tabwin() の結果を個々に取得するのは微妙だろう.

function! s:winid2tabnr(wid) abort " {{{
  return win_id2tabwin(a:wid)[0]
endfunction " }}}

tabnr から bufnr (複数)

指定したタブページ番号ののバッファ番号のリストを得る.

単に関数を呼び出すだけでよい.

function! s:tabnr2bufnr_list(tnr) abort " {{{
  return tabpagebuflist(a:tnr)
endfunction " }}}

tabnr から winnr (複数)

指定したタブページ番号のウィンドウ番号のリストを得る.

ウィンドウIDはウィンドウ削除時に振り直されるため,必ず1, 2, 3, ...という風に整数が連続するので,最後のウィンドウ番号を得るとよい.

function! s:tabnr2winid_list(tnr) abort " {{{
  return range(1, tabpagewinnr(a:tnr, '$'))
endfunction " }}}

tabnr から winid (複数)

指定したタブページ番号のウィンドウIDのリストを得る.

function! s:tabnr2winid_list(tnr) abort " {{{
  return map(range(1, tabpagewinnr(a:tnr, '$')), 'win_getid(v:val, a:tnr)')
endfunction " }}}

まとめ

個人的なメモとして,タブ番号,バッファ番号,ウィンドウ番号,ウィンドウIDの相互変換についてまとめた. 今後,.vimrcに書く設定やプラグインの作成に活かすことがあるかもしれない.

Vimのタブ番号,ウィンドウ番号,ウィンドウID,バッファ番号の一覧情報を表示する

はじめに

先日の記事Vimで既に対象バッファを開いているウィンドウがあるとき,そのウィンドウに移動するでは,「タブ開きすぎると,どのタブでどのバッファ開いているかわからなくなるから,同じバッファを複数のウィンドウで開いてしまう」というIQ1的な事柄を問題にし,それを解決するコマンドを提示した. しかし,そもそも全てのタブ配下の全てのウィンドウが,どのバッファに関連付けられているかの情報をパパッと見ることができれば,そういう問題も少しは減るはずだ. この記事では,全てのタブが管理している情報を表示するコマンドを提案する.

実装

何はともあれ実装である.このような実装を考えた. tabpagewinnr(タブ番号, '$') で対象タブにおけるウィンドウの個数が得られるので,あとは個数分イテレーションを行い, win_getid(ウィンドウ番号, タブ番号) とすることで,ウィンドウIDが得られる寸法だ. Window IDは一意であるため,そこからバッファ番号を得るとよい. ただし,Window IDからバッファ番号を得る関数は無く,その逆の win_findbuf() という関数があるため,逆引き辞書を作る.

" Window IDからバッファ番号を引く逆引き辞書を作成
function! s:create_winid2bufnr_dict() abort " {{{
  let winid2bufnr_dict = {}
  for bnr in filter(range(1, bufnr('$')), 'v:val')
    for wid in win_findbuf(bnr)
      let winid2bufnr_dict[wid] = bnr
    endfor
  endfor
  return winid2bufnr_dict
endfunction " }}}

function! s:show_tab_info() abort " {{{
  echo "====== Tab Page Info ======"
  let current_tnr = tabpagenr()
  let winid2bufnr_dict = s:create_winid2bufnr_dict()
  for tnr in range(1, tabpagenr('$'))
    let current_winnr = tabpagewinnr(tnr)
    echo (tnr == current_tnr ? '>' : ' ') 'Tab:' tnr
    echo '    Buffer number | Window Number | Window ID | Buffer Name'
    for wininfo in map(map(range(1, tabpagewinnr(tnr, '$')), '{"wnr": v:val, "wid": win_getid(v:val, tnr)}'), 'extend(v:val, {"bnr": winid2bufnr_dict[v:val.wid]})')
      echo '   ' (wininfo.wnr == current_winnr ? '*' : ' ') printf('%11d | %13d | %9d | %s', wininfo.bnr, wininfo.wnr, wininfo.wid, bufname(wininfo.bnr))
    endfor
  endfor
endfunction " }}}
command! -bar TabInfo call s:show_tab_info()

表示イメージとしては以下の通り. カレントタブの左に > を表示し,それぞれのタブにおけるカレントウィンドウの左には * を表示している.

f:id:koturn:20180212141845p:plain

カレントタブに関する情報欄に :echohl で色を付けるなどすると,見やすさが向上するかもしれない.

NG実装

これはNG実装なので,真似してはいけない. 問題点としては,各タブの情報収集において,対象タブへの移動を伴っている点であり,行数の多いバッファと関連付いたウィンドウをカレントウィンドウに持つタブに移動したときに,数秒固まってしまう.

function! s:show_tab_info() abort " {{{
  echo "====== Tab Page Info ======"
  let current_tnr = tabpagenr()
  for tnr in range(1, tabpagenr('$'))
    execute 'tabnext' tnr
    echo (tnr == current_tnr ? '>' : ' ') 'Tab:' tnr
    echo '    Buffer number | Window Number | Window ID | Buffer Name'
    let current_winnr = winnr()
    for wnr in range(1, winnr('$'))
      echo '   ' (wnr == current_winnr ? '*' : ' ') printf('%11d | %13d | %9d | %s', winbufnr(wnr), wnr, win_getid(wnr), bufname(winbufnr(wnr)))
    endfor
  endfor
  execute 'tabnext' current_tnr
endfunction " }}}
command! -bar TabInfo call s:show_tab_info()

まとめ

全てのタブが管理している全てのウィンドウの番号とID,およびそれらのウィンドウに関連付けられているバッファの番号とバッファ名を表示するコマンドを提案した. それらの情報を見ることができれば,無駄に同じバッファを複数のウィンドウ(特に異なるタブ間において)開き直すことは無くなるだろう.

Vimで既に対象バッファを開いているウィンドウがあるとき,そのウィンドウに移動する

はじめに

僕はVimのタブ機能をそこそこ活用する方である. だが,「このバッファは既に開いている」ということを忘れることが多々あり,複数のタブで同じバッファを開くことがある.

Vim8.0になり, win_gotoid() というVim scriptの関数が追加された. これは,指定したIDのウィンドウにフォーマスを当てるという関数であり,まさに「Vimで既に対象バッファを開いているウィンドウがあるとき,そのウィンドウに移動する」という機能に持ってこいである.

そこで,そういった機能を実現するコマンドを考えてみた.

実装

何はともあれ,実装は以下のようになる. win_gotoid() はVim8.0からの機能であるため,Vim7.3, Vim7.4向けの実装も用意しておく.

if exists('*win_gotoid')
  function! s:buf_open_existing(qmods, bname) abort " {{{
    let bnr = bufnr(a:bname)
    if bnr == -1
      echoerr 'Buffer not found:' a:bname
      return
    endif
    let wids = win_findbuf(bnr)
    if empty(wids)
      execute a:qmods 'new'
      execute 'buffer' bnr
    else
      call win_gotoid(wids[0])
    endif
  endfunction " }}}
  command! -bar -nargs=1 -complete=buffer Buffer  call s:buf_open_existing(<q-mods>, <f-args>)
else
  function! s:buf_open_existing(bname) abort " {{{
    let bnr = bufnr(a:bname)
    if bnr == -1
      echoerr 'Buffer not found:' a:bname
      return
    endif
    let tindice = map(filter(map(range(1, tabpagenr('$')), '{"tindex": v:val, "blist": tabpagebuflist(v:val)}'), 'index(v:val.blist, bnr) != -1'), 'v:val.tindex')
    if empty(tindice)
      new
      execute 'buffer' bnr
    else
      execute 'tabnext' tindice[0]
      execute bufwinnr(bnr) 'wincmd w'
    endif
  endfunction " }}}
  command! -bar -nargs=1 -complete=buffer Buffer  call s:buf_open_existing(<f-args>)
endif

コマンドとしては,以下のように使用する. <バッファ名> はTabキーで補完可能である.

:Buffer <バッファ名>

バッファが :hide 等で隠れている場合は,新たにウィンドウを作成して,開き直す. Vim8.0からは,Exコマンド定義において <mod> が利用できるようになったため, :topleft:botright と併用することも可能になっている.

ターミナルへの応用

Vim8.0からは :terminal が実装された. この :terminal についても,複数タブを開いている場合,必要ないのに新たにターミナルを立ち上げてしまうかもしれない. そこで,前述と同様,ターミナルが既に起動していれば,そこにフォーカスを当てるコマンドを考えた.

実は,バンビちゃん氏のVim で :terminal の使い勝手をよくしたという記事に影響を受けている.

if has('terminal')
  function! s:complete_term_bufname(arglead, cmdline, cursorpos) abort " {{{
    let arglead = tolower(a:arglead)
    return filter(map(term_list(), 'bufname(v:val)'), '!stridx(tolower(v:val), arglead)')
  endfunction " }}}

  function! s:term_open_existing(qmods, ...) abort " {{{
    if a:0 == 0
      let bnrs = term_list()
      if empty(bnrs)
        execute a:qmods 'terminal'
      else
        let wids = win_findbuf(bnrs[0])
        if empty(wids)
          terminal
        else
          call win_gotoid(wids[0])
        endif
      endif
    else
      let bnr = bufnr(a:1)
      if bnr == -1
        throw 'E94: No matching buffer for ' . a:1
      elseif index(term_list(), bnr) == -1
        throw a:1 . ' is not a terminal buffer'
      endif
      let wids = win_findbuf(bnr)
      if empty(wids)
        execute a:qmods term_getsize(bnr)[0] 'new'
        execute 'buffer' bnr
      else
        call win_gotoid(wids[0])
      endif
    endif
  endfunction " }}}
  command! -bar -nargs=? -complete=customlist,s:complete_term_bufname Terminal  call s:term_open_existing(<q-mods>, <f-args>)
endif

このコマンドはターミナルバッファ名を引数に取り,ターミナルであるバッファのみをTab補完候補に出す.

:Terminal <ターミナルバッファ名>

まとめ

「既に開いているバッファがあるなら,そこに移動する」という機能は win_gotoid() を利用すれば簡単に実装できる.

参考文献

VimのCtrl-X補完を使えるようになりたい

はじめに

Vimにはデフォルトで補完機能が備わっている. 実践Vim 思考のスピードで編集しよう!であったり,daisuzuさんの2015年のVim Advent Calendarの記事VimのCTRL-X補完について - daisuzu's notesを読むと,neocompletedeopleteに頼るだけではなく,デフォルトの補完機能を活用したいと考えるようになるものである.

しかし, ins-completion を見るとわかるように,Vim<C-x> から始まる補完は12種類もあり,初めのうちはとても覚えきれるものではない. そこで,この記事では, <C-x> 補完初心者が使いこなせるようになるまでのヒント表示キーマッピングを考えた.

ins-completion

まずは,補完にはどのような種類があるかを簡単にまとめる. 以下の表は ins-completion の内容と同じなので,先程見た人は無視してよい.

キーマッピング 補完 必須オプション
<C-X><C-l> 行全体
<C-X><C-n>, <C-X><C-n> 現在のファイルのキーワード
<C-X><C-k> 'dictionary' のキーワード 'dictionary'
<C-X><C-t> 'thesaurus' のキーワード 'thesaurus'
<C-X><C-i> 編集中と外部参照(インクルード)しているファイルのキーワード
<C-X><C-]> タグファイル('tags' で設定したパスで見つかるファイル)
<C-X><C-f> ファイル名
<C-X><C-d> 定義もしくはマクロ
<C-X><C-v> Vimコマンドライン
<C-X><C-u> ユーザ定義補完 'completefunc'
<C-X><C-o> オムニ補完 'omnifunc'
<C-X>s, <C-X><C-s> スペリング補完 'spell'
<C-n>, <C-p> 'complete' のキーワード

この内, <C-n>, <C-p><C-x> の先行入力を伴わないので,ヒント表示の対象から外す.

ヒント表示キーマッピング

とにかく,以下のようにしただけという話.

" 入力キーの辞書
let s:compl_key_dict = {
      \ char2nr("\<C-l>"): "\<C-x>\<C-l>",
      \ char2nr("\<C-n>"): "\<C-x>\<C-n>",
      \ char2nr("\<C-p>"): "\<C-x>\<C-p>",
      \ char2nr("\<C-k>"): "\<C-x>\<C-k>",
      \ char2nr("\<C-t>"): "\<C-x>\<C-t>",
      \ char2nr("\<C-i>"): "\<C-x>\<C-i>",
      \ char2nr("\<C-]>"): "\<C-x>\<C-]>",
      \ char2nr("\<C-f>"): "\<C-x>\<C-f>",
      \ char2nr("\<C-d>"): "\<C-x>\<C-d>",
      \ char2nr("\<C-v>"): "\<C-x>\<C-v>",
      \ char2nr("\<C-u>"): "\<C-x>\<C-u>",
      \ char2nr("\<C-o>"): "\<C-x>\<C-o>",
      \ char2nr('s'): "\<C-x>s",
      \ char2nr("\<C-s>"): "\<C-x>s"
      \}
" 表示メッセージ
let s:hint_i_ctrl_x_msg = join([
      \ '<C-l>: While lines',
      \ '<C-n>: keywords in the current file',
      \ "<C-k>: keywords in 'dictionary'",
      \ "<C-t>: keywords in 'thesaurus'",
      \ '<C-i>: keywords in the current and included files',
      \ '<C-]>: tags',
      \ '<C-f>: file names',
      \ '<C-d>: definitions or macros',
      \ '<C-v>: Vim command-line',
      \ "<C-u>: User defined completion ('completefunc')",
      \ "<C-o>: omni completion ('omnifunc')",
      \ "s: Spelling suggestions ('spell')"
      \], "\n")
function! s:hint_i_ctrl_x() abort
  echo s:hint_i_ctrl_x_msg
  let c = getchar()
  return get(s:compl_key_dict, c, nr2char(c))
endfunction

inoremap <expr> <C-x>  <SID>hint_i_ctrl_x()

途中で :echo した上でキー入力待ち状態を作るには, <expr> を活用するのがよい. 上記のキーマッピングを行うと,以下のようになる.

f:id:koturn:20180210164917g:plain

これで,補完を行おうとしたときに「あの補完はどの <C-x> の後にどのキーを押下すればよかったんだっけ?」とならず,とりあえず <C-x> を押下してみて,「お~このキーであの補完ができるんだった!」となるのではないかと思う. もちろん,これは <C-x> 補完に不慣れな人向けのキーマッピングである. いちいち,複数行の :echo が表示されるのは,常用するレベルになると鬱陶しいことこの上ないだろう. 慣れるまではヒント表示を使い,慣れた頃に .vimrc から削除するのがよいと思われる.

おまけ

本記事におけるヒントの表示は,レジスタ内容やマーク位置の表示にも応用できるのではないかと思い,以下のキーマッピングを考えた.

function! s:hint_cmd_output(prefix, cmd) abort
  redir => str
    :execute a:cmd
  redir END
  echo str
  return a:prefix . nr2char(getchar())
endfunction

" カーソル位置のマーク
nnoremap <expr> m  <SID>hint_cmd_output('m', 'marks')
" マーク位置へのジャンプ
nnoremap <expr> `  <SID>hint_cmd_output('`', 'marks')
" マーク位置へのジャンプ
nnoremap <expr> '  <SID>hint_cmd_output("'", 'marks')
" レジスタ参照(ヤンクや削除)
nnoremap <expr> "  <SID>hint_cmd_output('"', 'registers')
" マクロ記録
nnoremap <expr> q  <SID>hint_cmd_output('q', 'registers')
" マクロ再生
nnoremap <expr> @  <SID>hint_cmd_output('@', 'registers')

これらは次のようにヒントを表示する.

まずは,マークの例.

f:id:koturn:20180210164931g:plain

次に,ヤンクや削除時のレジスタ指定. 4"ayy のような行数指定を行っても問題ない.

f:id:koturn:20180210164938g:plain

まとめ

この記事では,VimのCtrl-X補完を使いこなせるようになるための初心者練習用ヒント表示のキーマッピングを紹介した. また,ヒント表示を応用して,マークやレジスタの内容を表示するキーマッピングも紹介した. あくまで初心者向けのキーマッピングであり,熟達するにつれて,ヒント表示がだんだん鬱陶しくなると思う. その頃に,この記事で紹介したキーマッピング.vimrc から削除し,よりスパルタンを目指すとよいだろう.

参考文献

IQ1を支えるコーディング術

この記事はIQが1Advent Calendarの10日目の記事になります. 昨日はMew_1406さんのIQ1と謝罪行脚と題された,怖いお話でしたね.

はじめに

ご存知の通り,僕はIQ1です. IQ1には様々な困難が存在します.

例えば,物が覚えられない.... 僕はプログラムを書くことがあるのですが,基本的なイディオムや標準ライブラリの関数の使い方等を覚えられず,このために苦戦することがあります.

そこで,この記事ではIQ1でも多少楽にプログラムを書く手法を紹介したいと思います. 特に,僕が書くことのあるC++に焦点を当てたいと思います.

スニペット展開

基本的にほとんどのエディタには「スニペット展開」という機能が標準,あるいはプラグインという形で利用可能です. スニペット展開とは何か,それは以下のGIF画像を見てもらうのが早いでしょう.

f:id:koturn:20171210023943g:plain

このように,事前に登録しておいたコードのテンプレートを挿入するのが,コードスニペットの展開というものになります. これは単なるコーディング速度の向上だけでなく,僕のようなIQ1にとっては記憶補助にもなるわけです.

前提

僕は普段テキストエディタとしてVimを使っています. ですので,この記事において,特に断りがない限り,Vimを使っていることを前提とします. また,Vimスニペット展開を行うプラグインとして,Shougo/neosnippet.vimを使うこととします

ただ,スニペット展開自体は,前述の通り,今頃のテキストエディタにもあるものなので,Vimはあくまで一例としてください.

neosnippetの導入

Vimneosnippet.vimを前提とするので,最小限のインストール方法について記載します.

プラグインマネージャに,Shougo/dein.vimを使っているなら,.vimrcに以下のような記述を加えると,使用できるようになるはずです. neosnippet.vimは補完プラグインとして,Shougo/deoplete.nvimShougo/neocomplete.vimを導入しておくと,ベンリさが100倍になるので,一緒に入れておきましょう. Neovim,Vim8(Windows除く)の場合はShougo/deoplete.nvim,Vim7やWindowsの場合はneocomplete.vimを導入する設定になっています. (Windowsではdeopleteの補完が遅いと聞いたので除外)

set encoding=utf-8

if has('vim_starting')
  let s:deindir = expand('~/.cache/dein')
  let s:deinlocal = s:deindir . '/repos/github.com/Shougo/dein.vim'
  let &runtimepath = s:deinlocal . ',' . &runtimepath
endif

if dein#load_state(s:deindir)
  call dein#begin(s:deindir)
  call dein#add('Shougo/dein.vim')
  if has('nvim') || !has('win32') && v:version >= 704
    call dein#add('Shougo/deoplete.nvim', {
        \ 'on_event': 'InsertEnter',
        \})
    if !has('nvim')
      call dein#add('roxma/nvim-yarp')
      call dein#add('roxma/vim-hug-neovim-rpc')
    endif
  elseif v:version > 703 || (v:version == 703 && has('patch885'))
    if has('lua')
      call dein#add('Shougo/neocomplete.vim', {
          \ 'on_event': 'InsertEnter',
          \ 'on_cmd': [
          \   'NeoCompleteEnable',
          \   'NeoCompleteDisable',
          \   'NeoCompleteLock',
          \   'NeoCompleteUnlock',
          \   'NeoCompleteToggle',
          \   'NeoCompleteSetFileType',
          \   'NeoCompleteClean',
          \   'NeoCompleteBufferMakeCache',
          \   'NeoCompleteDictionaryMakeCache',
          \   'NeoCompleteSyntaxMakeCache',
          \   'NeoCompleteTagMakeCache'
          \ ]
          \})
    else
      call dein#add('Shougo/neocomplcache', {
            \ 'on_event': 'InsertEnter',
            \ 'on_cmd': [
            \   'NeoComplCacheEnable',
            \   'NeoComplCacheDisable',
            \   'NeoComplCacheLock',
            \   'NeoComplCacheUnlock',
            \   'NeoComplCacheToggle',
            \   'NeoComplCacheLockSource',
            \   'NeoComplCacheUnlockSource',
            \   (v:version >= 703 ? 'NeoComplCacheSetFileType' : 'NeoComplCacheSetFileType'),
            \   'NeoComplCacheSetFileType',
            \   'NeoComplCacheClean',
            \ ],
            \ 'on_map': [['is', '<Plug>(neocomplcache_snippets_']]
            \})
    endif
  endif
  call dein#add('Shougo/neosnippet', {
        \ 'on_event': 'InsertEnter',
        \ 'on_cmd': [
        \   'NeoSnippetEdit',
        \   'NeoSnippetMakeCache',
        \   'NeoSnippetSource',
        \   'NeoSnippetClearMarkers'
        \ ],
        \ 'on_ft': 'neosnippet',
        \ 'on_map': [['nisx', '<Plug>(neosnippet_']],
        \})
  call dein#add('Shougo/neosnippet-snippets')
  call dein#end()
  call dein#save_state()
endif

if dein#tap('deoplete.nvim')
  let g:deoplete#enable_at_startup = 1
endif

if dein#tap('neocomplete.vim')
  let g:neocomplete#enable_at_startup = 1
endif

if dein#tap('neocomplcache')
  let g:neocomplcache_enable_at_startup = 1
endif

if dein#tap('neosnippet')
  imap <C-k>  <Plug>(neosnippet_expand_or_jump)
  smap <C-k>  <Plug>(neosnippet_expand_or_jump)
  imap <expr><TAB>  neosnippet#expandable() <Bar><Bar> neosnippet#jumpable() ?
        \ "\<Plug>(neosnippet_expand_or_jump)" : pumvisible() ? "\<C-n>" : "\<TAB>"
  smap <expr><TAB>  neosnippet#expandable() <Bar><Bar> neosnippet#jumpable() ?
        \ "\<Plug>(neosnippet_expand_or_jump)" : "\<TAB>"
  let g:neosnippet#snippets_directory = '~/.vim/neosnippets'
  let g:neosnippet#expand_word_boundary = 1
endif

filetype plugin indent on
syntax enable

動作確認

まず,

$ vim main.cpp

として,Vimを起動し,インサートモードに入り,main<C-k>としてみましょう. 以下のgifアニメのようになれば問題ありません.

f:id:koturn:20171210023827g:plain

なお,このgifアニメでは,前述の最低限の .vimrc を用いたときのスクリーンキャストを貼っていますが,これ以降は僕が普段使用している .vimrc でのスクリーンキャストを貼り付けます.

自分でスニペットを定義する

さて,ここまでで,Vimスニペットプラグインを導入し,デフォルトのスニペットを展開することをしました. しかし,スニペットとは自分で追加していくもの...ここでは僕が実際に普段使っているC++スニペットを紹介することにしましょう.

neosnippetのファイル配置

今回は ~/.vim/neosnippets/スニペットファイルを置きます. 前述の設定例でも,このパスを指定していますね.

C++スニペットの場合は, cpp.snip というファイル名にしなければなりません. (つまり,フルパスは ~/.vim/neosnippets/cpp.snip

neosnippetのスニペット定義について

:h neosnippet-snippet-syntax を見るのが早いですが,簡単に.

snippet [[スニペットのトリガー]]
abbr [[deoplet等の補完時に出てくる説明 (省略可)]]
alias [[別のトリガーを指定可能 (省略可)]]
regex [[ここに記述した正規表現にマッチしている場合にのみ展開可能 (省略可)]]
option [[説明が面倒なので,ヘルプを見て]]
  [[1段階インデントした位置にスニペット本体を記述]]

つまり,最低限のスニペットの例は以下のようになります.

snippet rbf
  for (auto&& ${1:e} : ${2:#:container}) {
    ${0}
  }

さて,よくわからない記述が出てきましたね. ${数字}<C-k> を押下する度にカーソルの移動する位置を表していて,そのときのデフォルトの展開結果を指定してたりします. ${0}は最終的にカーソルが移動する位置, ${1:e}e をデフォルトの展開結果(デフォルトで e が挿入される)とした,1番目のカーソルの移動位置, ${2:#:container} は2番目のカーソル移動位置で, container と表示はするものの,${1:e} と違って実際にテキスト挿入を行わないもの(コメント的なもの)となっています. 単に ${1} と書いた場合,デフォルト値が無いカーソル移動位置となります.

まぁ,詳細は :h neosnippet-snippet-syntax を見るなり,Shougo/neosnippet-snippetsの例を見てください.

main()関数

まずは main() からだよね,ということで,スニペットを作ってみます. まぁ,これはデフォルトのスニペットでも定義されているのですが,細かい部分で好みに合わないので,書き換えます.

C++のmain関数は

int
main(int argc, const char* argv[])
{

  return EXIT_SUCCESS;
}

って感じで書くので,単純に以下のようにスニペット化します. デフォルトで定義されているのを無効化するために,直前にdelete mainを書きます.

snippet main
  int
  main(${1:int argc, const char* argv[]})
  {
    ${0};
    return EXIT_SUCCESS;
  }

すると,素早く以下のようにmain関数を書けるようになります.

f:id:koturn:20171210023837g:plain

競プロをやっている人は普段使ってるテンプレートをスニペット登録しておくと便利かもしれないですね.

インクルードガード

ヘッダファイルにはインルードガードを書きますよね. コンパイラ拡張をなるべく嫌う派としては, #pragma を使わず,

#ifndef HOGE_H
#define HOGE_H

#endif

と書くと思いますが...面倒! 特に HOGE_H が2回登場しているので,これはスニペット化を考えるところです. そこで以下のスニペットを用意しましょう. (僕は #endif の後ろにコメント入れる主義なので,それも加えています)

snippet inc_guard
  #ifndef ${1:#:NAME}
  #define $1

  {0}

  #endif  // $1

すると, inc_guard<C-k> の入力で以下のように素早くインクルードガードを記述できます.

f:id:koturn:20171210023841g:plain

std::arraystd::vector 等の合計値を出す

std::arraystd::vector の要素の合計値を出したいときに困ることのひとつに std::sum() みたいな単純な関数が無いことがあると思います. <numeric>std::accumulate() を使えばいいのですが,

// #include <algorithm>
// #include <vector>
// #include <numeric>
// #include <random>

// 以下の2行は乱数入れてるだけなので,無視していいです
std::vector<int> vct(10);
std::generate(std::begin(vct), std::end(vct), std::mt19937(std::random_device{}()));
// 合計値を出すだけなのに,タイプ数が多い
auto sum = std::accumulate(std::cbegin(vct), std::cend(vct), 0);

このように,非常にタイプ数が多くて困りものです. なので,IQ1の僕は無い知恵を絞り,以下のスニペットを登録しました.

snippet sum
  std::accumulate(std::cbegin(${1}), std::cend($1), ${2:decltype($1)::value_type()})

第三引数を decltype(vct)::value_type() のように展開できるようにしておくと,要素型の値が何であってもデフォルト値を利用できるようになるので便利です. 例えば,int 型なら 0 ですし, double 型なら 0.0 ですね. (要素型が double であるのに,第三引数に 0 を指定すると,恐らく望んだ結果は返ってこないので,そういう事故防止にも役立ちます)

f:id:koturn:20171210023943g:plain

C++11以降の <regex>正規表現マッチングによるループ

C++11になり <regex> が追加されて,C++でも気軽に正規表現が利用できる時代となりました. しかし,ここで問題が1つあります. それは,<regex> は利用に際し,やや複雑なコードが要求されることです.

例えば,正規表現のマッチングによるループの例を見てみましょう.

// #incluse <iostream>
// #incluse <regex>
// #incluse <string>

std::string text("2017-12-10 12:34:56");
std::regex ptn("\\d+");
for (std::sregex_iterator itr = std::sregex_iterator(std::cbegin(text), std::cend(text), ptn), end; itr != end; ++itr) {
  std::cout << itr->str() << std::endl;
}

長い...こんなもの,とてもIQ1が気軽に利用できるものではありません!

しかし,こういうものはテンプレ,そして共通項があるというもの.

エイヤッと以下のスニペットを用意しましょう. 入力すべき項目は以下の4つです.

  1. イテレータの変数名
  2. 対象となる std::string の変数名
  3. マッチパターンとなる std::regex の変数名
  4. イテレータの終端を表す std::sregex_iterator のデフォルトでコンストラクトされたものを格納する変数名

4つ目は正直,決め打ちでもよいかな感はあるのですが,一応指定できるようにしておくと,同じブロックで既に変数名が使用されていた場合に対応できるというメリットがあります.

snippet regex_match_loop
  for (std::sregex_iterator ${1:itr} = std::sregex_iterator(std::cbegin(${2:#:text}), std::cend($2), ${3:#:regex}), ${4:end}; $1 != $4; ++$1) {
    ${0}
  }

f:id:koturn:20171210023947g:plain

スリープ

かつて,C++では,標準ライブラリでスリープを行う関数が提供されておらず,各環境に応じたスリープ系の関数を呼び出す必要がありました. 時は移り,C++11... <chrono><thread> が追加され,標準ライブラリの関数でスリープを行うことが可能になりました. ただし,やや長ったらしい記述が必要となるため,IQ1にとっては書くのが苦痛です.

// #include <chrono>
// #include <thread>
std::this_thread::sleep_for(std::chrono::milliseconds(1000));

スリープに必要なのは以下の2つ.

  1. 時間分解能(ミリ秒とか)
  2. どの程度スリープするか

なので,以下のスニペットを用意してみましょう.

std::this_thread::sleep_for(std::chrono::${1:milliseconds}(${2:1000}));

f:id:koturn:20171210023950g:plain

<algorithm> の関数にラムダを渡す

最もスニペットが有用なのは,ラムダ取る <algorithm> の関数のスニペットでしょう. 例えば, std::array の要素の内,3で割り切れるものがいくつあるか数えるコードを考えると,

// #include <algorithm>
// #include <array>
// #include <numeric>
// #include <iostream>

// 要素数とか入れる値は適当で
std::array<int, 10> arr;
std::iota(std::begin(arr), std::end(arr), 0);

auto cnt = std::count_if(
  std::cbegin(arr),
  std::cend(arr),
  [](const auto& e) {
    e % 3 == 0;
  });

と書くと思います. しかし,これはかなり面倒. 特にラムダの辺りが嫌な感じですね.

これをスニペットにすると以下のような感じでしょうか.

snippet count_if
alias count_f
abbr std::count_if <algorithm>
  std::count_if(
    std::cbegin(${1}),
    std::cend($1),
    [](const auto& ${2:e}) {
      return ${0};
    });

f:id:koturn:20171210023954g:plain

記述量が減って,かなり快適に書けるようになりました! ここで紹介している std:count_if だけでなく, std::sortstd::accumulate のラムダを取る版のスニペットを用意しておくと,非常に便利になるでしょう.

スニペットファイルを用意しました!!!

さて,ここまで紹介してきたneosnippetのC++用のスニペット定義ファイルを用意しましたC++11, C++14, C++17用と用意しています. 適当にコピペするなり,改変するなりして使ってください.

差異

C++11/C++14/C++17用のスニペットの差異は以下の通りです. 普段利用しているコンパイラに応じたものを使うといいでしょう. 例えば,競プロのジャッジサーバにC++11のコンパイラしか入っていないのであれば,C++11を使うのがよいでしょう.

C++11 -> C++14

std::sort()の述語やstd::accumulate()の集計関数等のラムダにジェネリックラムダを利用するように

decltype(vct)::value_type は見た目的に長いので,短い記法を使ってスッキリさせようというやつです. 関数等で参照として受け取った std::vector 等に対して用いる場合でも,いちいち std::remove_reference を狭まなくてよくなるので,楽ですね.

# before
std::sort(
  std::begin(${1}),
  std::end($1),
  [](const decltype($1)::value_type& x, const decltype($1)::value_type& y) {
    return ${0:x < y};
  });

# after
std::sort(
  std::begin(${1}),
  std::end($1),
  [](const auto& x, const auto& y) {
    return ${0:x < y};
  });

<algorithm> の読取専用のイテレータ引数に対して,std::begin()/std::end()でなく.std::cbegin()/std::cend()を用いるように

生成されるコードは変わらないと思うんですが,const付けられるものに対してはconstの方がいいよねというやつです. フリー関数の std::begin(), std::end()C++11で追加されたんですが,何故か std::cbegin(), std::cend()C++14になってから追加されたので,それに合わせた変更になります. C++11でもメンバー関数版の cbegin(), cend() 使えばいいじゃないという話になりそうですが, std::begin(), std::end() と釣り合いが取れなくなって気持ち悪いので....

# before (C++11)
snippet sum
  std::accumulate(std::begin(${1}), std::end($1), ${2:decltype($1)::value_type()})

# after (C++14)
snippet sum
  std::accumulate(std::cbegin(${1}), std::cend($1), ${2:decltype($1)::value_type()})

<type_traits> の関数

C++11では型の取得のために ::type にアクセスしていましたが,より簡潔に書けるようになったので,そちらを利用.

# before
snippet decay
  std::decay<T>::type

# after
snippet decay
  std::decay_t<T>

C++14 -> C++17

<type_traits> の関数

C++14までは型の判定に使用できるメタ関数の真偽値は value メンバーから取得していましたが,C++17ではもっと楽に取得できるようになったので,そちらを用いるようにしました.

まとめ

なお,neosnippet.vimの後続として,deoppet.nvimが開発されているとのことです. Vimconf 2017でShougoさんは,スニペットファイルはneosnippetと同じ形式とおっしゃていたと思うので,今の内にneosnippet用のスニペットファイルを充実させても損にはならないと思います.

最後に

ちゃっくさんの金で肉が食べたい!

明日は,shrcyanさんの記事になります.楽しみですね.

小さいHello Worldバイナリを作る

はじめに

先日は,少し不思議なHello Worldを紹介した. そこで思ったのが,この程度の小さいプログラムならば,gccは必要ないのではないかと思い至った. そこで,小さいHello Worldの実行ファイルを作ることにした.

方針

終了の仕方

先日の記事では,crt*.o ありきであったため,プログラマ視点でのプログマムのエントリポイントが main であったため,単に return 0; すればよかった. しかし,今回は crt*.o とのリンクは行わないため, return 0; に代わり, exit システムコールを呼び出す必要がある.

文字列データのアドレス

実行ファイルを生成するならば,"Hello World!\n" のアドレスも既知となる. したがって,ripから現在の実行中のアドレスを取得する必要はない.

また,前回と同様,文字列データは,コードの末尾に置くものとする.

セクションヘッダを削る

通常の実行ファイルは,

  1. ELFヘッダ
  2. プログラムヘッダ(複数)
  3. コードデータ
  4. セクション名テーブル
  5. セクションヘッダ(複数)

の5つを含むが 4.と5.は無くても良いので,以下の3つで実行ファイルを構成する.

  1. ELFヘッダ
  2. プログラムヘッダ(複数)
  3. コードデータ

セクションヘッダを削るため,$ objdump -d が効かなくなるが,まぁ良しとしよう.

実行ファイルの生成

上記の方針を踏まえ,実行ファイルを作るプログラムを書く. C言語で書く必要は無いのだが,ELFヘッダ,プログラムヘッダの構造体を利用することが可能なため,意外とCで書くのが楽になる. ただし,前回と同様,x64向けのプログラムを作る.

生成する実行ファイル名は a.out で固定しており,また,生成後に実行権限を付与し,実行するようにしてある.

これで,サイズにして171 bytesのHello Worldプログラムを作成できる.

コードの説明は特にしないが,構造体のメンバを見れば,どこで指定しているかは容易にわかる.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <elf.h>

#define BASE_ADDR    0x08000000
#define N_PROGRAM_HEADER  1
#define N_SECTION_HEADER  0
#define HEADER_SIZE  (sizeof(Elf64_Ehdr) + sizeof(Elf64_Phdr) * N_PROGRAM_HEADER)


int
main(void)
{
  unsigned int str_addr = BASE_ADDR + HEADER_SIZE + 37;
  Elf64_Ehdr ehdr;
  Elf64_Phdr phdr;
  char code[] =
    // mov $0x01,%rax  # System call number (write)
    "\x48\xc7\xc0\x01\x00\x00\x00"
    // mov $0x0d,%edx  # Third argument
    "\xba\x0d\x00\x00\x00"
    // mov $0x********,%rsi  # Second argument
    "\x48\xc7\xc6\x00\x00\x00\x00"
    // mov $0x01,%edi  # First argument
    "\xbf\x01\x00\x00\x00"
    // syscall
    "\x0f\x05"
    // mov $0x3c,%rax  # System call number (exit)
    "\x48\xc7\xc0\x3c\x00\x00\x00"
    // xor %edi,%edi   # First argument
    "\x31\xff"
    // syscall
    "\x0f\x05"
    // String data
    "Hello World!\n";
  FILE *f = fopen("a.out", "wb");
  if (f == NULL) {
    fputs("Failed to open a.out\n", stderr);
    return EXIT_FAILURE;
  }

  // ELF header
  ehdr.e_ident[EI_MAG0] = ELFMAG0;
  ehdr.e_ident[EI_MAG1] = ELFMAG1;
  ehdr.e_ident[EI_MAG2] = ELFMAG2;
  ehdr.e_ident[EI_MAG3] = ELFMAG3;
  ehdr.e_ident[EI_CLASS] = ELFCLASS64;
  ehdr.e_ident[EI_DATA] = ELFDATA2LSB;
  ehdr.e_ident[EI_VERSION] = EV_CURRENT;
  ehdr.e_ident[EI_OSABI] = ELFOSABI_LINUX;
  ehdr.e_ident[EI_ABIVERSION] = 0x00;
  ehdr.e_ident[EI_PAD] = 0x00;
  ehdr.e_type = ET_EXEC;
  ehdr.e_machine = EM_X86_64;
  ehdr.e_version = EV_CURRENT;
  ehdr.e_entry = BASE_ADDR + HEADER_SIZE;
  ehdr.e_phoff = sizeof(Elf64_Ehdr);
  ehdr.e_shoff = 0;
  ehdr.e_flags = 0x00000000;
  ehdr.e_ehsize = sizeof(Elf64_Ehdr);
  ehdr.e_phentsize = sizeof(Elf64_Phdr);
  ehdr.e_phnum = N_PROGRAM_HEADER;
  ehdr.e_shentsize = 0;
  ehdr.e_shnum = N_SECTION_HEADER;
  ehdr.e_shstrndx = SHN_UNDEF;
  fwrite(&ehdr, sizeof(ehdr), 1, f);

  // Program header
  phdr.p_type = PT_LOAD;
  phdr.p_flags = PF_R | PF_X;
  phdr.p_offset = 0x000000000000000000;
  phdr.p_vaddr = BASE_ADDR;
  phdr.p_paddr = BASE_ADDR;
  phdr.p_filesz = HEADER_SIZE + sizeof(code);
  phdr.p_memsz = HEADER_SIZE + sizeof(code);
  phdr.p_align = 0x0000000000000100;
  fwrite(&phdr, sizeof(phdr), 1, f);

  // Put string data address into code
  memcpy(code + 15, &str_addr, sizeof(str_addr));
  // Write .text section
  fwrite(code, 1, sizeof(code), f);

  fclose(f);
  f = NULL;

  // Show file size
  printf("Size of a.out: %lu bytes\n", HEADER_SIZE + sizeof(code));
  fflush(stdout);

  // Give execution permission
  chmod("a.out", 0755);
  // Execute created binary
  system("./a.out");

  return EXIT_SUCCESS;
}

コンパイルと実行結果は以下の通り.

$ gcc -O2 gen_hello_x64.c -o gen_hello_x64
$ ./gen_hello_x64
Size of a.out: 171 bytes
Hello World!

また,逆コンパイル結果は以下の通り. 前述した通り,セクションヘッダが無いため,プログラムデータ本体を見ることができない.

$ objdump -d a.out

a.out:     file format elf64-x86-64

おまけ: セクションヘッダを付ける

丁寧にセクションヘッダも付けるとしたら,プログラムは以下のようになる.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <elf.h>

#define BASE_ADDR    0x08000000
#define N_PROGRAM_HEADER  1
#define N_SECTION_HEADER  3
#define HEADER_SIZE  (sizeof(Elf64_Ehdr) + sizeof(Elf64_Phdr) * N_PROGRAM_HEADER)
#define FOOTER_SIZE  (sizeof(Elf64_Shdr) * N_SECTION_HEADER)

const char SH_STR_TBL[] = "\0.text\0.shstrtbl";


int
main(void)
{
  unsigned int str_addr = BASE_ADDR + HEADER_SIZE + 37;
  Elf64_Ehdr ehdr;
  Elf64_Phdr phdr;
  Elf64_Shdr shdr;
  char code[] =
    /* mov $0x01,%rax  # System call number (write) */
    "\x48\xc7\xc0\x01\x00\x00\x00"
    /* mov $0x0d,%edx  # Third argument */
    "\xba\x0d\x00\x00\x00"
    /* mov $0x********,%rsi  # Second argument */
    "\x48\xc7\xc6\x00\x00\x00\x00"
    /* mov $0x01,%edi  # First argument */
    "\xbf\x01\x00\x00\x00"
    /* syscall */
    "\x0f\x05"
    /* mov $0x3c,%rax  # System call number (exit) */
    "\x48\xc7\xc0\x3c\x00\x00\x00"
    /* xor %edi,%edi  # First argument */
    "\x31\xff"
    /* syscall */
    "\x0f\x05"
    /* String data */
    "Hello World!\n";
  FILE *f = fopen("a.out", "wb");
  if (f == NULL) {
    fputs("Failed to open a.out\n", stderr);
    return EXIT_FAILURE;
  }

  /* ELF header */
  ehdr.e_ident[EI_MAG0] = ELFMAG0;
  ehdr.e_ident[EI_MAG1] = ELFMAG1;
  ehdr.e_ident[EI_MAG2] = ELFMAG2;
  ehdr.e_ident[EI_MAG3] = ELFMAG3;
  ehdr.e_ident[EI_CLASS] = ELFCLASS64;
  ehdr.e_ident[EI_DATA] = ELFDATA2LSB;
  ehdr.e_ident[EI_VERSION] = EV_CURRENT;
  ehdr.e_ident[EI_OSABI] = ELFOSABI_LINUX;
  ehdr.e_ident[EI_ABIVERSION] = 0x00;
  ehdr.e_ident[EI_PAD] = 0x00;
  ehdr.e_type = ET_EXEC;
  ehdr.e_machine = EM_X86_64;
  ehdr.e_version = EV_CURRENT;
  ehdr.e_entry = BASE_ADDR + HEADER_SIZE;
  ehdr.e_phoff = sizeof(Elf64_Ehdr);
  ehdr.e_shoff = HEADER_SIZE + sizeof(code) + sizeof(SH_STR_TBL);
  ehdr.e_flags = 0x00000000;
  ehdr.e_ehsize = sizeof(Elf64_Ehdr);
  ehdr.e_phentsize = sizeof(Elf64_Phdr);
  ehdr.e_phnum = N_PROGRAM_HEADER;
  ehdr.e_shentsize = sizeof(Elf64_Shdr);
  ehdr.e_shnum = N_SECTION_HEADER;
  ehdr.e_shstrndx = 1;
  fwrite(&ehdr, sizeof(ehdr), 1, f);

  /* Program header */
  phdr.p_type = PT_LOAD;
  phdr.p_flags = PF_R | PF_X;
  phdr.p_offset = 0x000000000000000000;
  phdr.p_vaddr = BASE_ADDR;
  phdr.p_paddr = BASE_ADDR;
  phdr.p_filesz = HEADER_SIZE + sizeof(code) + sizeof(SH_STR_TBL) + FOOTER_SIZE;
  phdr.p_memsz = HEADER_SIZE + sizeof(code) + sizeof(SH_STR_TBL) + FOOTER_SIZE;
  phdr.p_align = 0x0000000000000100;
  fwrite(&phdr, sizeof(phdr), 1, f);

  /* Put string data address into code */
  memcpy(code + 15, &str_addr, sizeof(str_addr));
  /* Write .text section */
  fwrite(code, 1, sizeof(code), f);

  /* Write section header names */
  fwrite(SH_STR_TBL, 1, sizeof(SH_STR_TBL), f);

  /* First section header */
  shdr.sh_name = 0;
  shdr.sh_type = SHT_NULL;
  shdr.sh_flags = 0x0000000000000000;
  shdr.sh_addr = 0x0000000000000000;
  shdr.sh_offset = 0x0000000000000000;
  shdr.sh_size = 0x0000000000000000;
  shdr.sh_link = 0x00000000;
  shdr.sh_info = 0x00000000;
  shdr.sh_addralign = 0x0000000000000000;
  shdr.sh_entsize = 0x0000000000000000;
  fwrite(&shdr, sizeof(shdr), 1, f);

  /* Second section header (.shstrtbl) */
  shdr.sh_name = 7;
  shdr.sh_type = SHT_STRTAB;
  shdr.sh_flags = 0x0000000000000000;
  shdr.sh_addr = 0x0000000000000000;
  shdr.sh_offset = HEADER_SIZE + sizeof(code);
  shdr.sh_size = sizeof(SH_STR_TBL);
  shdr.sh_link = 0x00000000;
  shdr.sh_info = 0x00000000;
  shdr.sh_addralign = 0x0000000000000001;
  shdr.sh_entsize = 0x0000000000000000;
  fwrite(&shdr, sizeof(shdr), 1, f);

  /* Third section header (.text) */
  shdr.sh_name = 1;
  shdr.sh_type = SHT_PROGBITS;
  shdr.sh_flags = SHF_EXECINSTR | SHF_ALLOC;
  shdr.sh_addr = BASE_ADDR + HEADER_SIZE;
  shdr.sh_offset = HEADER_SIZE;
  shdr.sh_size = sizeof(code);
  shdr.sh_link = 0x00000000;
  shdr.sh_info = 0x00000000;
  shdr.sh_addralign = 0x0000000000000004;
  shdr.sh_entsize = 0x0000000000000000;
  fwrite(&shdr, sizeof(shdr), 1, f);

  fclose(f);
  f = NULL;

  /* Show file size */
  printf("Size of a.out: %lu bytes\n", HEADER_SIZE + sizeof(code) + sizeof(SH_STR_TBL) + FOOTER_SIZE);
  fflush(stdout);

  /* Give execution permission */
  chmod("a.out", 0755);
  /* Execute created binary */
  system("./a.out");

  return EXIT_SUCCESS;
}

上記のプログラムから a.out を生成し,objdump で逆アセンブルしてみると,以下のようになる.

$ gcc -O2 gen_hello_x64.c -o gen_hello_x64
$ ./gen_hello_x64
Size of a.out: 380 bytes
Hello World!
$ objdump -d a.out

a.out:     ファイル形式 elf64-x86-64


セクション .text の逆アセンブル:

0000000008000078 <.text>:
 8000078:       48 c7 c0 01 00 00 00    mov    $0x1,%rax
 800007f:       ba 0d 00 00 00          mov    $0xd,%edx
 8000084:       48 c7 c6 9d 00 00 08    mov    $0x800009d,%rsi
 800008b:       bf 01 00 00 00          mov    $0x1,%edi
 8000090:       0f 05                   syscall
 8000092:       48 c7 c0 3c 00 00 00    mov    $0x3c,%rax
 8000099:       31 ff                   xor    %edi,%edi
 800009b:       0f 05                   syscall
 800009d:       48                      rex.W
 800009e:       65                      gs
 800009f:       6c                      insb   (%dx),%es:(%rdi)
 80000a0:       6c                      insb   (%dx),%es:(%rdi)
 80000a1:       6f                      outsl  %ds:(%rsi),(%dx)
 80000a2:       20 57 6f                and    %dl,0x6f(%rdi)
 80000a5:       72 6c                   jb     0x8000113
 80000a7:       64 21 0a                and    %ecx,%fs:(%rdx)
        ...

無事に .text セクションの中身がわかるようになった. Hello World! の部分については無理に解釈されているため,でたらめなニーモニックが出力されているが,気にしなくてもよい.

まとめ

実行ファイルにはELFヘッダとプログラムヘッダが必須となるが,セクションヘッダは無くてもよい. ヘッダでエントリポイントを指定し,その位置から機械語を配置することで,実行ファイルができる.

なお,今回の記事の実行ファイル生成プログラムは以下のリポジトリにある.

また,工夫次第ではもっと小さい実行ファイルを作れるようだ. 以下の記事では終了ステータスを返すだけのx86プログラムだが,x64でも同様の手法がとれるだろう.

不思議なHello World

背景

最近になって,x64をちゃんと勉強したので,少しだけひねったHello WorldC言語で書いてみることにした. (※x64のLinux環境でしか動作しない)

const char main[] = "\x48\xc7\xc0\x01\x00\x00\x00\xba\x0d\x00\x00\x00\xbf\x01\x00\x00\x00\xe8\x0c\x00\x00\x00\x48\x81\xc6\x11\x00\x00\x00\x0f\x05\x31\xc0\xc3\x48\x8b\x34\x24\xc3Hello World!\n";

何とワンライナーである. ヘッダのインクルードが無く,main関数ではなく,main配列となっている点が通常のHello Worldと比較すると異質な点であろう.

ワンライナーではわかりづらいので,コメントを入れてみる. 要は機械語である.

const char main[] =
  // mov $0x01,%rax
  "\x48\xc7\xc0\x01\x00\x00\x00"
  // mov $0x0d,%edx
  "\xba\x0d\x00\x00\x00"
  // mov $0x01,%edi
  "\xbf\x01\x00\x00\x00"
  // callq 0x0c
  "\xe8\x0c\x00\x00\x00"
  // add $0x11,%rsi
  "\x48\x81\xc6\x11\x00\x00\x00"
  // syscall
  "\x0f\x05"
  // xor %eax,%eax
  "\x31\xc0"
  // ret
  "\xc3"
  // mov (%rsp),%rsi
  "\x48\x8b\x34\x24"
  // ret
  "\xc3"
  // String data
  "Hello World!\n";

先にタネ明かしをすると,以下を行っているだけにすぎない.

// 第1引数:ファイルディスクリプタ (1: stdout)
// 第2引数:文字列領域へのポインタ
// 第3引数:出力文字数
write(1, "Hello World!\n", 13);

このことを念頭に置き,解説を行っていく.

解説

全体像

mainというのは所詮 crt*.o 中から呼び出しされているに過ぎない. 従って,main という名前の指し示すアドレスの先に関数と同様のデータがあれば問題はない.

基本的に1つの配列に納めたかったので,コードとデータを同一の領域に置いてある. 別々にした場合,データのアドレスを取得するのが面倒になるためだ. (引数でデータのポインタを渡す等の処理が必要になるが,今回はmainを作っているため難しい)

配列 main 中の "Hello World!\n" より前は実行するコードであり,"Hello World!\n" は当然データである. 後は,コード中のいずれかの位置で,その地点のアドレスを取得し, "Hello World!\n" 領域のアドレスを取得すればよい.

システムコール

x64におけるシステムコールでは各レジスタが以下のように役割を果たす. 今回利用するのは第三引数まで.

レジスタ 役割
rax システムコール番号
rdi システムコールの第1引数
rsi システムコールの第2引数
rdx システムコールの第3引数

今回利用する write システムコールC言語APIは以下の通り.

size_t write(int fd, const void *buf, size_t count)

引数 役割
fd ファイルディスクリプタ
buf 出力データのサイズ
count 出力文字数

以上より,レジスタを以下の状態にすることを目指す.

レジスタ 中身
rax 1 (writeシステムコールの番号は1)
rdi 1 (stdoutは1)
rsi Hello Worldへのアドレス
rdx 13 (“Hello World!\n” の文字数は13)

現在位置の取得

Hello World! 文字列アドレスの取得のために,実行しているコードの位置を取得する必要がある. 現在位置はレジスタripに入っているが,直接値を取り出すことはできない. したがって,

  1. call命令を利用し,rspレジスタの管理アドレスにripの値を書き込ませる.
  2. rspが書き込んだ領域からripを取得(mov (%rsp),%rsi

という手段で取得する.

コードまとめ

ここまでの話をまとめると,配列の中身は以下の通りになる.

命令 動作
mov $0x01,%rax raxに1を格納
mov $0x0d,%edx rdxの下位32bitに0x0d(0x14)をセット
mov $0x01,%edi rdiの下位32bitに0x01をセット
callq 0x0c 12bbyte先を関数として呼び出し
add $0x11,%rsi 0x0fをrsiに加算.この命令位置から17byte先が "Hello World!\n"
syscall システムコール
xor %eax,%eax mainの返り値を0に設定(eaxは関数の返り値)
ret main関数のreturn
(%rsp),%rsi 呼出元のアドレスをrsiに格納
ret 呼出位置アドレス取得関数のreturn(add $0x0f,%rsiに復帰)
"Hwllo World!\n" 文字列データ

その他いろいろ

通常,関数の返り値はraxに格納するものだが,面倒なので今回は直接rsiに放り込むという手段を取っている. 自分でコードを書く分には,x64の呼出規約を無視しても構わないだろう.

まとめ

この記事では,少し奇妙な形のC言語Hello Worldを紹介した. 一見,難解に見えるHello Worldのコードであるが,わかってしまえば大したことはない.

今回はx64用のコードを紹介した. なので,Wandboxに貼り付ければ実行できる.

x86については読者の課題としよう(笑)