Evinceの実行ファイルをいじってファイル履歴の数を増やす

仕事でけっこうな数のPDFファイルを開く.諸事情でパスが深くて場所も散らばってるので,がんばって一度開いた履歴はなるべく長く取っておいて省力のために活用したい.ところが現状では直近5個しかメニューで選べないので,これを20個程度に増やしたい.
とりあえずGitでソースを取ってくる.遅い...

  • git://git.gnome.org/evince

今使ってるのは,この通り Evince 3.4.0 である.

$ dpkg -l evince
要望=(U)不明/(I)インストール/(R)削除/(P)完全削除/(H)維持
| 状態=(N)無/(I)インストール済/(C)設定/(U)展開/(F)設定失敗/(H)半インストール/(W)トリガ待ち/(T)トリガ保留
|/ エラー?=(空欄)無/(R)要再インストール (状態,エラーの大文字=異常)
||/ 名前              バージョン     説明
+++-===================-===================-======================================================
ii  evince              3.4.0-0ubuntu1.7    Document (PostScript, PDF) viewer
$ lsb_release -a
LSB Version:    core-2.0-amd64:(略)
Distributor ID: Ubuntu
Description:    Ubuntu 12.04.4 LTS
Release:        12.04
Codename:       precise
$ uname -a
Linux kaidev01 3.2.0-67-generic #101-Ubuntu SMP Tue Jul 15 17:46:11 UTC 2014 x86_64 x86_64 x86_64 GNU/Linux

2014年基準では結構古いが... 仕方ない.当該バージョンをチェックアウト.

$ cd evince-HEAD
$ git co 3.4.0
Note: checking out '3.4.0'.

You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by performing another checkout.

If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -b with the checkout command again. Example:

  git checkout -b new_branch_name

HEAD is now at 1267587... release: 3.4.0

ここでしばらく関連するソースコードを探すために試行錯誤grep*1.最初 "history" で調べたが,どうもページジャンプの履歴関連のコードがヒットするばかりだ.答えは "recent" の名前を持つ変数・関数群だった.

