d.sunnyone.org
sunnyone.org

ページ

2016-02-08

PHPが出力するsyslogの「ool www」の謎を追う

PHPからerror_log()でsyslogを吐いたときに出力される「ool www」という謎の文字列が一体なんなのか調べた。いいから回避策が知りたいという人はこちらへ。おまけが本編という人もいるかもしれない。

発端

とあるPHPアプリケーションがエラーログをPHP標準関数error_log()で吐く。それをフィルタしたくなったのだが、error_log()にはたいしたフックポイントもなく、アプリケーション自体には手を入れたくないので、とりあえずsyslogで受けてSyslogサーバーがなんとかするのがよいかと思って「error_log」の設定を「syslog」にしてみると、なぜかこんな感じに……

Feb  7 22:02:36 phptest ool www: LOG CONTENT

……この「ool www」は一体なんなんだ? Out-of-L...うーん。ここの文字列だけなら実害はないが、なんでこんなことになっているのかわからないので調べることにした。

テストファイル

当該のPHPアプリケーションは置いといて、1ファイルでも再現するので、以下のPHPプログラムでテストすることにする。
<?php error_log("ERRORTEST"); ?>

環境はUbuntu 14.04にnginx + php5-fpm。設定値「error_log」を「syslog」にしておく。あまりバージョンに拘っていないので、リンクが適当だったりするが気になるかたは必要なバージョンのソースを見に行ってほしい。

ソースを追う

error_log()関数の流れ

くだんのerror_log()関数をPHPから呼び出すと、こんな感じで動く。

まず、error_logのPHP_FUNCTIONの記述がext/standard/basic_functions.cにある。ここが実体である_php_error_log_ex()を呼び出す。

ext/standard/basic_functions.cの_php_error_log_ex()は、switchで引数に応じたログ出力(メールなど)を行う。今回のmessageのみの呼び出しではdefault: にたどり着き、php_log_err()を呼び出す。

php_log_err()は、main/main.cにあり、ここで「error_log」変数のチェックを行い、値が「syslog」のときはphp_syslog()関数でsyslogに出力している。

if (!strcmp(PG(error_log), "syslog")) {
 php_syslog(LOG_NOTICE, "%s", log_message);
 PG(in_error_log) = 0;
 return;
}

基本的にメッセージを引き回しているだけで、ここまでに変わった文字列処理は見受けられない。

php_syslog()関数とは

であればポイントはphp_syslog()であろうと、php_syslog()の定義をphp_syslog.hに見つけるも、こうなっていた。

なんとphp_syslog()はsyslog()であった。つまり、PHP内で何か特別な処理をすることなく、そのままsyslog()関数にメッセージを投げているだけである。

syslog関数の使い方

こうなってくるとsyslog(3)が大事になる。manがあり、日本語が古くないようなので日本語のsyslog(3)を見てみる。

プロトタイプとしては、こうなっているようだ。
#include <syslog.h>

void openlog(const char *ident, int option, int facility);
void syslog(int priority, const char *format, ...);
void closelog(void); 

呼び出しとあわせると、priorityがLOG_NOTICEで、formatが"%s"、%s部分としてlog_messageが渡ることになる。

説明を読むと
ident で指定した文字列は各メッセージの前に付与される。
とあるので「ool www」の部分はidentのようだ。

処理の中でとくにopenlog()する処理は見受けられなかったので
openlog() は必須ではなく、必要に応じて syslog() から呼び出される。 syslog() が呼び出した場合、 ident のデフォルト値は NULL になる。
の部分に該当し、
通常は ident にはプログラム名が設定される。 ident が NULL の場合、プログラムが ident として使用される
とあるので「ool www」はプログラム名のようだ。しかし「ool www」なんてプログラムはいないぞ?

syslog()関数の実装

こうなると、identがNULLだったときのopenlog()の動きを知りたくなる。そこでglibcを見たところ、openlog()の実体はmisc/syslog.cにある。

openlog()は内部のopenlog_internal()を呼び、ここでidentはLogTag変数に代入される。

LogTagがNULLのときの処理を探してみると、syslog()のログ処理の実体である__vsyslog_chk()に以下の記述があった。

if (LogTag == NULL)
      LogTag = __progname;

__prognameは何かと見ると、頭のほうでexternで宣言されていた。
extern char *__progname; /* Program name, from crt0. */
だいぶあやしい。

「__progname」とは

で、誰が__prognameをセットしているのかと探してみると「misc/init-misc.c」にあった。

void
__init_misc (int argc, char **argv, char **envp)
{
  if (argv && argv[0])
    {
      char *p = strrchr (argv[0], '/');
      if (p == NULL)
         __progname = argv[0];
      else
         __progname = p + 1;
      __progname_full = argv[0];
    }
}
__prognameは「argv[0]の最後の'/'より後」のようだ。なるほどargv[0]の役目を考えると妥当そうに見える。__progname_fullとしてargv[0]も保持されている。

php-fpmのargv[0]ってなんだっけ?

phpが動作するのは、php-fpmプロセス。そういえばphp-fpmは独自設定した文字列がpsで見えてたよなと見てみると、こうなっている。

$ ps -ef | grep '[p]hp-fpm'
root      2664     1  0 22:02 ?        00:00:00 php-fpm: master process (/etc/php5/fpm/php-fpm.conf)                    
www-data  2666  2664  0 22:02 ?        00:00:00 php-fpm: pool www                                                       
www-data  2667  2664  0 22:02 ?        00:00:00 php-fpm: pool www

php-fpmは明らかにargv[0]を書き換えている。勘のいい人ならもうお分かりだろう。一応検証するために、PHPを適当に展開してfpm.cのfpm_init()の頭と末尾(argvが書き換わる前と後)に以下のようにログを仕込んでみる(ヘッダやexternは適宜挿入)と...

zlog(ZLOG_NOTICE, "argv[0]: %s", argv[0]);

zlog(ZLOG_NOTICE, "__progname: %s", __progname);
zlog(ZLOG_NOTICE, "__progname_full: %s", __progname_full);

こんな風になる。

$ sudo ./sapi/fpm/php-fpm -F -y /etc/php5/fpm/php-fpm.conf 
[08-Feb-2016 00:25:23] NOTICE: argv[0]: ./sapi/fpm/php-fpm
[08-Feb-2016 00:25:23] NOTICE: fpm is running, pid 24381
[08-Feb-2016 00:25:23] NOTICE: __progname: ster process (/etc/php5/fpm/php-fpm.conf)
[08-Feb-2016 00:25:23] NOTICE: __progname_full: php-fpm: master process (/etc/php5/fpm/php-fpm.conf)
もう少しわかりやすくすると、こう。


そう、元々あった最後の/までの文字数分、新しい文字列の開始からスキップされている。ログ出力がだるいので例ではmaster processになっているが、pool wwwでどうなるかを想像するのはたやすいと思う。

結論:syslog tagに使われる__prognameを、プログラムパスであるargv[0]からディレクトリ部分を取り除いた文字列へのポインタとしてlibcが保存したあと、php-fpmがargv[0]を書き換えたため、謎の文字列「ool www」が生まれた。

一応Bug reportは作っておいた。PHPが対処すべき問題のように感じるけど、libcが文字列をコピーしておかないのが悪いって言われるとそうすかって感じなので、正直期待できない:https://bugs.php.net/bug.php?id=71544

回避策:identに妥当な値を設定するには?

暗黙のopenlog()をさせなければいい。つまり、期待のプログラムが動く前にopenlog()すればよい。

具体的には、以下のようなファイルを例えば/etc/php5/fpm/fixsyslog.phpと置いて……

php.iniのauto_prepend_fileで参照する。
auto_prepend_file = /etc/php5/fpm/fixsyslog.php

これで常にopenlog()が呼ばれるので現象は起こらない。

感想

原因に関しては、ソースを追うことですっきりさせることができた。オープンソースのありがたみって、やっぱりこういうところにあると思う。解決策は、もやもやが残るままだけど…

おまけ:php-fpmはどのようにargv[0]を書き換えているか?

php-fpmは見ての通りargv[0]を書き換えるのだけど、argvを全部足しても期待の文字数に足りないなんてこともあるはず。そんなときの処理がfpm_env.cに書かれていた。

ざっくり言うと「environがargvとメモリ空間上に連続で置いてあるから、一応確認しつつenvironにどいてもらって、そこをargv[0]として使いましょう」である。グロい!コメントによると、nginxやpureftpdもそうしているらしい。

この記事も最後なので該当コードを貼り付けておく。


2016-01-25

Ansibleを複数ホストへのコマンド投げツールとしてad-hocに使う

Ansibleは普通「playbook」と呼ばれるファイルを記述し、それを使って動かすのだけど、もっとてきとーに使っても便利なときは便利なので、今日はその使い方の紹介。実行する側は、ansibleがインストールされている必要はあるが(apt-get install ansibleでよい)、対象ホストはsshでアクセスできればOK。

複数ホストにコマンドを投げる

シンプルな例: 適当なファイルをcatする

もっとも理解が簡単なのは、権限だとかは気にせずにただコマンドが実行できればよい、という使い方。サーバがたくさんあるんだけど、ひとつひとつ実行するのはダルい。そんなときは、こんな風に実行すると、実行して結果を並べてくれる。

$ ansible -a "cat /etc/hostname" -i host1,host2 -u yoichi all
host1 | success | rc=0 >>
host1

host2 | success | rc=0 >>
host2

-aにコマンドを指定し、-iで対象ホストをカンマ区切りで並べ、-uにユーザを指定し、allをつける。

ユーザの指定なくsshできる状態なら、-uはなくてもいい。また、playbookを書いていてinventory fileを持っているなら、そのファイルを-iで指定したほうが当然楽。この例ではありがたみが全然ないが、もし1ホストにしたければ「-i hostname,」のように最後に,をつける。

sudoする例: crontabに設定された内容を見る

Ansibleをansible-playbookコマンドで使っている人にはお馴染みの、-sでsudo(-Kでパスワード)、-Sでsuさせることもできる。例えば、crontabに設定された項目を見たい、とか。
$ ansible -s -a "crontab -u root -l" -i host1,host2 all
host1 | FAILED | rc=1 >>
no crontab for root

host2 | success | rc=0 >>
# Edit this file to introduce tasks to be run by cron.
(略)
# m h  dom mon dow   command
30 5 * * * /usr/local/bin/rotate.sh

切り離す例: 一斉シャットダウン

停電準備で一斉シャットダウンしたいです、とかいうこともあると思う。shutdownのケースでは、結果を待てないので-Bオプションを使ってバックグラウンドで切り離す。

$ ansible -s -K -B 1 -a "shutdown -h now" -i host1,host2 all
SUDO password: 
background launch...

host1 | success >> {
    "ansible_job_id": "867312552566.13878",
    "results_file": "/root/.ansible_async/867312552566.13878",
    "started": 1
}

host1 | success >> {
    "ansible_job_id": "867312552566.2418",
    "results_file": "/root/.ansible_async/867312552566.2418",
    "started": 1
}

Ansibleのモジュールを活用する

上述の例はすべてコマンドだったが、当然ながら、Ansibleのモジュールも活用できる。

モジュール活用例1: サービスを再起動させる

例えば、サービス再起動では、serviceコマンドでもいいが、serviceモジュールを使うとこのようになる。

$ ansible -s -m service -a "name=myservice state=restarted" -i host1,host2 all
host1 | success >> {
    "changed": true,
    "name": "myservice",
    "state": "started"
}

host2 | success >> {
    "changed": true,
    "name": "myservice",
    "state": "started"
}

-mでモジュール名を指定し、-aでモジュールの引数を指定する。実は-aはいつでもモジュールの引数なのだが、-mのデフォルトが「command」なので、コマンドを実行できるというわけだ。

モジュール活用例2: copyモジュールでファイルを配置する

ad-hocに使う例ではない気がするが、その気になればファイルを持っていくことも可能。

$ echo hoge > hogefile
$ ansible -m copy -a "src=hogefile dest=/tmp/hogefile1" -i host1,host2 all
host1 | success >> {
    "changed": true,
    (略)
}

host2 | success >> {
    "changed": true,
    (略)
}

注意

あくまで一回限りで使うようなものであって、残したいものだったり、ansibleコマンドを3つも4つも打つようであれば、playbookを作るのがよい。上述のコマンドを羅列すればyaml書かなくても呼べるじゃん?っていうのはナシで。ベストプラクティス構成にしておくのがよいと思うが、1ファイルplaybookでもまぁansibleコマンドがたくさん書いてあるよりは良いと思う。

参考

こういうのをad-hoc commandsと呼ぶらしい→Introduction To Ad-Hoc Commands
他にも例がいくつかあるので、参考にされたい。

おわりに