$ ack --cc -i recent
shell/ev-window.c
72:#include "ev-open-recent-action.h"
167:    GtkRecentManager *recent_manager;
168:    GtkActionGroup   *recent_action_group;
169:    guint             recent_ui_id;
267:#define MAX_RECENT_ITEM_LEN (40)
313:static void     ev_window_add_recent                    (EvWindow         *window,
1642:           ev_window_add_recent (ev_window, ev_window->priv->uri);
2489:ev_window_cmd_recent_file_activate (GtkAction *action,
2492:   GtkRecentInfo *info;
2495:   info = g_object_get_data (G_OBJECT (action), "gtk-recent-info");
2498:   uri = gtk_recent_info_get_uri (info);
2506:ev_window_open_recent_action_item_activated (EvOpenRecentAction *action,
2516:ev_window_add_recent (EvWindow *window, const char *filename)
2518:   gtk_recent_manager_add_item (window->priv->recent_manager, filename);
2522:compare_recent_items (GtkRecentInfo *a, GtkRecentInfo *b)
2527:   has_ev_a = gtk_recent_info_has_application (a, evince);
2528:   has_ev_b = gtk_recent_info_has_application (b, evince);
2533:           time_a = gtk_recent_info_get_modified (a);
2534:           time_b = gtk_recent_info_get_modified (b);
(略)

ここで MAX_RECENT_ITEM_LEN というCPP定数を見つけて「これか?」と一瞬色めき立ったが,どうも表示上の文字数の上限に過ぎないようだ.もうちょっと見ていくとGTKの提供する GtkRecentManager というデータ構造を gtk_recent_... という名前のAPIで操作するらしいことが分かった.

$ ack --cc gtk_recent
shell/ev-window.c
2498:   uri = gtk_recent_info_get_uri (info);
2518:   gtk_recent_manager_add_item (window->priv->recent_manager, filename);
2527:   has_ev_a = gtk_recent_info_has_application (a, evince);
2528:   has_ev_b = gtk_recent_info_has_application (b, evince);
2533:           time_a = gtk_recent_info_get_modified (a);
2534:           time_b = gtk_recent_info_get_modified (b);
2632:   items = gtk_recent_manager_get_items (ev_window->priv->recent_manager);
2646:           if (!gtk_recent_info_has_application (info, evince) ||
2647:               (gtk_recent_info_is_local (info) && !gtk_recent_info_exists (info)))
2652:                   n_items + 1, gtk_recent_info_get_display_name (info));
2654:                mime_type = gtk_recent_info_get_mime_type (info);
2670:                                   gtk_recent_info_ref (info),
2671:                                   (GDestroyNotify) gtk_recent_info_unref);
2697:   g_list_foreach (items, (GFunc) gtk_recent_info_unref, NULL);
7263:   ev_window->priv->recent_manager = gtk_recent_manager_get_default ();

shell/ev-open-recent-action.c
46:     uri = gtk_recent_chooser_get_current_uri (chooser);
58:     toolbar_recent_menu = gtk_recent_chooser_menu_new_for_manager (gtk_recent_manager_get_default ());
59:     gtk_recent_chooser_set_local_only (GTK_RECENT_CHOOSER (toolbar_recent_menu), FALSE);
60:     gtk_recent_chooser_set_sort_type (GTK_RECENT_CHOOSER (toolbar_recent_menu), GTK_RECENT_SORT_MRU);
61:     gtk_recent_chooser_set_limit (GTK_RECENT_CHOOSER (toolbar_recent_menu), 5);
66:     filter = gtk_recent_filter_new ();
67:     gtk_recent_filter_add_application (filter, g_get_application_name ());
68:     gtk_recent_chooser_set_filter (GTK_RECENT_CHOOSER (toolbar_recent_menu), filter);

ミツケター

61:     gtk_recent_chooser_set_limit (GTK_RECENT_CHOOSER (toolbar_recent_menu), 5);

薄々予感してたが長さ決め打ちか... (確かに5個が上限となってる.)これはソースを書き換えて再コンパイルしないといけない感じ.ところが実際にやったことある人は分かると思うがGUIアプリ,特にEvinceのようなGNOMEアプリは依存性とかビルドオプションとか多くて色々めんどい.もちろん,そもそもUbuntuのパッケージとして入れたのだから .deb とかが手に入るはずで,apt-get source から始まる手順に従えば再現性あるビルドができるはずだが... 色々めんどくてやだ.
ということでバイナリを書き換えることにする.

$ mkcd /tmp/hoge
$ cp =evince .
$ objdump -d evince LL

ここで mkcdmkdir -p 後に cd する俺シェル関数,=cmdZsh標準の `which cmd` の略記,LLZshの「グローバル」エイリアスとして "| less" に展開されるよう設定してある.
検索すると一か所でしか使われてない(ソースで検索しても同じかもね).

   2a19b:       be 05 00 00 00          mov    $0x5,%esi
   2a1a0:       48 89 c7                mov    %rax,%rdi
   2a1a3:       e8 38 13 ff ff          callq  1b4e0 <gtk_recent_chooser_set_limit@plt>

うろ覚えのAMD64呼び出し規約*2でも %rdi, %rsi, ... に第1, 第2, ... 引数が詰まるはずなんで,

   2a19b:       be 05 00 00 00          mov    $0x5,%esi

さすがにx86 MOV のオペコードまでは覚えてないが((意味的なオペコードのベース値(と呼んでよかろう)とレジスタ番号を足し算した結果として上記の 0xbe ができる.覚えられる訳がない...))この $0x5 を書き換えればよいと想像が付く.怖いので&不必要なので編集は1バイトの範囲内 (<= 255) に止め,切りよく 0x20 にしよう.
残念ながら sed(1) はバイナリファイルに対しまともに動かないことが知られてるので,手順は

  1. xxd(1) でhexダンプのテキストを保存し,
  2. それをエディタで編集し,
  3. xxd -r で実行ファイルに戻す

Vimだと直接あれこれできるが((:help using-xxd 辺りに解説されてるようだ.)),大差ないので地道に...

$ xxd evince > evince.xxd
$ vim evince.xxd

アドレス "2a190: " で / 検索すると次の行が見つかる(xxd(1) はデフォルトで1行に16バイトを表示,つまりアドレス表記は最終桁の切り捨て)

10778 002a190: 4889 ee48 89df e845 17ff ffbe 0500 0000  H..H...E........

これをさくっと書き換え,

10778 002a190: 4889 ee48 89df e845 17ff ffbe 2000 0000  H..H...E........

逆変換(このとき,当然と言えば当然だが,ASCIIダンプ部は無視されるので律儀にいじらなくてもよい)

$ xxd -r evince.xxd > evince

さて実行... するとクラッシュした.あれー?? どこか別の場所を一貫して編集しないといけないのか.GUI関連と思しき shell/ ディレクトリで,一単語としての "5" を検索.ack(1) はPerlベースなので単語境界の表現は \b を使う.

$ ack --cc '\b5\b' shell/
shell/ev-properties-dialog.c
64:     gtk_container_set_border_width (GTK_CONTAINER (properties), 5);
73:     gtk_container_set_border_width (GTK_CONTAINER (properties->notebook), 5);

shell/ev-sidebar-thumbnails.c
127:    *width = MAX ((gint)(w * scale + 0.5), 1);
128:    *height = MAX ((gint)(h * scale + 0.5), 1);

shell/ev-window.c
113:    EV_CHROME_SIDEBAR       = 1 << 5,
1275:           request_width = (gint)(width_ratio * document_width + 0.5);
1276:           request_height = (gint)(height_ratio * document_height + 0.5);
2693:           if (++n_items == 5)
4409:   gtk_container_set_border_width (GTK_CONTAINER (GTK_DIALOG (dialog)), 5);
4417:   gtk_container_set_border_width (GTK_CONTAINER (editor), 5);
4418:   gtk_box_set_spacing (GTK_BOX (EGG_TOOLBAR_EDITOR (editor)), 5);

shell/ev-message-area.c
108:    gtk_misc_set_alignment (GTK_MISC (area->priv->label), 0.0, 0.5);
117:    gtk_misc_set_alignment (GTK_MISC (area->priv->secondary_label), 0.0, 0.5);
122:    gtk_misc_set_alignment (GTK_MISC (area->priv->image), 0.5, 0.0);

shell/ev-progress-message-area.c
94:     gtk_misc_set_alignment (GTK_MISC (area->priv->label), 0.0, 0.5);

shell/eggfindbar.c
307:  alignment = gtk_alignment_new (0.0, 0.5, 1.0, 0.0);
349:  gtk_misc_set_alignment (GTK_MISC (priv->status_label), 0.0, 0.5);
656: * be something like "5 results on this page" or "No results"

shell/ev-properties-license.c
110:    gtk_misc_set_alignment (GTK_MISC (title), 0.0, 0.5);
118:    alignment = gtk_alignment_new (0.5, 0.5, 1., 1.);

shell/ev-file-monitor.c
113:            g_timeout_add_seconds (5, (GSourceFunc)timeout_cb, ev_monitor);

shell/ev-password-view.c
122:    align = gtk_alignment_new (0.5, 0.5, 0.0, 0.0);
256:    gtk_container_set_border_width (GTK_CONTAINER (dialog), 5);
257:    gtk_box_set_spacing (GTK_BOX (content_area), 2); /* 2 * 5 + 2 = 12 */
258:    gtk_container_set_border_width (GTK_CONTAINER (action_area), 5);
281:    gtk_container_set_border_width (GTK_CONTAINER (hbox), 5);
288:    gtk_misc_set_alignment (GTK_MISC (icon), 0.5, 0.0);
297:    gtk_misc_set_alignment (GTK_MISC (label), 0.0, 0.5);
333:    gtk_misc_set_alignment (GTK_MISC (label), 0.0, 0.5);

shell/ev-annotation-properties-dialog.c
95:             gtk_misc_set_alignment (GTK_MISC (label), 0., 0.5);
96:             gtk_grid_attach (GTK_GRID (grid), label, 0, 5, 1, 1);
111:            gtk_grid_attach (GTK_GRID (grid), dialog->icon, 1, 5, 1, 1);
136:    gtk_container_set_border_width (GTK_CONTAINER (annot_dialog), 5);
155:    gtk_misc_set_alignment (GTK_MISC (label), 0., 0.5);
166:    gtk_misc_set_alignment (GTK_MISC (label), 0., 0.5);
176:    gtk_misc_set_alignment (GTK_MISC (label), 0., 0.5);
181:                                                          0, 100, 5);
208:    gtk_misc_set_alignment (GTK_MISC (label), 0., 0.5);