「ansible」というソフトウェア名がついているコマンドがこの動作であるところから、もともとはこういう風に使うように始まったのではないかと私は勝手に思っている。現時点ではメインではない使い方になった(と思う)ので、知らなくても全然使えないことはないが、ベースの考え方はこのエージェントなしにリモートホストに任意にモジュールを投げ込んで実行するツールだと思うと、理解も応用もしやすいと思うので、触ってみておいて損はないかなと思う。

2016-01-10

avahi-browse で mDNS/DNS-SD のホスト・サービスの探索をする

apt-get install avahi-utilsして「avahi-browse -alr」とすればよい。たいした話ではないけれど、多分忘れるので。

出力例はこんな感じ。
$ avahi-browse -alr
+   eth0 IPv6 sample [00:11:22:33:44:55]                      Workstation          local
+   eth0 IPv4 sample [00:11:22:33:44:55]                      Workstation          local
+   eth0 IPv4 Jenkins                                         Web Site             local
+   eth0 IPv4 jenkins                                         _jenkins._tcp        local
+   eth0 IPv4 jenkins                                         _hudson._tcp         local
=   eth0 IPv6 sample [00:11:22:33:44:55]                      Workstation          local
   hostname = [sample.local]
   address = [fe80::82ee:1234:5678:9999]
   port = [9]
   txt = []
=   eth0 IPv4 sample [00:11:22:33:44:55]                      Workstation          local
   hostname = [sample.local]
   address = [192.168.1.152]
   port = [9]h
   txt = []
=   eth0 IPv4 Jenkins                                       Web Site             local
   hostname = [jenkins.local]
   address = [192.168.1.101]
   port = [8005]
   txt = ["version=1.643" "url=http://jenkins.example.local:8005/" "slave-port=46298" "path=/" "server-id=5ca870f7e5eb41888299af4853b2021b"]
=   eth0 IPv4 jenkins                                       _jenkins._tcp        local
   hostname = [jenkins.local]
   address = [192.168.1.101]
   port = [8005]
   txt = ["version=1.643" "url=http://jenkins.example.local:8005/" "slave-port=46298" "path=/" "server-id=5ca870f7e5eb41888299af4853b2021b"]
=   eth0 IPv4 jenkins                                       _hudson._tcp         local
   hostname = [jenkins.local]
   address = [192.168.1.101]
   port = [8005]
   txt = ["version=1.643" "url=http://jenkins.example.local:8005/" "slave-port=46298" "path=/" "server-id=5ca870f7e5eb41888299af4853b2021b"]

2015-12-20

PowerShellでスクレイピングする (AngleSharp編)

これは、PowerShell Advent Calendar 2015の12/20分の記事です。Invoke-WebRequestとIEエンジンによるDOM処理とは違うやり方について書きます。

---
PowerShellを使うと、Invoke-WebRequestを使うことで、特にライブラリ等を準備せずともWebページをスクレイピングすることができる(参考:PowerShellでスクレイピング 後編 HTMLをパースする) 。

これはこれで便利なのだが、少し複雑なHTMLになると、Where-ObjectやSelect-Objectがだんだん増えてきてしまう。こんな感じである。



やっぱりDOMツリーをたどるなら使い慣れたCSSセレクタの記法が使いたいよね?ということで、今回はCSSセレクタが使える.NET用HTMLパーサライブラリ「AngleSharp」をPowerShellから使ってみるお話。本当はC#で馴染みのあったCsQueryを使おうと思ったのだけど「Not Actively Maintained」ということでAngleSharpにしてみた。

インストール

例によって適当なフォルダに移動してnuget.exeを使ってダウンロードする。
> nuget install AngleSharp

使い方

初期化

まず、AngleSharp.dllをロードし、loaderが設定されたconfigを使って、BrowsingContextを作っておく。

このBrowsingContextを使っていく。

GETリクエスト

シンプルにGETリクエストを出すには、文字列のURLを使ってOpenAsyncを呼び出す。そして返ってきたものに対して、最初に一致したものだけでよければQuerySelector()を、全て欲しい場合はQuerySelectorAll()を呼び出す。

例として、このブログの右側の「ブログ アーカイブ」の一番上のタイトル(画像参照)を取得するにはこんな感じである。



~Asyncは、流儀通りTask<T>が返ってくる。PowerShell世界では扱いに困るのでResultでひたすらブロックしていく(ひどい)

ほぼ同じであるが、もう一つの例として@mutaguchiさんのブログPowerShell Scripting Weblogの右側にあるMVPのタイトルを取得するにはこんな感じである。


各ブラウザの開発者ツール(F12)のコンソールで、document.querySelector('#ArchiveList')などとして、先に試しておくとよい。

POSTリクエスト

POSTにするには、OpenAsync()の第2引数にリクエストオブジェクトを生成する。POSTリクエストを作る便利メソッドはいくつかあり、例えばPostAsUrlEncodedを使うとこんな感じである。

$docができたら、あとはGETと同様に、QuerySelector()やQuerySelectorAll()を呼び出せばOKである。

感想

これで一応、AngleSharpをPowerShellから呼ぶことはできた。Where-Objectを連発するよりは見通しのよいものが書けると思う。ただし、AngleSharpの呼び出す部分は拡張メソッドだらけになっており、PowerShellから大変に呼びにくいので、うーんという感じ。やはりInvoke-WebRequestが、querySelector / querySelectorAllできれば万事OKだったのに、という感じであった。

2015-12-13

Loqui 0.6.4 リリース

今回のリリースは主に、昨晩の「Ubuntu 15.10 リリース記念オフラインミーティング 15.12」のあとの飲み会で、@ut_maito氏が初めて使っているところを見て、使いにくそうに見えた部分のうち、さらっと改善できそうな部分についての対応です。バグを見つけたので併せて直しました。

主な変更点はこんなかんじ。

* Shift+Enterで送信する機能の追加
→複数行モードのときキーボードで送信する手段がなかったので送信できるようにした。すでにCtrl+Enterは1行モード時にNOTICEとして送信にしていたので、Shift+EnterでPRIVMSGで送信。1行の側もShift+Enterで送信できるようにして、どちらもCtrl+EnterでNOTICE / Shift+EnterでPRIVMSGになるよう統一した。

* 初回起動時にアカウント設定ダイアログを表示
→最初に何していいかわからない感じが強く出ていたので、手っ取り早くできそうなところで、最初に使うことになるアカウント設定ダイアログを出すことにした。

* アカウント削除時にクラッシュするバグの修正
→アカウント設定の検証をしていたら、アカウント削除で落ちることがあるバグを見つけたので修正。

* アカウント設定からプロトコル選択の削除
* 不要な「ツール」メニューの削除
→拡張用に若干複雑なUIになっていたのだけど、多分追加しないので削除。プロトコル選択画面で選べるIP Messenger機能は若干実装されていたのだけど、使っている人はいないよね。