shell/ev-utils.c
95:       x_offset = (blur_radius * 4) / 5;
98:       y_offset = (blur_radius * 4) / 5;

shell/ev-open-recent-action.c
61:     gtk_recent_chooser_set_limit (GTK_RECENT_CHOOSER (toolbar_recent_menu), 5);

数が大したことないので,がんばって目で探すと,

shell/ev-window.c
...
2693:           if (++n_items == 5)

これだろ.周辺コードはこんな感じ:

2605 static void
2606 ev_window_setup_recent (EvWindow *ev_window)
2607 {
2608         GList        *items, *l;
2609         guint         n_items = 0;
...
2632         items = gtk_recent_manager_get_items (ev_window->priv->recent_manager);
2633         items = g_list_sort (items, (GCompareFunc) compare_recent_items);
2634
2635         for (l = items; l && l->data; l = g_list_next (l)) {
...
2646                 if (!gtk_recent_info_has_application (info, evince) ||
2647                     (gtk_recent_info_is_local (info) && !gtk_recent_info_exists (info)))
2648                         continue;
...
2681                 gtk_ui_manager_add_ui (ev_window->priv->ui_manager,
2682                                        ev_window->priv->recent_ui_id,
2683                                        "/MainMenu/FileMenu/RecentFilesMenu",
2684                                        label,
2685                                        action_name,
2686                                        GTK_UI_MANAGER_MENUITEM,
2687                                        FALSE);
2688                 g_free (action_name);
2689                 g_free (label);
2690                 if (icon != NULL)
2691                         g_object_unref (icon);
2692
2693                 if (++n_items == 5)
2694                         break;
2695         }
...
2699 }