初回起動時はチャンネルバッファの部分が余っているので、ここにガイド的なことを書いてあげたいなぁと思ったのだけど、だいぶ拡張がいりそうなので見送り。

キー(Ctrl+↑/Ctrl+↓)でチャンネルを選択すると、チャンネルツリーの選択部分がわかりにくいという話もあったのだけど、直そうとするとややこしそうなので見送り。手元のCinnamonだと起きないので、テーマに依ってしまう部分もある。

こんなんだけど、ちょっとでもわかりやすくなってるといいな。

2015-12-06

HDDが故障した話 (QNAP TS-219PにDebian jessieをインストールする)

多分5年以上使っていたNAS, TS-219PについているHDDの2台のうち片方が壊れた。Software RAID1構成なので、交換してリビルドすればいいという話だったはずなのだが、長期戦になってしまった。

今回は時系列に沿って。日数はフィクションです。仕事の話じゃなくてよかった(仕事だったらもっと慎重だったろうから、こんなんにならないと思うけど)。

1日目: 拡張できるはずだったのに

もともと2TB x 2本の構成だった。もちろん2TBを買ってきて取り替えればよい。だが、容量を拡張できる機能があることがわかったので、2015年現在のGB単価的(→サハロフの秋葉原レポート)に4TBにしようと思って4TBを2本買ってきた。

もちろん、時代的に4TB扱えなくてもそういうものかという感じな気がするので、compatibility listを見て4TBに対応していることを確認して買った。

ところが、4TBを差しても認識しない。ファームウェアがだいぶ古いので、アップグレードしてみることにする。

2日目: ファームウェアアップグレード

ファームウェアアップデートすると、今度は生きている2TBのディスクまでWeb管理画面から見えなくなってしまった。sshログインを使って、cat /proc/filesystemsを見ると、ext4がいない。ファームウェアの更新がうまくいかなかったのかなと思って再度更新をかけようとすると、バージョンを確認しろという旨のエラーが出て受け付けてもらえない。

バージョンがとんでもなく変わっていたので、変化に耐えられなかったのであろう。もうだめだ。ブラックボックスな独自OSで戦うのはここで限界。汎用OSで使えるものを探そう。

3日目: Debianインストール チャレンジ

Debianが使えないかなと調べてみると、Martin Michlmayr さんという方がまとめてくれている。手順まで詳しく載っており、これは戦えそうである。
→Debian on QNAP TS-21x/TS-22x

インストール手順はInstalling Debian on QNAP TS-21x/TS-22xにある通り、

まずフラッシュ領域をバックアップする(後にわかることだが、これは大変に重要であり、絶対に避けてはいけない。)

vfatもマウントできない状態なので、別のマシンでUSBメモリをext2でフォーマットしておいたものを挿入し、catでコピーする。
mount /dev/sdX1 /tmp/usb
cd /tmp/usb
cat /dev/mtdblock0 > mtd0
cat /dev/mtdblock1 > mtd1
cat /dev/mtdblock2 > mtd2
cat /dev/mtdblock3 > mtd3
cat /dev/mtdblock4 > mtd4
cat /dev/mtdblock5 > mtd5
cd
umount /tmp/usb

そうしたら、カーネルやインストーラの入ったinitrdをダウンロードし、flash-debianスクリプトを起動する。
cd /tmp
busybox wget http://ftp.debian.org/debian/dists/stable/main/installer-armel/current/images/kirkwood/network-console/qnap/ts-219/initrd.gz
busybox wget http://ftp.debian.org/debian/dists/stable/main/installer-armel/current/images/kirkwood/network-console/qnap/ts-219/kernel
busybox wget http://ftp.debian.org/debian/dists/stable/main/installer-armel/current/images/kirkwood/network-console/qnap/ts-219/flash-debian
busybox wget http://ftp.debian.org/debian/dists/stable/main/installer-armel/current/images/kirkwood/network-console/qnap/ts-219/model
sh flash-debian

リブートするよう促されるので、rebootした。
reboot

DHCPでアドレスが払い出されるので、そのアドレスに対してinstallerユーザでsshログインする。
$ ssh installer@192.168.1.XX

cursorsのUIが起動し、インストーラが開始する。あとは選べばよいだけ……のように見えた。

4日目: インストーラが途中で止まる

普通のDebianインストーラだと安心し進んでいくと、パッケージのインストールのプログレスバーが途中で止まってしまう。最初からやり直しても同じ。

何度かディスクの構成を変えながら試してみても変わらないので、もう一つinstallerユーザでsshし、shellを選び/var/logのログを見ていると「UUID ~ doesn't exist in /dev/disk/by-uuid」なるログが。

そのキーワードを基に検索してみると、こんなバグ報告を発見。
#791794 - INSTALL REPORT (Jessie on QNAP TS-420U)

どうやらmdでミラーを組んでしまうと、/dev/disk/by-uuidのIDが更新されず、flash-kernelという裏で動くmtdデバイスにカーネルイメージを書き込むツールが書き込みに失敗するらしい。このツールがエラー終了するのではなく、Ctrl+C待ちで止まってしまっているので、フロントのインストーラのプログレスが止まったままとなっていたようだ。

書いてある感じに、ディスクの構成が終わったタイミングでもうひとつssh接続し、shellに降りて
udevadm tigger
とすることで、進むようになった。

このバグで触れられていたために、mdで作られたRAIDミラーがdegradedだと、起動時にemergency shellに落ちるという恐怖のバグを発見。通常コンソールのないこのNASでは、sshできない状態に落とされるのは致命的である。

#784070 - mdadm Software RAID1 with GPT on Debian 8.0.0 amd64 - Does not mount/boot on disk removal

おすすめされる行為ではないが、sidには修正済みのパッケージがあったため、バージョンの変化やdependencyなどを見てsidから持ってきて入れた。実際に1本外して試したところ、きちんと立ち上がるようになっていた。

LVMを構成したり、NFSやSambaなどなどNASっぽいデーモンをインストールしたりする。Ansibleで作ったのでやりなおせる。

5日目: rsyncデータコピー待ち

USB接続した元々の2TB HDDからrsyncでデータをコピーする。たいへんに時間がかかるが、待つしかない。

6日目: 再起動したら……

データのコピーも終わり、デーモンの動作もOK, 念のため再起動を試しておく。

……立ち上がらない。コンソールがないので状況もわからない。

11/30: フラッシュ・リカバリ

Martinさんはリカバリの方法も書いてくれていたので、QNAP NASのリカバリの手順でインストーラを起動して中の様子を見てみることにする。

Recovery mode of QNAP TS-21x/TS-22x

TS-219Pのリカバリモードは、ブート時にtftpでリカバリイメージを取得し、フラッシュ領域を書き直す。

debian-installerを動作させるイメージを作るには、まずカーネルイメージにpaddingを加えて所定のサイズにする。
dd if=kernel of=kernel.pad ibs=2097152 conv=sync

あとはinitrd.gzと、もともとのmtdblockイメージを連結して作る。取っててよかったバックアップ!
cat mtdblock0 mtdblock4 mtdblock5 kernel.pad initrd.gz mtdblock3 > F_TS-219

このファイル名は、u-boot パラメータが格納されているmtdblock4に入っているので念のため確認しておく。
strings mtdblock4 | grep bootp_vendor_class

次に、適当なノートPCを使ってDHCPサーバ + TFTPサーバを立てる。Martinさんは普通にdhcpdで立てているようだが、dnsmasqはそのどちらの機能も備えており、さくっと立てることができた。

あとは、NASをdnsmasqを立てたノートPCと直結し、背面にあるLANコネクタの隣あたりにあるピンで押すリセットボタンを押しながら、電源ボタンを押して起動する。すると、ピー、ピーと短く鳴って起動する。

その時のコマンド・ログはこんな感じだった。
$ sudo dnsmasq --no-daemon --port=0 --interface=eth0 --domain=example.com \
  --dhcp-range=192.168.0.3,192.168.0.253,255.255.255.0,1h --dhcp-boot=F_TS-219 \
  --enable-tftp --tftp-root=`pwd`
dnsmasq: started, version 2.75 DNS disabled
dnsmasq: compile time options: IPv6 GNU-getopt DBus i18n IDN DHCP DHCPv6 no-Lua TFTP conntrack ipset auth DNSSEC loop-detect inotify
dnsmasq-dhcp: DHCP, IP range 192.168.0.3 -- 192.168.0.253, lease time 1h
dnsmasq-tftp: TFTP root is /tmp/ts219
dnsmasq-dhcp: DHCPDISCOVER(eth0) 00:08:9b:XX:XX:XX
dnsmasq-dhcp: DHCPOFFER(eth0) 192.168.0.216 00:08:9b:XX:XX:XX
dnsmasq-dhcp: DHCPREQUEST(eth0) 192.168.0.216 00:08:9b:XX:XX:XX
dnsmasq-dhcp: DHCPACK(eth0) 192.168.0.216 00:08:9b:XX:XX:XX
dnsmasq-tftp: sent /tmp/ts219/F_TS-219 to 192.168.0.216
--no-daemonで起動すると、わざわざtail -fしなくてもログが追え便利であった。

10分程待つと再びピー、ピーと音がなり、再起動される。すると、無事に同じようにinstaller@で接続できるようになった。

インストーラが起動したら、ディスクの設定画面まで進めたあと、別のssh接続を行い、shellに降りる。shellが起動したら、以下のようにchrootを実行して、ディスク上の環境を得る。

mount /dev/md0 /mnt/md0
mount --bind /dev /mnt/md0/dev
mount --bind /proc /mnt/md0/proc
mount --bind /sys /mnt/md0/sys
chroot . bin/bash

fstabがおかしいかなと新しく作ったエントリをコメントアウトしたりした。終わったら、元のカーネルに戻すべくフラッシュする。
flash-kernel

rebootすると、無事起動した。原因についてははっきりわかっていないが、おそらく、fstabうんぬんではなく、USB接続したディスクをmdデバイスとして見せた状態でinitrdを更新してしまったために、変な情報を覚えられてしまったのかなと思っている。ディスクが見えなくなるから起動しなくなるかもよみたいなことを書いてあった記憶。

7日目: USBシリアルからコンソール出せれば便利じゃね?

ブートオプション変更すればUSBシリアルにコンソール出せるんじゃね?このデバイスはU-Boot使ってるからfw_setenvでブートオプション変更できるんじゃね?と思ったらやっぱりあった。

U-Boot tools for Debian ARM Linux in QNAP Server | Black God

ダメでもどうせフラッシュすればいいやーと思って書き換えてみることにする。

まずは設定。
$ cat /proc/mtd
dev:    size   erasesize  name
mtd0: 00080000 00040000 "U-Boot"
mtd1: 00200000 00040000 "Kernel"
mtd2: 00900000 00040000 "RootFS1"
mtd3: 00300000 00040000 "RootFS2"
mtd4: 00040000 00040000 "U-Boot Config"
mtd5: 00140000 00040000 "NAS Config"
# vi /etc/fw_env.config
(書き込み)
# Configuration file for fw_(printenv/saveenv) utility for
# QNAP TS-119, TS-219 and TS-219P.

# MTD device name Device offset Env. size Flash sector size
/dev/mtd4 0x0000 0x1000 0x40000

確認。
# fw_printenv

ttyUSB0を使うように書き換え(コマンドはイメージです:注:このデバイスでのコンソールアクセスがない状態での書き換えはやってはいけません)
# sudo fw_setenv bootargs 'console=ttyS0,115200 root=/dev/ram initrd=0xa00000,0x900000 ramdisk=32768'

再びfw_printenvを実行してみると、書き換わっている。

いざ再起動……立ち上がらない。まぁいいや、リカバリならもう慣れた……リカバリ完了。立ち上がらない!?

8日目: カーネルビルドという選択肢

試しにPCでUSBシリアルを接続し、grubからconsole=ttyUSB0として起動しようとしてみると、起動しない。先にやっておくべきだった!

でもU-Bootのコンフィグが入っているのは先に見たようにmtd4。フラッシュイメージにはmtd4が入っている。直るはずだ!

とMartinさんのページをよく見てみると、こんな記述が。
During recovery mode, mtd0 (the boot loader), mtd4 (the boot loader configuration) and on some devices mtd5 (device configuration) are ignored
……なぜここを読み飛ばしてしまっていたのか。mtd4はリカバリでは戻らない。やってしまった……

U-Bootのコンソールを叩きにいけば直るはずなので、シリアルコンソールに接続してコマンドを叩けば直る。でも現状その接続はない。そこでもう一つの選択肢。設定されたbootargsで立ち上がるカーネルを作ればよい。

ということで、活躍していないsqueezeのarmelデバイスがあったので、これを使ってlinux-sourceをダウンロード。中に入っているはずのカーネルパッケージに含まれるconfigを見ると、どうもCONFIG_USB_SERIAL=mであるのが影響していそうだ。