なるほど,実際にメニューにファイル履歴の項目を並べるコードか.どうやら GtkRecentManager は私がGNOME系アプリで開いたファイル履歴を全て返すので,Evince内で「Evinceで開いたもの」をフィルタしており,それがいつ5個に達するかは最初から予測はできないからループ内で数を数えているみたい.
ローカル変数であるループ・カウンタの上限はAPI呼び出しの引数よりやや探し辛いが,そのちょっと前にある特徴的な gtk_ui_manager_add_ui() の呼び出しを逆アセンブリで検索.

$ objdump -d evince | ack -C10 'callq .*gtk_ui_manager_add_ui@plt'
   2d512:       49 89 c6                mov    %rax,%r14
   2d515:       e8 06 f8 fe ff          callq  1cd20 <gtk_action_get_label@plt>
   2d51a:       48 8b 53 38             mov    0x38(%rbx),%rdx
   2d51e:       41 b9 20 00 00 00       mov    $0x20,%r9d
   2d524:       4d 89 f0                mov    %r14,%r8
   2d527:       48 89 c1                mov    %rax,%rcx
   2d52a:       8b b2 10 01 00 00       mov    0x110(%rdx),%esi
   2d530:       48 8b ba 18 01 00 00    mov    0x118(%rdx),%rdi
   2d537:       48 8d 15 0a 89 02 00    lea    0x2890a(%rip),%rdx        # 55e48 <_IO_stdin_used+0x2f08>
   2d53e:       c7 04 24 00 00 00 00    movl   $0x0,(%rsp)
   2d545:       e8 86 fa fe ff          callq  1cfd0 <gtk_ui_manager_add_ui@plt>
   2d54a:       48 89 ef                mov    %rbp,%rdi
   2d54d:       e8 4e 10 ff ff          callq  1e5a0 <g_object_unref@plt>
   2d552:       4d 8b 64 24 08          mov    0x8(%r12),%r12
   2d557:       4d 85 e4                test   %r12,%r12
   2d55a:       0f 85 60 ff ff ff       jne    2d4c0 <ev_gui_menu_position_tree_selection+0x2b70>
   2d560:       48 83 c4 10             add    $0x10,%rsp
   2d564:       4c 89 ef                mov    %r13,%rdi
   2d567:       5b                      pop    %rbx
   2d568:       5d                      pop    %rbp
   2d569:       41 5c                   pop    %r12