ということで、=yになるようmake menuconfigでちょいちょいといじって、カーネルを作り直す。起動しない。linux-sourceからのカーネルビルドの手順にまったく沿っていないためか、そもそも元のconfigで作り直しても全然違うサイズになる。やり方が雑過ぎたようだ。

9日目: クロスビルド環境でのカーネルコンパイル(失敗)

make menuconfigを眺めていると、ブートオプションを強制するオプションがあった。.config的にはこんな感じである。
CONFIG_CMDLINE="console=ttyS0,115200 root=/dev/ram initrd=0xa00000,0x900000 ramdisk=32768"
CONFIG_CMDLINE_FORCE=y

なおビルドについては、armelデバイスが古すぎるためか、一晩寝てもコンパイルが完了しない。これでは時間がかかりすぎるので、クロスビルドするしかないか、と思って適当なページを見ながら、適当なUbuntu 15.10のマシンを使ってコンパイルするも、コンパイルエラー。まともに環境を作ろうとすると大変そうなのでこのビルド方式はあきらめ。
How do I cross-compile the kernel on a Ubuntu host? - Raspberry Pi Stack Exchange

$ sudo apt-get install git ncurses-dev make gcc-arm-linux-gnueabi
(カーネルとconfigを配置/展開)
make ARCH=arm CROSS_COMPILE=/usr/bin/arm-linux-gnueabi- oldconfig
make ARCH=arm CROSS_COMPILE=/usr/bin/arm-linux-gnueabi- menuconfig
make ARCH=arm CROSS_COMPILE=/usr/bin/arm-linux-gnueabi- zImage

10日目: そもそもarmelをエミュレーションした環境でビルドする(途中)

そういえばx86なマシンでもqemu使えばarmel環境をエミュレーションできるんじゃね?ということで環境を作る。
qemu-debootstrapを使ってユーザモードQEMUで動くDockerイメージを作ってみる - ももいろテクノロジー

$ sudo apt-get install debootstrap qemu-user-static
$ sudo qemu-debootstrap --verbose --arch=armel --variant=buildd \
  --include=vim-tiny,less jessie rootfs-debian-armel http://http.debian.net/debian/
$ sudo chroot rootfs-debian-armel

今度こそjessieになったのでHowToRebuildAnOfficialDebianKernelPackageの手順を使ってビルドしてみる。armel指定の部分はこんな感じ。

# fakeroot make -f debian/rules.gen setup_armel_none_kirkwood
# fakeroot make -f debian/rules.gen binary-arch_armel_none_kirkwood binary-indep \
DEBIAN_KERNEL_JOBS=4

普通に進んでいたので、このままほっとけばできたのかもしれないが、4つコアを使っている感じがなく時間がかかりすぎて作業時間オーバー。とりあえず断念。

11日目: そしてシリアルコンソールへ

カーネルビルド方式では、リカバリまで含めての試行錯誤の1ターンがかかりすぎるので、ついにシリアルコンソールに手を出すことにした。ここで失敗するとハードウェア破壊を意味するので、あまりやりたくなかったのだ。

やりかたはMartinさんがピン配置を書いてくれているので、この通り接続する。
Serial console for QNAP TS-21x/TS-22x

コネクタはPHR-4というもので千石電商の2Fにあったので買ってケーブルとピンヘッダに差すコネクタをハンダ付けしてケーブルを作った。

RS-232Cレベルではなく、3.3Vなので昔買ったFT232RLの変換アダプタにつなげばよさそうだなーと思ったが、Raspberry Piのシリアルピン使えるんじゃね?とRasPiの存在を思いだし、RasPiが登場。TS-219P - RasPiをTx - Rx, Rx - Tx, GND - GNDで接続した。

RasPi側ピン
(https://www.raspberrypi.org/documentation/usage/gpio/)より

TS-219P側ピン

黄: Tx
黒: Vcc (接続しない)
赤: Rx
青: GND


シリアルポートの場所はここ。ケースは固いので開けたくなかったが、開けないと手が届かない。

RasPiは、ブートオプションconsole=に当該ポートが指定されていて使えないので、/boot/cmdline.txtを編集してconsole=ttyAMAの記述をトル。
http://elinux.org/RPi_Serial_Connection
console=パラメータの変更はまさに悩んでいる部分なので、こうさらっと変更できると複雑な気分。

あとは起動して、
$ cu -s 11200 -l /dev/ttyAMA0
として待つ。あとはTS-219Pをブートすればコンソールが現れるのでauto-bootを止める。あとはsetenv, saveしてブート。
         __  __                      _ _
        |  \/  | __ _ _ ____   _____| | |
        | |\/| |/ _` | '__\ \ / / _ \ | |
        | |  | | (_| | |   \ V /  __/ | |
        |_|  |_|\__,_|_|    \_/ \___|_|_|
 _   _     ____              _
| | | |   | __ )  ___   ___ | |_ 
| | | |___|  _ \ / _ \ / _ \| __| 
| |_| |___| |_) | (_) | (_) | |_ 
 \___/    |____/ \___/ \___/ \__|  ** LOADER **
 ** MARVELL BOARD: DB-88F6281A-BP LE 

U-Boot 1.1.4 (Jan  5 2009 - 12:58:51) Marvell version: 3.4.4

U-Boot code: 00600000 -> 0067FFF0  BSS: -> 00690DCC

Soc: MV88F6281 Rev 3 (DDR2)
CPU running @ 1200Mhz L2 running @ 400Mhz
SysClock = 400Mhz , TClock = 200Mhz 
(略)
Hit any key to stop autoboot:  0 
QNAP: Recovery Button pressed: 0
Marvell>> printenv
(略)
Marvell>> setenv bootargs console=ttyS0,115200 root=/dev/ram initrd=0xa00000,0x900000 ramdisk=32768
Marvell>> saveenv
Saving Environment to Flash...
................................................................
.
Un-Protected 1 sectors
Erasing Flash...
.
Erased 1 sectors
Writing to Flash... done
................................................................
.
Protected 1 sectors
Marvell>> printenv
baudrate=115200
(略)
bootargs=console=ttyS0,115200 root=/dev/ram initrd=0xa00000,0x900000 ramdisk=32768

Environment size: 1339/4092 bytes
Marvell>> boot
Unknown command 'uart1' - try 'help'
## Booting image at 00800000 ...
   Image Name:   
   Created:      2015-12-02  15:12:54 UTC
   Image Type:   ARM Linux Kernel Image (uncompressed)
   Data Size:    1968432 Bytes =  1.9 MB
   Load Address: 00008000
   Entry Point:  00008000
   Verifying Checksum ... OK
OK

Starting kernel ...

Uncompressing Linux... done, booting the kernel.

無事起動。これでちゃんと起動するDebian環境が得られた。

おわりに

USBシリアルでコンソールを見ようというのは本当に余計だった。

自分の環境ではこんな目にあってしまったが、今まで見てきたARMデバイスの中ではDebian officialで済む範囲が多いデバイスなので、Debianを入れてみるというのはアリだと思う。カーネルイメージは公式のものを使えなかったり、ファンコントロールは外部だったりといったことが多いが、これはどちらもofficialに含まれている。

もちろんメーカーの力は得られなくなるので、いわゆる自己責任で。新しく買ってきてというよりは、昔の保証切れてそうな箱でやってみるという感じがよいかとは思う。

2015-11-24

Linuxを使ってUEFIシステムのWindowsをセーフモードで起動させる

しばらくセーフモードのお世話になっていないうちに、セーフモードはF8で起動するものではなくなっていたらしい。セーフモードを構成してからシャットダウンしてセーフモードとか。回復ディスクがなく、Windowsが起動しない場合で、明示的にセーフモードで起動したいときはどうしたらいいんだろう?ということで、Linuxを使ってセーフモードを明示的に構成する方法について今回はまとめた。

注意:PCメーカーやMicrosoftの推奨する方法ではありません。壊れにくいような手順は取っていますが、重要なシステムに適用することはおすすめしません。普通に回復ディスクを使うのが楽だと思います。どちらかというと、仕組みを読み取ってもらえれば幸いです。

手順の概要

最近のWindowsの起動時の設定はBoot Configuration Data (BCD) というところに格納されており、ここにセーフモードで起動するかどうかの設定も入っている。そこで、このBCDをUSBメモリからブートしたUbuntuを使って書き換えることで、Windowsをセーフモードで起動させる。

なおBCD書き換えの際、標準外のツールを使うため、ファイルが壊される可能性を考慮して、セカンダリBCDストアを構成し、そちらの設定を変更することにする。

図にすると以下の通り。

環境/必要なもの

対象の環境は、UEFIで稼動するWindows。Windows 10で検証したが、Vista以降ならいけると思う。

また、必要なものはUbuntu 14.04 (or later) のブータブルUSBメモリ や DVD。すでにデュアルブート環境なら、それでも問題ない。

一応、検証した環境(VirtualBox)でのbcdeditコマンドの出力結果はこんな感じ。
Windows ブート マネージャー
--------------------------------
identifier              {bootmgr}
device                  partition=\Device\HarddiskVolume2
path                    \EFI\Microsoft\Boot\bootmgfw.efi
description             Windows Boot Manager
locale                  ja-JP
inherit                 {globalsettings}
default                 {current}
resumeobject            {43ea4f06-91dc-11e5-8faa-ac88953b08c0}
displayorder            {current}
toolsdisplayorder       {memdiag}
timeout                 30

Windows ブート ローダー
--------------------------------
identifier              {current}
device                  partition=C:
path                    \Windows\system32\winload.efi
description             Windows 10
locale                  ja-JP
inherit                 {bootloadersettings}
recoverysequence        {43ea4f08-91dc-11e5-8faa-ac88953b08c0}
recoveryenabled         Yes
isolatedcontext         Yes
allowedinmemorysettings 0x15000075
osdevice                partition=C:
systemroot              \Windows
resumeobject            {43ea4f06-91dc-11e5-8faa-ac88953b08c0}
nx                      OptIn
bootmenupolicy          Standard

手順

1) セカンダリブートパス・セカンダリBCDストアの構成

1-1) Ubuntuの起動

ブータブルUSBメモリなどでUbuntuを起動する。

1-2) EFI システムパーティション (ESP) のマウント

Ubuntuが起動したら、EFI システムパーティションをどこか適当な場所にマウントする。マウントしたら、EFI/Microsoft/Boot/BCDが存在することを確認しておく。
$ sudo parted /dev/sda print
モデル: ATA VBOX HARDDISK (scsi)
ディスク /dev/sda: 34.4GB
セクタサイズ (論理/物理): 512B/512B
パーティションテーブル: gpt

番号  開始    終了    サイズ  ファイルシステム  名前                          フラグ
 1    1049kB  473MB   472MB   ntfs              Basic data partition          hidden, diag
 2    473MB   578MB   105MB   fat32             EFI system partition          boot
 3    578MB   595MB   16.8MB                    Microsoft reserved partition  msftres
 4    595MB   34.4GB  33.8GB  ntfs              Basic data partition          msftdata

$ sudo mkdir /mnt/efi
$ sudo mount /dev/sda2 /mnt/efi
$ find /mnt/efi -name BCD
/mnt/efi/EFI/Microsoft/Boot/BCD     ←Microsoft/Boot/BCDが存在することを確認
/mnt/efi/EFI/Microsoft/Recovery/BCD

1-3) Microsoftディレクトリのコピー

EFIアプリケーションであるWindows Boot Manager (bootmgfw.efi)及びその設定であるBoot Configuration Data (BCD)が含まれるMicrosoftディレクトリをコピーする。
$ cd /mnt/efi/EFI
$ sudo cp -pr Microsoft MicrosoftSafeMode

手順参考:Windows RE を起動するためのハードウェア回復ボタンの追加

2) セカンダリBCDストアのBCDの書き換え

2-1) hivexregeditツールのインストール

設定が格納されているBCDは、レジストリハイブの形式をとっている。そこで、レジストリハイブを編集できる「hivexregedit」ツールを使えるようにする。

「hivexregedit」ツールは、libwin-hivex-perlパッケージに入っているので、これをインストールすればOK。
$ sudo apt-get install libwin-hivex-perl
なお、他にもレジストリハイブを扱うツールがlibhivex-binパッケージにいくつか入っているので、必要があれば入れる。

2-2) BCDファイルをレジストリエディタ形式にエクスポート

hivexregeditの--exportオプションを使って、BCDの内容を*.regファイルでおなじみの形式にエクスポートする。
$ hivexregedit --unsafe-printable-strings --export /mnt/efi/EFI/MicrosoftSafeMode/Boot/BCD '\' > /tmp/bcd.reg
これにより、テキストエディタやビューワで表示が可能になった。--unsafe-printable-stringsをつけないと、文字列が軒並みhex表記になって見れたもんじゃない。

2-3) Windows Boot Managerのobjectを探す

@junichia氏のブログエントリ「ブート構成データ(BCD)ストアを理解すれば VHDブート は簡単」に図があるので見て欲しいのだけど、BCDにはひとつのWindows Boot Managerのobjectと、複数のWindows Boot Loaderのobjectがある。Boot Loader側にsafe modeの設定もあるのだけど、まずはどれがdefaultで利用されているBoot Loaderか確認するため、Boot Manager側のobjectを探し出す。

boot managerのオブジェクトを探し出すには「bootmgfw.efi」を文字列検索するのが簡単。
$ less /tmp/bcd.reg
(/で検索)
[\Objects\{9dea862c-5cdd-4e70-acc1-f32b344d4795}\Elements\12000002]
"Element"=str(1):"\EFI\Microsoft\Boot\bootmgfw.efi^@"

[\Objects\{9dea862c-5cdd-4e70-acc1-f32b344d4795}\Elements\12000004]
"Element"=str(1):"Windows Boot Manager"

これで、boot managerのオブジェクトのGUIDが「{9dea862c-5cdd-4e70-acc1-f32b344d4795}」であることがわかった。

なお、各項目はElements\以下の数値で何か判断することが可能である。上記における「12000002」は「BcdLibraryString_ApplicationPath」でbcdeditコマンドでは「path」として表示される。同様に「12000004」は「BcdLibraryString_Description」でbceditコマンドでは「description」である。それぞれの数値が何を意味するかは、MSDNにも記述があるのだが、Geoff Chappell氏の「BCD Elements」が見やすくておすすめである。

2-4) Widnows Boot Managerのobjectから「default」を探し出す

Boot Managerがわかったので、そのGUIDを利用して、デフォルトのBoot Loaderの指定を探し出す。デフォルトのBoot Loaderが記述されているのはBcdBootMgrObject_DefaultObject (default)で「23000003」である。

$ less /tmp/bcd.reg
(/で検索)
[\Objects\{9dea862c-5cdd-4e70-acc1-f32b344d4795}\Elements\23000003]
"Element"=str(1):"{43ea4f07-91dc-11e5-8faa-ac88953b08c0}"

ここでデフォルトのBoot Loaderが{43ea4f07-91dc-11e5-8faa-ac88953b08c0}であることがわかった。一応、{43ea4f07-91dc-11e5-8faa-ac88953b08c0} のBcdLibraryString_ApplicationPath (path) 「12000002」及び BcdLibraryString_Description (description) 「12000004」を確認しておく。
$ less /tmp/bcd.reg
(/で検索)
[\Objects\{43ea4f07-91dc-11e5-8faa-ac88953b08c0}\Elements\12000002]
"Element"=str(1):"\Windows\system32\winload.efi"

[\Objects\{43ea4f07-91dc-11e5-8faa-ac88953b08c0}\Elements\12000004]
"Element"=str(1):"Windows 10"
これでいいようだ。

2-5) Boot Loader objectにセーフモード設定を組み込む

Boot Loaderのobjectにセーフモード関連のElementを追加する*.regファイルを作り、それをhivexregeditの--mergeオプションを使って組み込む。

項目がいくつかあるので後の表を見て欲しいが、例えばmsconfigで言うところのセーフモード: 代替シェルで起動する記述は以下の通りである。生成したbcd.regをコピーして作るのが楽。
$ cp /tmp/bcd.reg /tmp/bcd-safe.reg
$ vi /tmp/bcd-safe.reg
~~~
Windows Registry Editor Version 5.00

[\Objects\{43ea4f07-91dc-11e5-8faa-ac88953b08c0}\Elements\25000080]
"Element"=hex(3):00,00,00,00,00,00,00,00

[\Objects\{43ea4f07-91dc-11e5-8faa-ac88953b08c0}\Elements\26000081]
"Element"=hex(3):01
~~~
上記の例ではBcdOSLoaderInteger_SafeBoot (safeboot) 「25000080」を「Minimal」にし、BcdOSLoaderBoolean_SafeBootAlternateShell (やsafebootalternateshell) 「26000081」をtrueにしている。

以下は検証したわけではないが、msconfig.exeの「ブート」タブの表示(以下参考)とbcdeditの出力を見ながら、「BCD Elements」とつきあわせて表にまとめたものが以下となる。


msconfigの項目bcdeditの項目名称Element備考
-pathBcdLibraryString_ApplicationPath 12000002string
-descriptionBcdLibraryString_Description 12000004string
-defaultBcdBootMgrObject_DefaultObject23000003GUID
セーフブートsafebootBcdOSLoaderInteger_SafeBoot 250000800: 最小(Minimal) /
1: ネットワーク(Network) /
2: Active Directory修復(DsRepair)
(代替シェルのときのみ)safebootalternateshellBcdOSLoaderBoolean_SafeBootAlternateShell26000081boolean
GUI ブートなしquietbootBcdOSLoaderBoolean_DisableBootDisplay26000041boolean
ブートログbootlogBcdOSLoaderBoolean_BootLogInitialization 26000090boolean
基本ビデオvgaBcdOSLoaderBoolean_UseVgaDriver26000040boolean
OS ブート情報sosBcdOSLoaderBoolean_VerboseObjectLoadMode26000091boolean

regファイルを作ったら、--mergeでmergeする。
$ sudo hivexregedit --merge /mnt/efi/EFI/MicrosoftSafeMode/Boot/BCD /tmp/bcd-safe.reg

再度hivexregedit --exportしてみて、組み込まれていればOK。

2-6) umount

言うまでもないかもしれないが、umountしておく。
$ sudo umount /mnt/efi

3) grubから新しいBCDを使って起動

UbuntuをEFIで起動しているということは、EFIで動作するgrubが使えるということである。そこで、そのgrubを利用して上記で作ったセカンダリブートパス・BCDストアを選択して起動する。

再びUbuntu ブータブルUSBメモリで再起動し、grubメニューが現れたら、「c」キーを押しコンソールモードに切り替える。切り替わったら、以下のように入力して、新しいbootmgfw.efiを使って起動する。

grub> set root=(hd0,gpt2)
grub> chainloader /efi/MicrosoftSafeMode/Boot/bootmgfw.efi
grub> boot

hdのパスはPCによって異なるはずなので、以下の図のようにTAB補完を活用しながら入力するとよい。


これでセーフモードで立ち上がってくれば成功。とくにEFI firmware側の設定は変更していないので、ブータブルUSBメモリを外して普通に起動すれば元通り。気になるようであれば、EFIシステムパーティションから今回作ったディレクトリを削除しておけばOKである。