--
   2df52:       e8 49 06 ff ff          callq  1e5a0 <g_object_unref@plt>
   2df57:       48 8b 54 24 20          mov    0x20(%rsp),%rdx
   2df5c:       4c 8b 44 24 38          mov    0x38(%rsp),%r8
   2df61:       41 b9 20 00 00 00       mov    $0x20,%r9d
   2df67:       4c 89 f1                mov    %r14,%rcx
   2df6a:       48 8b 42 38             mov    0x38(%rdx),%rax
   2df6e:       48 8d 15 2b 7f 02 00    lea    0x27f2b(%rip),%rdx        # 55ea0 <_IO_stdin_used+0x2f60>
   2df75:       48 8b b8 18 01 00 00    mov    0x118(%rax),%rdi
   2df7c:       8b b0 00 01 00 00       mov    0x100(%rax),%esi
   2df82:       c7 04 24 00 00 00 00    movl   $0x0,(%rsp)
   2df89:       e8 42 f0 fe ff          callq  1cfd0 <gtk_ui_manager_add_ui@plt>
   2df8e:       48 8b 7c 24 38          mov    0x38(%rsp),%rdi
   2df93:       e8 88 f8 fe ff          callq  1d820 <g_free@plt>
   2df98:       4c 89 f7                mov    %r14,%rdi
   2df9b:       e8 80 f8 fe ff          callq  1d820 <g_free@plt>
   2dfa0:       4d 85 e4                test   %r12,%r12
   2dfa3:       74 08                   je     2dfad <ev_gui_menu_position_tree_selection+0x365d>
   2dfa5:       4c 89 e7                mov    %r12,%rdi
   2dfa8:       e8 f3 05 ff ff          callq  1e5a0 <g_object_unref@plt>
   2dfad:       83 7c 24 34 05          cmpl   $0x5,0x34(%rsp)
   2dfb2:       0f 85 80 fd ff ff       jne    2dd38 <ev_gui_menu_position_tree_selection+0x33e8>
--
   45d18:       74 37                   je     45d51 <ev_gui_menu_position_tree_selection+0x1b401>
   45d1a:       66 0f 1f 44 00 00       nopw   0x0(%rax,%rax,1)
   45d20:       8b 73 30                mov    0x30(%rbx),%esi
   45d23:       4c 8d 84 24 80 00 00    lea    0x80(%rsp),%r8
   45d2a:       00
   45d2b:       c7 04 24 00 00 00 00    movl   $0x0,(%rsp)
   45d32:       49 8b 14 24             mov    (%r12),%rdx
   45d36:       48 8b 3b                mov    (%rbx),%rdi
   45d39:       41 b9 20 00 00 00       mov    $0x20,%r9d
   45d3f:       4c 89 c1                mov    %r8,%rcx
   45d42:       e8 89 72 fd ff          callq  1cfd0 <gtk_ui_manager_add_ui@plt>
   45d47:       4d 8b 64 24 08          mov    0x8(%r12),%r12
   45d4c:       4d 85 e4                test   %r12,%r12
   45d4f:       75 cf                   jne    45d20 <ev_gui_menu_position_tree_selection+0x1b3d0>
   45d51:       4c 89 f7                mov    %r14,%rdi
   45d54:       e8 c7 7a fd ff          callq  1d820 <g_free@plt>
   45d59:       48 83 44 24 30 01       addq   $0x1,0x30(%rsp)
   45d5f:       8b 44 24 30             mov    0x30(%rsp),%eax
   45d63:       39 44 24 54             cmp    %eax,0x54(%rsp)
   45d67:       0f 8f 93 fc ff ff       jg     45a00 <ev_gui_menu_position_tree_selection+0x1b0b0>
   45d6d:       8b 74 24 54             mov    0x54(%rsp),%esi

うーん3か所か... さらに特徴的なAPI呼び出しに注目, g_free() を2個と g_object_unref() を1個呼び出してるのは2個目のみで,それだと同定できた.よく見ると

   2dfad:       83 7c 24 34 05          cmpl   $0x5,0x34(%rsp)
   2dfb2:       0f 85 80 fd ff ff       jne    2dd38 <ev_gui_menu_position_tree_selection+0x33e8>

お,これは

2693                 if (++n_items == 5)
2694                         break;

に対応するだろう.この $0x5 を書き換えればよさそう.Hexダンプのアドレス "2dfa0: " を探すと,当該インストラクションは2行に泣き別れになっており,やや見辛いが,

11771 002dfa0: 4d85 e474 084c 89e7 e8f3 05ff ff83 7c24  M..t.L........|$
11772 002dfb0: 3405 0f85 80fd ffff 0f1f 8400 0000 0000  4...............

やることは同じ.

11771 002dfa0: 4d85 e474 084c 89e7 e8f3 05ff ff83 7c24  M..t.L........|$
11772 002dfb0: 3420 0f85 80fd ffff 0f1f 8400 0000 0000  4...............

改めて逆変換.

$ xxd -r evince.xxd > evince

これで動きました.まぁソースが読めるんだから簡単だわな... 初級編ということで.

*1:正確には,最近はPerlで実装されたack ( http://beyondgrep.com/ ) を使うことが多い.ギガバイト級を舐めるんでなければフツーに便利.

*2:最近,ほんとStackOverflowには助かってるわ... http://stackoverflow.com/questions/2535989/what-are-the-calling-conventions-for-unix-linux-system-calls-on-x86-64