Jekyll2022-11-20T03:54:48+00:00https://msmania.github.io/feed.xmlすなのかたまりすべてはここから市民権取得のプロセス (N400) について2022-11-20T03:00:00+00:002022-11-20T03:00:00+00:00https://msmania.github.io/2022/11/20/n400-naturalization<h1 id="はじめに">はじめに</h1>
<p>数ヶ月前に市民権を取得し、アメリカでの私のステータスが Permanent resident から Citizen になりました。2013 年の夏に L1 ビザでこちらに来てから 9 年以上が経過したことになります。2004 年に大学入学に伴っていわゆる上京をした身なので、東京 (及び埼玉) での生活の期間よりアメリカでの生活が長くなろうとしています。感慨深いですね。</p>
<p>他の人の役に立つかどうかは分かりませんが、そのプロセスをまとめておきます。なお、移民弁護士は使わずに書類の準備から全て一人でやりましたが、特に難しそうなステップはありませんでした。USCIS のウェブサイトはとても親切にできており、申請もオンラインで可能なので、逮捕歴など特別な事情がある場合を除けばわざわざ弁護士を雇う必要はないと思います。もし弁護士を雇う場合、N400 のパッケージは $1,000 程度だと思います。</p>
<h1 id="タイムライン">タイムライン</h1>
<p>まずは気になるタイムラインですが、以下の通りでした。</p>
<ul>
<li>2022-02-02 - Form N-400 をオンラインで提出。その日のうちに受領の確認と、指紋は以前に採取したものを再利用するので今回は不要との連絡が来る。</li>
<li>2022-07-29 - 面接予約の通知。日時は約 2 ヶ月後の 9/16 8:00am。朝早い。</li>
<li>2022-09-16 - USCIS オフィスに行き面接。無事合格し、その日のうちに同じ場所で Oath ceremony に参加し、Certificate Of Naturalization 受領。</li>
</ul>
<p>したがって、書類提出から市民権取得まで 7 ヶ月 と 2 週間かかったわけですが、以下のリンクからシアトルでの N-400 の Processing Time を見ると、2022/11/19 時点で “80% of cases are completed within 19 Months” と出ます。2 月時点でどのぐらいの Processing Time だったかは覚えていませんが、あまり当てにならないですね。5 人に 1 人は 2 年近くかかるというのも驚きです。</p>
<p><a href="https://egov.uscis.gov/processing-times/">https://egov.uscis.gov/processing-times/</a></p>
<p>なお私は永住権を 2017/4/17 に取得したので、国籍の取得が可能になるのは 2022/4/17 からでした。しかし、申請に関しては市民権取得が可能になる日の 90 日前、つまり私の場合は 1/17 から Early Filing が可能でした。Early Filing に余分な手間や料金がかかるわけではないので、早く市民権が欲しい人は是非活用しましょう。</p>
<p><a href="https://www.uscis.gov/forms/uscis-early-filing-calculator">https://www.uscis.gov/forms/uscis-early-filing-calculator</a></p>
<h1 id="市民権取得の前に">市民権取得の前に</h1>
<p>大前提として、市民権取得のプロセスは USCIS の以下のページに網羅されているので、確認しておきましょう。(このブログを含めて)個人ブログや Youtube を参考にするのももちろんいいですが、古かったり間違っている可能性があるので、注意が必要です。</p>
<p><a href="https://www.uscis.gov/citizenship">https://www.uscis.gov/citizenship</a></p>
<p>特に以下のページにリンクがある N-400 フォームそのものと Instructions、Document Checklist は重要です。</p>
<p><a href="https://www.uscis.gov/n-400">https://www.uscis.gov/n-400</a></p>
<ul>
<li>Form N-400: Use with Adobe Reader</li>
<li>Instructions for Form N-400</li>
<li>Document Checklist, Current Fees, Naturalization Eligibility Worksheet</li>
</ul>
<p>さて、これから市民権取得を考えている人が強いて気を付けるとすれば、</p>
<ul>
<li>渡航履歴は詳細に記録しておく。<a href="https://i94.cbp.dhs.gov/">https://i94.cbp.dhs.gov/</a> が便利。</li>
<li>古いパスポートは捨てない。</li>
<li>確定申告忘れない。申告したら Transcript は毎年必ず保管しておく。</li>
<li>犯罪、特に飲酒運転には気を付ける。</li>
</ul>
<p>あたりでしょうか。まあ普通に生活していれば問題ないですね。市民権を取得する条件として 30 ヶ月ルールとか 3 ヶ月ルールとかがありますが、引っかかる人はそんなにいないでしょう。バックパッカーは要注意・・・?</p>
<h1 id="提出書類について">提出書類について</h1>
<p>N-400 はウィザード形式になっていて、設問にオンラインで答えて提出までできます。細かい順番は忘れましたが、申請費用もオンラインでクレジットカードで払えます。</p>
<p>私が N-400 フォームとともに提出した書類は以下の通りです。郵送する必要はなく、スキャンしてアップロードすれば OK です。ロストする心配がないので安心です。署名が必要なものは、印刷して署名してからスキャンしました。</p>
<ul>
<li>グリーンカードのフォトコピー</li>
<li>戸籍謄本原本</li>
<li>戸籍謄本の英語訳</li>
<li>Form 1040 (U.S. Individual Tax Return) 2018, 2019, 2020, 2021</li>
<li>IRS Tax Return Transcript 2018, 2019, 2020, 2021</li>
</ul>
<p>Instructions に以下のように書いてある通り、米国居住者は写真は不要です。写真は面接前にUSCIS オフィスで撮影してくれます。</p>
<blockquote>
<ol>
<li>Photographs.
Only applicants who reside overseas must provide two identical color photographs of yourself taken recently.</li>
</ol>
</blockquote>
<p>戸籍謄本は “Photocopy of your Current Legal Marital Status Document” に該当する書類です。英語訳については、大昔に MSWord で自力で作成したテンプレートを使い、私本人が翻訳者として署名して提出しました。Notarization は不要なので、自分でやってコストを抑えましょう。</p>
<p>Tax の書類に関しては、N-400 の Instructions に以下のように書かれています。Strongly encourages とまで書かれれば提出しない理由がないですね。じゃあなんでお前は過去 4 年分しか提出してないんだよ、という話ですが、2017 年の書類がなぜか IRS のサイトからダウンロードできなかったんですよね。申告はしたはずなんですが・・・いやまじで。というわけで 5 年分きっちりと出さなくても問題ないパターンもあるようですが、不要なリスクを避けるために毎年必ず Transcript は PDF や紙で保管しておきましょう。ええ私はいつも TurboTax で申告した後は確認すらせずそのまま放置してましたよ。ずぼら・・・</p>
<blockquote>
<p>D. Tax Returns and Overdue Taxes.</p>
<p>Bring photocopies of income tax returns that you filed with the IRS for the past 5 years, or 3 years if filing for naturalization on the basis of marriage to a U.S. citizen. Tax returns are not required for every case. However, USCIS strongly encourages you to bring your tax returns; especially if you are filing based on marriage to a U.S. citizen or have traveled outside the United States for a period that lasted 6 months or more. You can request copies of Federal tax documents at your local IRS office or www.irs.gov.</p>
<p>You may also bring an original IRS tax transcript listing tax information for the past 5 years (3 years if filing on the basis of marriage to a U.S. citizen). To obtain a free IRS tax transcript, visit www.irs.gov. Select “Tools” and then select “Order a Return or Account Transcript.”</p>
</blockquote>
<p>海外渡航歴、すなわち Time Outside the United States は N-400 フォーム内の項目です。これを紙で提出するのだとけっこう面倒だと思いますが、オンラインだと出国日と帰国日を入れると自動的に合計渡航日数まで計算されるので楽です。紙と違って記入欄が足りなくなることもないのが素晴らしい。</p>
<p>ユニークなところだと、Part 12. 設問 19 の “Did you EVER receive any type of military, paramilitary (a group of people who act like a military group but are not part of the official military), or weapons training?” について、ハンドガンの初心者講習を受けたことがあるので、Yes にしてそのことを Additional Information のところに記入しました。面接のときには特に何も聞かれませんでした。No にしてもよかったのでしょうが念のため。<a href="/2016/06/25/l1-visa-update.html">L1 ビザ更新のとき</a>も生物化学の知識のところで Yes と書いて特に問題なかったことがあります。物怖じせずに Yes に該当する設問には Yes と答えましょう。当然ですが嘘をつくのが一番よくない。</p>
<h1 id="面接対策について">面接対策について</h1>
<p>Naturalization の面接といえばシビックテストに目が行きがちですが、それ以外も重要です。面接直前でもいいので、以下のページにある USCIS が作成した Youtube ビデオは必ず見ておきましょう。全体の流れの理解や、質問や回答の具体例を知るのに参考になります。</p>
<p><a href="https://www.uscis.gov/citizenship/learn-about-citizenship/the-naturalization-interview-and-test">https://www.uscis.gov/citizenship/learn-about-citizenship/the-naturalization-interview-and-test</a></p>
<p>パンデミックのせいか、面接はリモートでした。といっても自宅で受けるのではなく、USCIS のオフィスに行き、ブースにある iPad とヘッドセットを使って面接官と会話する感じです。他の人の回答がわりと耳に入るので気が散ります。というかプライバシーはどこへ。</p>
<p>カンニングを防ぐため、パスポートとグリーンカード以外は全て引き出しにしまったことをスタッフの人が確認してから面接が始まります。iPad 越しに最新のパスポートの確認はされましたが、その他提出した書類の原本の確認は行なわれませんでした。とはいっても、面接の案内に持ち物として書かれているものは全部持っていきましょう。提出した書類の原本とかボールペンとか持ってこいと書いてあるはずです。</p>
<p>シビックテストについては、今は 2008 年版でいいのでそんなに難しくないです。サンキューバイデン。100 問程度ちゃちゃっと覚えましょう。もちろんシビックテストの内容は今後また変更される可能性があるので、USCIS のページを必ず確認しましょう。選挙直後で議員の人数や名前が変わったりすると厄介ですね。アメリカの小学生が習う内容なんでしょうが、ほとんどの日本人は事前に勉強しないと受からないんじゃないですかねー。というかワシントン州の州都すら知らなかった・・・シアトルじゃないのか。</p>
<p><a href="https://www.uscis.gov/citizenship/find-study-materials-and-resources">https://www.uscis.gov/citizenship/find-study-materials-and-resources</a></p>
<p>テストは全て口頭で質問されるので、紙に書いて覚えるだけでなく、耳も慣らしておくことをおすすめします。大抵のシビックテスト関連の携帯アプリには、問題を読み上げてくれる機能があるので、それを使って耳を鍛えましょう。</p>
<p>ちなみに私は本番の一問目でやらかしました。聞かれたのが Who does a U.S. Senator represent? だったのですが、Who is one of your state’s U.S. Senators now? だと完全に早とちりし、どや顔で Patty Murray と答えました。アホや・・・。ただ面接官が “Are you sure?” 的なことを言ってくれて言い直す機会をくれたので、一応全問正解で通りました。無慈悲な面接官なら不正解にされるところです。なんだかんだ緊張します。みなさん気を付けましょう。</p>
<p>Writing については対策なしでも問題ないと思いますが、私は以下のサイトで練習しておきました。私のときの課題は “Lincoln was the president during the Civil war.” でした。地名や人名などの固有名詞は要注意です。マサチューセッツとかペンシルバニアとかアルファベットで書けますか?</p>
<p><a href="https://passcitizenshipexam.com/writingTest1/test.html">https://passcitizenshipexam.com/writingTest1/test.html</a><br />
<a href="https://uscitizenshipsupport.com/">https://uscitizenshipsupport.com/</a></p>
<p>面接のメインは、Reading/Writing やシビックテストではなく、N-400 の内容の再確認です。提出した書類の内容は面接前に見直しておいた方がいいです。矛盾があるとたぶん面倒なことになります。私の場合は、N-400 を提出した後に以下の二点が変わっていたので、適当なタイミングでそれを伝えました。</p>
<ul>
<li>転職して雇用者が変わった</li>
<li>渡航歴が増えた</li>
</ul>
<p>面接の終盤は、「逮捕されたことがあるか?」「テロリストグループに所属していたことがあるか?」という No を連発する質問が来るのですが、前触れもなくいきなり「合衆国憲法を遵守するか?」的な Yes を求められる質問に切り替わるので、最後まで気を抜かないようにしましょう。</p>
<h1 id="市民権取得後のプロセスについて">市民権取得後のプロセスについて</h1>
<p>市民権取得後に必要、もしくは可能となるプロセスがあります。私はまだ上の3つしかやっていませんが、気づいたことを書いておきます。</p>
<ul>
<li>有権者登録 (Voter Registration) <a href="https://vote.gov/">https://vote.gov/</a></li>
<li>パスポート申請</li>
<li>GlobalEntry 更新</li>
<li>ソーシャルセキュリティ更新</li>
<li>Real ID 申請</li>
</ul>
<p>まず国籍を取得したらまっさきに欲しくなるのがアメリカのパスポートでしょう。パスポートの申請には Certificate of Naturalization の原本を国務省に送る必要があることに注意が必要です。パスポートを申請する場合は、署名した Certificate のフォトコピーを取っておくことをおすすめします。</p>
<p>私の場合のパスポート申請のタイムラインは以下の通りでした。Expedited service と 1-2 Day Delivery のオプションをつけてこれです。日本のパスポートはもっと早かったはず。申請料も日本よりはるかに高い。</p>
<ul>
<li>2022-9-26 DS-11 提出</li>
<li>2022-10-19 Shipped</li>
<li>2022-10-20 Approved, Passport book 受領</li>
</ul>
<p>なぜか Approved になる前に Shipped になりました。謎。Passport book だけでなく、記念にPassport card も作ったのですが、Passport book 到着の一週間後に Passport card、そのさらに一週間後に Certificate が戻ってきました。したがって 6 週間は Certificate が手元から離れます。他に Certificate が必要な手続きがある場合、手続きの順番をしっかり考えましょう。もしかすると Passport book だけにすれば一週間早く Certificate が戻ってくるかもしれません。急ぐ人は参考までに。</p>
<p>ところで、Passport book の送付に関しては追跡番号が通知されますが、Passport card と Certificate についての追跡番号は教えてくれません。USPS を信じて気長に待ちましょう。</p>
<p>パスポートは、DS-11 という書類を記入して Passport Acceptance Facility と呼ばれる窓口に持っていきます。オンライン申請はできません。DS-11 フォームの記入自体は Form Filler というのがあってオンラインでできるので、印刷して署名した紙を窓口に持っていきます。</p>
<p><a href="https://travel.state.gov/content/travel/en/passports/need-passport/apply-in-person.html">https://travel.state.gov/content/travel/en/passports/need-passport/apply-in-person.html</a></p>
<p>細かいところですが、母親の名前は旧姓を記入する必要があります。あと、Employer の名前はオンラインだと Optional になっていますが、窓口のスタッフによるとそうではないらしいので、書いた方がいいです。</p>
<p>Passport Acceptance Facility は以下のサイトで検索できます。</p>
<p><a href="https://iafdb.travel.state.gov/">https://iafdb.travel.state.gov/</a></p>
<p>施設には種類があり、アポイントが必要なところと不要なところがあります。おすすめはアポイント不要なところです。私はシアトル市がやっている Southwest Customer Service Center というところで申請しました。ガラガラでした。最寄りは USPO なんですがパンデミックのせいか新規申し込みを全然受け付けていないようです。</p>
<p><a href="https://www.seattle.gov/customer-service-centers/southwest-customer-service-center">https://www.seattle.gov/customer-service-centers/southwest-customer-service-center</a></p>
<p>GlobalEntry はグリーンカードに紐付いているため、市民権を取得したら更新が必要です。私は TSA Precheck も同じようにグリーンカードに紐付いていると勘違いしていましたが、Precheck は独自の ID で管理されているので市民権取得による影響は受けません。よって、国内旅行に関して言えば GlobalEntry の更新は不要です。</p>
<p>面倒なことに、GlobalEntry の更新も面接が必要です。空き状況は以下のページで確認できます。</p>
<p><a href="https://ttp.cbp.dhs.gov/schedulerui/schedule-interview/location?lang=en&vo=true&service=UP">https://ttp.cbp.dhs.gov/schedulerui/schedule-interview/location?lang=en&vo=true&service=UP</a></p>
<p>パンデミックが終わったせいか、どこも劇混みなので早めに予約しましょう。私が 10 月に予約しようとした時点で、Sea-tac 空港の次のアポイントメントは来年の 3 月になっていました。そのときは運良く Blaine に 11 月上旬の空きが一つだけあったので、そこに予約を入れました。車で片道一時間半かけて 10 分程度の面接を受けに行く効率の悪さよ。キャンセルが出ていきなり空きが出ることもあるので、いいスロットが見つからない場合は何回か空きをチェックしたほうがいいです。</p>はじめにKernel-debugging Windows 10 ARM642021-06-27T00:00:00+00:002021-06-27T00:00:00+00:00https://msmania.github.io/2021/06/27/windows-arm64<h1 id="はじめに">はじめに</h1>
<p>Qiita に書くにはしょぼいネタなので久々にこちらに書きます。</p>
<p>先日、「そうだ ARM64 やろう」と思いついて Raspberry Pi を買いました。以前 QEMU で ARM64 の Linux カーネルを触ったことはあるのですが、実機で試しておきたかったためです。その当時 QEMU 上で ARM64 版 Windows を動かそうとも試みましたが、どうやらドライバーが対応していないらしく起動しませんでした。今回無事カーネル デバッグ環境を作るところまでできたので、そこまでの過程をまとめました。</p>
<h1 id="デバイス選定">デバイス選定</h1>
<p>まずはハードウェアの選定です。Raspberry Pi を買うことが動機ではなかったので、手始めに既製品のパソコンも探したのですが、どれも高すぎました。$1,000 超えは論外で、学習用途なことを考えると $350 も厳しい。</p>
<ul>
<li><a href="https://www.microsoft.com/en-us/p/surface-pro-x/8qg3bmrhnwhk">Microsoft Surface Pro X</a> - $999.99</li>
<li><a href="https://www.lenovo.com/us/en/laptops/ideapad/ideapad-flex-series/Lenovo-Flex-5G-14Q8CX05/p/82AK0002US">Lenovo IdeaPad Flex 5G (14”) - Iron Grey</a> - $1,349.99</li>
<li><a href="https://www.hp.com/us-en/shop/mdp/laptops/elitebook-folio-356504--1#!">HP Elite Folio 13.5 inch 2-in-1 Notebook PC</a> - $1,889.00</li>
<li><a href="https://www.samsung.com/us/computing/galaxy-book-go/">Samsumg Galaxy Book Go</a> - $349</li>
</ul>
<p>困っていたところ、ラズベリーパイでいけるらしいという記事を発見。いいじゃん。</p>
<p>ラズパイ4にWindows 10 on ARM64をインストールする - Qiita<br />
<a href="https://qiita.com/mkht/items/9d173334dc5b26bfef46">https://qiita.com/mkht/items/9d173334dc5b26bfef46</a></p>
<p>というわけでアマゾンで一番人気のスターター キットを買いました。メモリは当然 Max の 8GB を選びます。お値段 $119.99。これはプチプラ。</p>
<p>Amazon.com: CanaKit Raspberry Pi 4 8GB Starter Kit - 8GB RAM: Computers & Accessories
<a href="https://www.amazon.com/dp/B08956GVXN">https://www.amazon.com/dp/B08956GVXN</a></p>
<h1 id="os-インストールメモリ設定">OS インストール/メモリ設定</h1>
<p>CanaKit のパッケージ内容がこれ。組み立ては簡単でした。ドライバーすら不要。</p>
<p><img src="/assets/2021-06-26-canakit.jpg" alt="" /></p>
<p>とりあえず Windows のインストールを始める前に、キットに入っていた SD カードをそのまま使ったところ、NOOBS とかいうブート画面が難なく起動。</p>
<p><img src="/assets/2021-06-26-noobs.jpg" style="width:50%" /></p>
<p>動作確認ができたので、次に Windows をインストールします。大容量の SD カードが手元に他にないので、CanaKit 付属の SD カードの中身を無慈悲に消去して Windows のブート用にします。手順は以下のサイトを参考にしました。大体どこの情報も同じで、WoR (= Windows on Raspberry) というプロジェクトが用意してくれているソフトを使うだけです。</p>
<p>Guide – Windows 10 ARM64 on Pi 4B – Making Pi ServerReady<br />
<a href="https://rpi4-uefi.dev/win10-arm64-on-pi-4b/">https://rpi4-uefi.dev/win10-arm64-on-pi-4b/</a></p>
<p>特に罠はなく、気になるのは OS のインストール イメージを作るステップで、UUP のサイトからダウンロードした怪しげなバッチファイルを実行するのが若干怖いぐらいです。慎重な人はテスト専用の環境で実行してください。</p>
<p>執筆当時の最新バージョンは Windows 10 Insider Preview 10.21390.2025 (co_release) [arm64] となっており 21390.2025.210527-1818.CO_RELEASE_SVC_IM_CLIENTPRO_OEMRET_A64FRE_EN-US.ISO という ISO ファイルができました。話が逸れますが、co_release というブランチ名はコバルトというコードネームですね。</p>
<p>Microsoftが2021年秋を見込むWindowsの一大プロジェクト「Sun Valley」と「Cobalt」:Windowsフロントライン(1/2 ページ) - ITmedia PC USER<br />
<a href="https://www.itmedia.co.jp/pcuser/articles/2011/02/news074.html">https://www.itmedia.co.jp/pcuser/articles/2011/02/news074.html</a></p>
<p>周期表に従ったコードネームはバナジウムから始まっています。Threshold 2 や Redstone 5 とかいう連番になった過去のコードネームよりは遥かにましですが、二番煎じ感が否めません。</p>
<p>Get Ready for Windows 10 “Vanadium” and “Vibranium”<br />
<a href="https://www.howtogeek.com/fyi/get-ready-for-windows-10-vanadium-and-vibranium/">https://www.howtogeek.com/fyi/get-ready-for-windows-10-vanadium-and-vibranium/</a></p>
<p>本題に戻って WoR の画面を幾つか貼っておきます。デフォルトの設定のままですんなり起動しました。</p>
<p><img src="/assets/2021-06-26-wor01.png" style="width:75%" /></p>
<p><img src="/assets/2021-06-26-wor02.png" style="width:75%" /></p>
<p><img src="/assets/2021-06-26-wor03.png" style="width:75%" /></p>
<p>作成した SD カードを入れて Raspberry Pi を初回起動するわけですが、とにかく動作が著しく遅いので忍耐力が必要です。OOBE が出てインストールが終わるまでに 30 分ぐらいかかります。</p>
<p><img src="/assets/2021-06-26-windows-boot.jpg" style="width:45%" />
<img src="/assets/2021-06-26-windows-oobe.jpg" style="width:45%" /></p>
<p>起動した後タスクマネージャーを見ると、常に Disk I/O が 100% で張り付いており、SD カードの I/O が原因だと分かります。そこで I/O を減らすため、以下の設定を行ないます。</p>
<ul>
<li>UEFI の画面で “Limit RAM to 3 GB” を Disable に設定<br /><img src="/assets/2021-06-26-uefi-3gb.jpg" style="width:70%" /></li>
<li>ページファイルサイズを 0 に設定<br /><img src="/assets/2021-06-26-zeroswap.png" style="width:40%" /></li>
</ul>
<p>これで物理メモリを 8GB フルに使うようになったはずで、実際にパフォーマンスは大きく改善しました。ページングの影響がずいぶんと大きかったみたいです。メモリ使用量は 3GB 程度なので、4GB でもページファイルなしでギリギリいけそうです。</p>
<p>とはいっても相変わらず Disk I/O は 100% のままで実用にはほど遠い遅さです。やはり Galaxy Book Go ぐらいはケチらずに買うべきだったか。</p>
<p><img src="/assets/2021-06-26-taskmgr-cpu.png" style="width:45%" />
<img src="/assets/2021-06-26-taskmgr-disk.png" style="width:45%" /></p>
<p>Winver と msinfo32 はこのようになりました。基盤はソニーが作っているらしい。</p>
<p><img src="/assets/2021-06-26-winver.png" style="width:35%" />
<img src="/assets/2021-06-26-msinfo32.png" style="width:60%" /></p>
<p>もちろん x64 の Windows から RDP 接続できますし、ファイル共有も問題なく動作し、アーキテクチャの違いは感じません。</p>
<h1 id="ツール類">ツール類</h1>
<p>何かソフトを入れましょう。まずは Sysinternals Suite の ARM64 版ですかね。</p>
<p>Sysinternals Suite - Windows Sysinternals | Microsoft Docs<br />
<a href="https://docs.microsoft.com/en-us/sysinternals/downloads/sysinternals-suite">https://docs.microsoft.com/en-us/sysinternals/downloads/sysinternals-suite</a></p>
<p>デバッガーも必須です。デバッガー単体はダウンロードできないので、WDK または SDK の ISO 経由でインストールします。記事を書き溜めている間に Windows 11 が発表されたので、Windows 11 の SDK に入っているデバッガーを入れてみましょう。Windows 11 のメジャー バージョンは 22000 みたいです。</p>
<p>Download the Windows Driver Kit (WDK) - Windows drivers | Microsoft Docs<br />
<a href="https://docs.microsoft.com/en-us/windows-hardware/drivers/download-the-wdk">https://docs.microsoft.com/en-us/windows-hardware/drivers/download-the-wdk</a></p>
<p>dbgsrv.exe を起動してリモート デバッグを試します。ARM64 のアセンブリ全然ワカランチン。関数の先頭の <code class="language-plaintext highlighter-rouge">pacibsp</code> って何やねん、と思って調べたら話題の <a href="https://developer.arm.com/documentation/dui0801/g/A64-General-Instructions/PACIB--PACIZB--PACIB1716--PACIBSP--PACIBZ">PAC</a> だった・・・。これは後で勉強しないといけない。</p>
<p><img src="/assets/2021-06-26-ntsd.png" alt="" /></p>
<p>ARM64 の Windows 10 では、x64/x86/arm のプロセスも動きます。残念なことにタスク マネージャーがプロセスのアーキテクチャを表示してくれるのに対して、Process Explorer は bitness しか教えてくれません。タスクマネージャーの方が使えるじゃん。</p>
<p><img src="/assets/2021-06-26-taskmgr-arch.png" style="width:75%" /></p>
<p><img src="/assets/2021-06-26-procexp.png" style="width:75%" /></p>
<p>というか x64 のエミュレーションはできないと思っていたのですが、最近できるようになったんですね。これは素晴らしい仕事。</p>
<p>Introducing x64 emulation in preview for Windows 10 on ARM PCs to the Windows Insider Program | Windows Insider Blog<br />
<a href="https://blogs.windows.com/windows-insider/2020/12/10/introducing-x64-emulation-in-preview-for-windows-10-on-arm-pcs-to-the-windows-insider-program/">https://blogs.windows.com/windows-insider/2020/12/10/introducing-x64-emulation-in-preview-for-windows-10-on-arm-pcs-to-the-windows-insider-program/</a></p>
<p>最後に Firefox を入れてみました。せっかくネイティブ arm64版もあるので。</p>
<p><img src="/assets/2021-06-26-nightly.png" alt="" /></p>
<p>なお x86 版 Firefox を動かそうとすると、CHPE (=Compiled Hybrid Portable Executable) の動作に対応していないのでちょっと問題が起きます。試していませんが、x64 on ARM64 も同様の問題があるかもしれません。後で直す。</p>
<p>1708587 - Nightly x86 on ARM64 error 0x800000003 on first launch with launcher process<br />
<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1708587">https://bugzilla.mozilla.org/show_bug.cgi?id=1708587</a></p>
<h1 id="カーネルデバッグ">カーネルデバッグ</h1>
<p>ユーザーモードのデバッグはできることが分かりましたが、やはりカーネルこそキング、ということでカーネルデバッグもやりましょう。が、しかし、繋ぎ方が問題になります。</p>
<p>Windows 10 であれば、Ethernet 経由で繋げたいところですが、Raspberry Pi のネットワーク アダプターは Broadcom の BCM6E4E で、どうやらデバッグ機能は持っていないようです。</p>
<p><img src="/assets/2021-06-26-ethernet.png" style="width:40%" /></p>
<p>Supported Ethernet NICs for Network Kernel Debugging in Windows 10 - Windows drivers | Microsoft Docs<br />
<a href="https://docs.microsoft.com/en-us/windows-hardware/drivers/debugger/supported-ethernet-nics-for-network-kernel-debugging-in-windows-10">https://docs.microsoft.com/en-us/windows-hardware/drivers/debugger/supported-ethernet-nics-for-network-kernel-debugging-in-windows-10</a></p>
<p>USB の方は 2.0 も 3.0 もデバッグ機能なし。</p>
<p><img src="/assets/2021-06-26-usb3.png" alt="" /></p>
<p>最後の希望で、シリアル ポートを物理的に付けられないかを調べると、GPIO に TXD/RXD ピンがあってこれを D-Sub 9 ピンに変換することができるようです。これは熱い展開。</p>
<p>SHA - - - Raspberry Pi - Installing a RS232 Serial Port<br />
<a href="http://www.savagehomeautomation.com/projects/raspberry-pi-installing-a-rs232-serial-port.html">http://www.savagehomeautomation.com/projects/raspberry-pi-installing-a-rs232-serial-port.html</a></p>
<p>デバイスマネージャーを見ると、確かに COM1 が存在してドライバーも動作しているようです。</p>
<p><img src="/assets/2021-06-26-devmgmt.png" style="width:60%" />
<img src="/assets/2021-06-26-driver.png" style="width:35%" /></p>
<p>アマゾンで “TTL DB9” で検索し、はんだ付けが不要で、かつユーザー評価が良かったこれをチョイス。たったの $7.49。ジャンパーケーブルも一緒に買ったのですが、この商品にもジャンパーケーブルが必要分の 4 本入ってました。親切!</p>
<p>Amazon.com: DZS Elec RS232 DB9 Male Serial Port to TTL Converter MAX3232 Root Module Connector MCU Programme Mobile Root Vehicle Examine and Repair Converter: Computers & Accessories<br />
<a href="https://www.amazon.com/dp/B072KJSS5C">https://www.amazon.com/dp/B072KJSS5C</a></p>
<p>まずは Raspberry Pi 上でカーネル デバッグの設定をします。Raspberry Pi 側のボーレートが不明なので、とりあえずお決まりの 115200 にしておきます。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>> bcdedit /debug on
The operation completed successfully.
> bcdedit /dbgsettings SERIAL DEBUGPORT:1 BAUDRATE:115200
The operation completed successfully.
> bcdedit /dbgsettings
debugtype Serial
debugport 1
baudrate 115200
The operation completed successfully
</code></pre></div></div>
<p>システムを終了し電源を抜いてから、シリアルポートを接続します。作業は簡単で、4 つのピン (TX, RX, GND, VCC) をそれぞれ GPIO の TXL, RXL, GRD, 3V3 に繋ぐだけです。</p>
<p>ところで、デバッガー側のマシンにシリアルポートあったっけ?という話ですが、<a href="https://msmania.github.io/2019/11/23/desktop-pc.html">前回マザーボードを交換したとき</a>に触れたように、シリアルポート カードというロマン溢れるパーツを差しているので準備万端です。まあ正直実際にこれを使う時が来るとは思わなかった。なんという伏線。</p>
<p>デバッガー側のポートにはボーレートを設定できるみたいなので、念のため 115200 にしておきます。</p>
<p><img src="/assets/2021-06-26-baudrate.png" style="width:40%" /></p>
<p>ポート同士をご家庭によくある RS-232C ケーブルで繋ぎます。こんな感じになりました。</p>
<p><img src="/assets/2021-06-26-connect-db9.jpg" alt="" /></p>
<p>以下のコマンドでデバッガーを起動しておいてから Raspberry Pi を起動します。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kd.exe -k com:port=\\.\com1,baud=115200 -b
</code></pre></div></div>
<p>出力来た!</p>
<p><img src="/assets/2021-06-26-kd-boot.png" alt="" /></p>
<p>ブート中、SerPL011 というモジュールが必ずブレークします。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kd> knL
# Child-SP RetAddr Call Site
00 fffff803`da098b30 fffff803`dc959194 nt!DebugService2+0x8
01 fffff803`da098b30 fffff803`dd008d10 nt!DbgLoadImageSymbols+0x44
02 fffff803`da098b70 fffff803`dcfeeb00 nt!KdInitSystem+0xbf0
03 fffff803`da098d10 00000000`00000000 nt!KiSystemStartup+0x170
kd> g
The target has requested that the debugger execute a command: !amli err 0xc 0000000000000001 0000000000000003 0000000000000000 0000000000000000;g">!amli err 0xc 0000000000000001 0000000000000003 0000000000000000 0000000000000000;g
The target has requested that the debugger execute a command: !amli err 0xc 0000000000000001 0000000000000003 0000000000000000 0000000000000000;g">!amli err 0xc 0000000000000001 0000000000000003 0000000000000000 0000000000000000;g
KDTARGET: Refreshing KD connection
KDTARGET: Refreshing KD connection
Break instruction exception - code 80000003 (first chance)
SerPL011!PL011BreakPoint+0x18:
fffff807`8b2d9d20 d43e0000 brk #0xF000
3: kd> knL
# Child-SP RetAddr Call Site
00 fffffa0f`6c6b3770 fffff807`8b2e04bc SerPL011!PL011BreakPoint+0x18
01 fffffa0f`6c6b3780 fffff807`8b2dfbc4 SerPL011!PL011pDeviceParseResources+0x1b4
02 fffffa0f`6c6b3800 fffff803`dfb5df00 SerPL011!PL011EvtDevicePrepareHardware+0x34
03 fffffa0f`6c6b3860 fffff803`dfb6bc74 Wdf01000!FxPnpDevicePrepareHardware::InvokeClient+0x30
04 fffffa0f`6c6b3880 fffff803`dfb5c854 Wdf01000!FxPrePostCallback::InvokeStateful+0x144
05 (Inline Function) --------`-------- Wdf01000!FxPnpDevicePrepareHardware::Invoke+0x54
06 fffffa0f`6c6b38f0 fffff803`dfb5b1cc Wdf01000!FxPkgPnp::PnpPrepareHardware+0x164
07 fffffa0f`6c6b3940 fffff803`dfb5afc4 Wdf01000!FxPkgPnp::PnpEventHardwareAvailable+0xdc
08 fffffa0f`6c6b3990 fffff803`dfb5ad8c Wdf01000!FxPkgPnp::PnpEnterNewState+0x184
09 fffffa0f`6c6b3a10 fffff803`dfb5aad0 Wdf01000!FxPkgPnp::PnpProcessEventInner+0x27c
0a fffffa0f`6c6b3a90 fffff803`dfb74428 Wdf01000!FxPkgPnp::_PnpProcessEventInner+0x30
0b fffffa0f`6c6b3ab0 fffff803`dfb745b8 Wdf01000!FxEventQueue::EventQueueWorker+0xb0
0c fffffa0f`6c6b3af0 fffff803`dc849cdc Wdf01000!FxWorkItemEventQueue::_WorkItemCallback+0x28
0d fffffa0f`6c6b3b10 fffff803`dc849394 nt!IopProcessWorkItem+0x8c
0e fffffa0f`6c6b3b70 fffff803`dc97b000 nt!ExpWorkerThread+0x1d4
0f fffffa0f`6c6b3d30 fffff803`dc807de4 nt!PspSystemThreadStartup+0x50
10 fffffa0f`6c6b3d90 00000000`00000000 nt!KiStartSystemThread+0x24
</code></pre></div></div>
<p>PL011 というのは、<a href="https://www.raspberrypi.org/documentation/configuration/uart.md">ここ</a>を見るとシリアル通信の規格で、カーネルデバッグに使っている GPIO の方ではなく、Bluetooth に関係があるようなことが書いてある気がします。当該ドライバーのソースコードは <a href="https://github.com/ms-iot/rpi-iotcore/blob/31e89330c37564d96e246a64210cfeaf8c45007c/drivers/uart/bcm2836/serPL011/PL011device.cpp#L701">GitHub 上</a>にありました。残念ながら ARM64 アセンブリがチンプンカンプンなのでどの assert がヒットしたのかがすぐに分からないわけですが。もしかしたら contribute チャンスなのかもしれないので、これも TODO リスト入りです。とりあえずは続行したら普通に起動したからヨシ!</p>
<p>その後適当にコマンドを打つなど。やはりシリアル経由での .reload は遅い。1394 が復活して欲しい。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>0: kd> g
Breakpoint 0 hit
win32kfull!xxxCreateWindowEx:
fffff70c`55f1f3c0 d503237f pacibsp
2: kd> !process -1 0
PROCESS ffffd38b8e928080
SessionId: 1 Cid: 1394 Peb: f842a99000 ParentCid: 0dc8
DirBase: 14563e000 ObjectTable: ffffa90cd35535c0 HandleCount: 45.
Image: mn_arm64.exe
2: kd> .reload
Connected to Windows 10 21390 ARM 64-bit (AArch64) target at (Sat Jun 26 09:58:58.727 2021 (UTC - 7:00)), ptr64 TRUE
Loading Kernel Symbols
..................
Press ctrl-c (cdb, kd, ntsd) or ctrl-break (windbg) to abort symbol loads that take too long.
Run !sym noisy before .reload to track down problems loading symbols.
.............................................
................................................................
.................................
Loading User Symbols
...............
Press ctrl-c (cdb, kd, ntsd) or ctrl-break (windbg) to abort symbol loads that take too long.
Run !sym noisy before .reload to track down problems loading symbols.
.
Loading unloaded module list
.........
2: kd> knL
# Child-SP RetAddr Call Site
00 fffffa0f`6eee0800 fffff70c`55f0e998 win32kfull!xxxCreateWindowEx
01 fffffa0f`6eee0800 fffff70c`55cdd074 win32kfull!NtUserCreateWindowEx+0x578
02 fffffa0f`6eee0990 fffff803`dc808360 win32k!NtUserCreateWindowEx+0x64
03 fffffa0f`6eee09f0 fffff803`dc807fd8 nt!KiSystemServiceCopyEnd+0x38
04 fffffa0f`6eee0a50 00007ff8`403476f4 nt!KiSystemServiceExit
05 000000f8`42cff290 00007ff8`40bcc96c win32u!NtUserCreateWindowEx+0x4
06 000000f8`42cff290 00000000`00000000 USER32!CreateWindowExW+0x80c
2: kd> r
x0=0000000000000000 x1=fffffa0f6eee0860 x2=fffffa0f6eee08c0 x3=fffffa0f6eee08d0
x4=0000000000cf0000 x5=0000000080000000 x6=0000000000000000 x7=00000000000001e6
x8=000000000000012c x9=fffffa0f6eee0878 x10=fffff803dc61e930 x11=00007ff7aa970f20
x12=00007ff7aa970f42 x13=000000000000007f x14=0000000000000020 x15=0000000000000080
x16=0000000000000000 x17=fffffa0f6eee0878 x18=ffffd38b8e928700 x19=00007ff7aa971560
x20=0000000000000000 x21=fffff74881723390 x22=0000000000cf0000 x23=0000000000000000
x24=fffff748806cb010 x25=0000000000000000 x26=0000000000000000 x27=0000000000000000
x28=0000000000000000 fp=fffffa0f6eee0850 lr=fffff70c55f0e998 sp=fffffa0f6eee0800
pc=fffff70c55f1f3c0 psr=80000144 N--- EL1
win32kfull!xxxCreateWindowEx:
fffff70c`55f1f3c0 d503237f pacibsp
2: kd> u .
win32kfull!xxxCreateWindowEx:
fffff70c`55f1f3c0 d503237f pacibsp
fffff70c`55f1f3c4 a9ba7bfd stp fp,lr,[sp,#-0x60]!
fffff70c`55f1f3c8 a90153f3 stp x19,x20,[sp,#0x10]
fffff70c`55f1f3cc a9025bf5 stp x21,x22,[sp,#0x20]
fffff70c`55f1f3d0 a90363f7 stp x23,x24,[sp,#0x30]
fffff70c`55f1f3d4 a9046bf9 stp x25,x26,[sp,#0x40]
fffff70c`55f1f3d8 f9002bfb str x27,[sp,#0x50]
fffff70c`55f1f3dc 910003fd mov fp,sp
2: kd> !cpuinfo
CP Model Revision Manufacturer Features Speed
0 D08 r00p03 A 0000000000000000 1500 Mhz
1 D08 r00p03 A 0000000000000000 1500 Mhz
2 D08 r00p03 A 0000000000000000 1500 Mhz
3 D08 r00p03 A 0000000000000000 1500 Mhz
</code></pre></div></div>
<p>以上、どこまで実用に値するのかは分かりませんが、カーネルデバッグ環境を作ることができました。せっかくなので SD カードをもう一枚買って Linux も入れてみましょうかね。</p>はじめにDebugging Windows SEH2019-11-24T00:00:00+00:002019-11-24T00:00:00+00:00https://msmania.github.io/2019/11/24/seh-debug<h1 id="はじめに">はじめに</h1>
<p>Clang でコンパイルしたコードをデバッグすると、MSVC よりも遥かにトリッキーな最適化を目にすることが多いです。たまに想定外の動作に遭遇して、「これは絶対コンパイラーが間違っている」 と豪語しても、まじめに調べると間違っているのは大体自分の方です。しかし今回こそはコンパイラーが間違っていると思われる動作に遭遇したので紹介します。</p>
<h1 id="未知との遭遇">未知との遭遇</h1>
<p>問題となるコードを以下に示します。</p>
<div class="language-c++ highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#include <algorithm>
#include <stdio.h>
#include <windows.h>
#include <winnt.h>
#include <winternl.h>
</span>
<span class="k">template</span> <span class="o"><</span><span class="k">typename</span> <span class="nc">T</span><span class="p">></span>
<span class="k">struct</span> <span class="nc">SimpleHolder</span> <span class="p">{</span>
<span class="n">T</span> <span class="n">val_</span> <span class="o">=</span> <span class="p">{};</span>
<span class="kt">void</span> <span class="n">set</span><span class="p">(</span><span class="k">const</span> <span class="n">T</span> <span class="n">val</span><span class="p">)</span> <span class="p">{</span> <span class="n">val_</span> <span class="o">=</span> <span class="n">val</span><span class="p">;</span> <span class="p">}</span>
<span class="k">operator</span> <span class="k">const</span> <span class="n">T</span><span class="o">&</span><span class="p">()</span> <span class="k">const</span> <span class="p">{</span> <span class="k">return</span> <span class="n">val_</span><span class="p">;</span> <span class="p">}</span>
<span class="p">};</span>
<span class="n">PVOID</span> <span class="nf">SwapThreadLocalStoragePointer</span><span class="p">(</span><span class="n">PVOID</span> <span class="n">newValue</span><span class="p">)</span> <span class="p">{</span>
<span class="n">std</span><span class="o">::</span><span class="n">swap</span><span class="p">(</span><span class="o">::</span><span class="n">NtCurrentTeb</span><span class="p">()</span><span class="o">-></span><span class="n">Reserved1</span><span class="p">[</span><span class="mi">11</span><span class="p">],</span> <span class="n">newValue</span><span class="p">);</span>
<span class="k">return</span> <span class="n">newValue</span><span class="p">;</span>
<span class="p">}</span>
<span class="k">const</span> <span class="kt">uint32_t</span> <span class="n">kTlsDataValue</span> <span class="o">=</span> <span class="mi">42</span><span class="p">;</span>
<span class="k">static</span> <span class="k">thread_local</span> <span class="n">SimpleHolder</span><span class="o"><</span><span class="kt">uint32_t</span><span class="o">></span> <span class="n">sTlsData</span><span class="p">;</span>
<span class="kr">__declspec</span><span class="p">(</span><span class="n">noinline</span><span class="p">)</span> <span class="kt">bool</span> <span class="nf">TestThreadLocalStorageHead</span><span class="p">()</span> <span class="p">{</span>
<span class="k">auto</span> <span class="n">origTlsHead</span> <span class="o">=</span> <span class="n">SwapThreadLocalStoragePointer</span><span class="p">(</span><span class="nb">nullptr</span><span class="p">);</span>
<span class="kt">bool</span> <span class="n">isExceptionThrown</span> <span class="o">=</span> <span class="nb">false</span><span class="p">;</span>
<span class="kr">__try</span> <span class="p">{</span>
<span class="n">sTlsData</span><span class="p">.</span><span class="n">set</span><span class="p">(</span><span class="o">~</span><span class="n">kTlsDataValue</span><span class="p">);</span>
<span class="p">}</span>
<span class="kr">__except</span> <span class="p">(</span><span class="n">GetExceptionCode</span><span class="p">()</span> <span class="o">==</span> <span class="n">EXCEPTION_ACCESS_VIOLATION</span>
<span class="o">?</span> <span class="n">EXCEPTION_EXECUTE_HANDLER</span>
<span class="o">:</span> <span class="n">EXCEPTION_CONTINUE_SEARCH</span><span class="p">)</span> <span class="p">{</span>
<span class="n">isExceptionThrown</span> <span class="o">=</span> <span class="nb">true</span><span class="p">;</span>
<span class="p">}</span>
<span class="n">SwapThreadLocalStoragePointer</span><span class="p">(</span><span class="n">origTlsHead</span><span class="p">);</span>
<span class="n">sTlsData</span><span class="p">.</span><span class="n">set</span><span class="p">(</span><span class="n">kTlsDataValue</span><span class="p">);</span>
<span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">isExceptionThrown</span><span class="p">)</span> <span class="p">{</span>
<span class="n">printf</span><span class="p">(</span><span class="s">"[%s] No exception from setter!</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">__FUNCTION__</span><span class="p">);</span>
<span class="k">return</span> <span class="nb">false</span><span class="p">;</span>
<span class="p">}</span>
<span class="k">if</span> <span class="p">(</span><span class="n">sTlsData</span> <span class="o">!=</span> <span class="n">kTlsDataValue</span><span class="p">)</span> <span class="p">{</span>
<span class="n">printf</span><span class="p">(</span><span class="s">"[%s] TLS is broken!</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">__FUNCTION__</span><span class="p">);</span>
<span class="k">return</span> <span class="nb">false</span><span class="p">;</span>
<span class="p">}</span>
<span class="n">printf</span><span class="p">(</span><span class="s">"[%s] Passed!</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">__FUNCTION__</span><span class="p">);</span>
<span class="n">fflush</span><span class="p">(</span><span class="n">stdout</span><span class="p">);</span>
<span class="k">return</span> <span class="nb">true</span><span class="p">;</span>
<span class="p">}</span>
<span class="kt">int</span> <span class="nf">main</span><span class="p">(</span><span class="kt">int</span> <span class="n">argc</span><span class="p">,</span> <span class="kt">char</span><span class="o">*</span> <span class="n">argv</span><span class="p">[])</span> <span class="p">{</span>
<span class="n">TestThreadLocalStorageHead</span><span class="p">();</span>
<span class="k">return</span> <span class="mi">0</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>
<p>何をやっているかといえば、TEB に保存されている TLS Storage へのポインターを null にセットしてから実際に thread local な変数にアクセスし、AV が発生することを確かめています。このコードを Clang 9.0.0 x86_64-pc-windows-msvc の最適化オプション O2 でビルドして Windows 10 x64 上で実行すると、なんと Second chance exception でクラッシュします。</p>
<h2 id="問題-1-compile-time-memory-ordering">問題 1: Compile-time Memory Ordering</h2>
<p>実行時のデバッグ ログです。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>0:000> g
(1db4.b68): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
*** WARNING: Unable to verify checksum for t.exe
t!TestThreadLocalStorageHead+0x60:
00007ff7`94981080 488b04c1 mov rax,qword ptr [rcx+rax*8] ds:00000000`00000000=????????????????
0:000> g
(1db4.b68): Access violation - code c0000005 (!!! second chance !!!)
t!TestThreadLocalStorageHead+0x60:
00007ff7`94981080 488b04c1 mov rax,qword ptr [rcx+rax*8] ds:00000000`00000000=????????????????
0:000> uf t!TestThreadLocalStorageHead
t!TestThreadLocalStorageHead:
00007ff7`94981020 55 push rbp
00007ff7`94981021 56 push rsi
00007ff7`94981022 4883ec28 sub rsp,28h
00007ff7`94981026 488d6c2420 lea rbp,[rsp+20h]
00007ff7`9498102b 65488b042530000000 mov rax,qword ptr gs:[30h]
00007ff7`94981034 65488b0c2558000000 mov rcx,qword ptr gs:[58h]
00007ff7`9498103d 488b7058 mov rsi,qword ptr [rax+58h]
00007ff7`94981041 48c7405800000000 mov qword ptr [rax+58h],0
00007ff7`94981049 8b05f12f0000 mov eax,dword ptr [t!_tls_index (00007ff7`94984040)]
00007ff7`9498104f 488b04c1 mov rax,qword ptr [rcx+rax*8]
00007ff7`94981053 488d8804000000 lea rcx,[rax+4]
00007ff7`9498105a bad5ffffff mov edx,0FFFFFFD5h
00007ff7`9498105f e8bc000000 call t!SimpleHolder<unsigned int>::set (00007ff7`94981120) <<<< 1st sTlsData.set()
00007ff7`94981064 65488b042530000000 mov rax,qword ptr gs:[30h]
00007ff7`9498106d 65488b0c2558000000 mov rcx,qword ptr gs:[58h]
00007ff7`94981076 48897058 mov qword ptr [rax+58h],rsi
00007ff7`9498107a 8b05c02f0000 mov eax,dword ptr [t!_tls_index (00007ff7`94984040)]
00007ff7`94981080 488b04c1 mov rax,qword ptr [rcx+rax*8] <<<< crash!!
00007ff7`94981084 c780040000002a000000 mov dword ptr [rax+4],2Ah <<<< <<<< 2nd sTlsData.set()
00007ff7`9498108e 488d0d871f0000 lea rcx,[t!`string' (00007ff7`9498301c)]
00007ff7`94981095 488d15641f0000 lea rdx,[t!`string' (00007ff7`94983000)]
00007ff7`9498109c e88f000000 call t!printf (00007ff7`94981130)
00007ff7`949810a1 31c0 xor eax,eax
00007ff7`949810a3 4883c428 add rsp,28h
00007ff7`949810a7 5e pop rsi
00007ff7`949810a8 5d pop rbp
00007ff7`949810a9 c3 ret
</code></pre></div></div>
<p>クラッシュしているのは <code class="language-plaintext highlighter-rouge">__try</code> で囲んだ最初の <code class="language-plaintext highlighter-rouge">sTlsData.set</code> ではなく、2 度目の <code class="language-plaintext highlighter-rouge">sTlsData.set</code> であることが分かります。したがって、例外が捕捉されなかったことが問題なのではなく、最初の <code class="language-plaintext highlighter-rouge">sTlsData.set</code> で例外が発生しなかったことがおかしいのです。</p>
<p>クラッシュが起きなかった理由はアセンブリを見ると明らかで、コンパイラーが処理の順番を入れ替えたからです。具体的にはこの部分。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>00007ff7`9498102b 65488b042530000000 mov rax,qword ptr gs:[30h] <<<< TEB
00007ff7`94981034 65488b0c2558000000 mov rcx,qword ptr gs:[58h] <<<< caching TLS head #1
00007ff7`9498103d 488b7058 mov rsi,qword ptr [rax+58h] <<<< caching TLS head #2
00007ff7`94981041 48c7405800000000 mov qword ptr [rax+58h],0 <<<< resetting TLS head
00007ff7`94981049 8b05f12f0000 mov eax,dword ptr [t!_tls_index (00007ff7`94984040)]
00007ff7`9498104f 488b04c1 mov rax,qword ptr [rcx+rax*8]
00007ff7`94981053 488d8804000000 lea rcx,[rax+4] <<<< rcx = sTlsData
</code></pre></div></div>
<p>本来であれば +104f の <code class="language-plaintext highlighter-rouge">mov</code> でクラッシュして欲しいのですが、TLS Head をリセットする +1041 の命令よりも先に、リセット前の TLS head を +1034 の命令でキャッシュしてしまっているため、クラッシュしません。</p>
<p>ソースコードの順番を忠実に守るのであれば、この部分は以下のようにコンパイルされるべきです。TLS Head をキャッシュする処理は <code class="language-plaintext highlighter-rouge">SwapThreadLocalStoragePointer</code> ではなく <code class="language-plaintext highlighter-rouge">sTlsData.set</code> の一部であるため、TLS Head をリセットした後に実行しなければなりません。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>mov rax,qword ptr gs:[30h] <<<< TEB
mov rsi,qword ptr [rax+58h] <<<< caching TLS head #2
mov qword ptr [rax+58h],0 <<<< resetting TLS head
mov rcx,qword ptr gs:[58h] <<<< caching TLS head #1
mov eax,dword ptr [t!_tls_index (00007ff7`94984040)]
mov rax,qword ptr [rcx+rax*8]
lea rcx,[rax+4] <<<< rcx = sTlsData
</code></pre></div></div>
<p>推測ですが、Clang はキャッシュ ヒット率を上げるために命令を入れ替えている気がします。つまり、<code class="language-plaintext highlighter-rouge">gs</code> セグメントにアクセスする処理をまとめたい、という狙いです。結果的に Windows では <code class="language-plaintext highlighter-rouge">gs:[30h]</code> は <code class="language-plaintext highlighter-rouge">TEB::NtTib.Self</code> として自分自身を参照しているだけで、<code class="language-plaintext highlighter-rouge">ptr[gs:[30h] + 58] == gs:[58h]</code> という式が成立するため、順番を入れ替えるメリットは無いはずですが、コンパイラーはその事実を知りません。</p>
<p>この現象を回避するため、まずは基本に忠実に Memory Barrier を入れてみましたが、回避できませんでした。具体的には、以下のように <code class="language-plaintext highlighter-rouge">TestThreadLocalStorageHead</code> の冒頭にインライン アセンブリを追加してみましたが、コンパイル結果は Memory Barrier の有無に関わらず全く同じでした。</p>
<div class="language-c++ highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kr">__declspec</span><span class="p">(</span><span class="n">noinline</span><span class="p">)</span> <span class="kt">bool</span> <span class="nf">TestThreadLocalStorageHead</span><span class="p">()</span> <span class="p">{</span>
<span class="k">auto</span> <span class="n">origTlsHead</span> <span class="o">=</span> <span class="n">SwapThreadLocalStoragePointer</span><span class="p">(</span><span class="nb">nullptr</span><span class="p">);</span>
<span class="cp">#if defined(__clang__)
</span> <span class="k">asm</span> <span class="k">volatile</span><span class="p">(</span><span class="s">""</span> <span class="o">:::</span> <span class="s">"memory"</span><span class="p">);</span>
<span class="cp">#endif
</span> <span class="kt">bool</span> <span class="n">isExceptionThrown</span> <span class="o">=</span> <span class="nb">false</span><span class="p">;</span>
<span class="kr">__try</span> <span class="p">{</span>
<span class="n">sTlsData</span><span class="p">.</span><span class="n">set</span><span class="p">(</span><span class="o">~</span><span class="n">kTlsDataValue</span><span class="p">);</span>
<span class="p">}</span>
<span class="p">...</span>
</code></pre></div></div>
<p><code class="language-plaintext highlighter-rouge">volatile</code> を使っても駄目で、他にエレガントな回避方法が思い浮かばなかったので、<code class="language-plaintext highlighter-rouge">SwapThreadLocalStoragePointer</code> のインライン展開を明示的に無効化して回避してみました。</p>
<p>コードをこうします。<code class="language-plaintext highlighter-rouge">SwapThreadLocalStoragePointer</code> に <code class="language-plaintext highlighter-rouge">__declspec(noinline)</code> を付けただけです。</p>
<div class="language-c++ highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#include <algorithm>
#include <stdio.h>
#include <windows.h>
#include <winnt.h>
#include <winternl.h>
</span>
<span class="k">template</span> <span class="o"><</span><span class="k">typename</span> <span class="nc">T</span><span class="p">></span>
<span class="k">struct</span> <span class="nc">SimpleHolder</span> <span class="p">{</span>
<span class="n">T</span> <span class="n">val_</span> <span class="o">=</span> <span class="p">{};</span>
<span class="kt">void</span> <span class="n">set</span><span class="p">(</span><span class="k">const</span> <span class="n">T</span> <span class="n">val</span><span class="p">)</span> <span class="p">{</span> <span class="n">val_</span> <span class="o">=</span> <span class="n">val</span><span class="p">;</span> <span class="p">}</span>
<span class="k">operator</span> <span class="k">const</span> <span class="n">T</span><span class="o">&</span><span class="p">()</span> <span class="k">const</span> <span class="p">{</span> <span class="k">return</span> <span class="n">val_</span><span class="p">;</span> <span class="p">}</span>
<span class="p">};</span>
<span class="kr">__declspec</span><span class="p">(</span><span class="n">noinline</span><span class="p">)</span> <span class="n">PVOID</span> <span class="nf">SwapThreadLocalStoragePointer</span><span class="p">(</span><span class="n">PVOID</span> <span class="n">newValue</span><span class="p">)</span> <span class="p">{</span>
<span class="n">std</span><span class="o">::</span><span class="n">swap</span><span class="p">(</span><span class="o">::</span><span class="n">NtCurrentTeb</span><span class="p">()</span><span class="o">-></span><span class="n">Reserved1</span><span class="p">[</span><span class="mi">11</span><span class="p">],</span> <span class="n">newValue</span><span class="p">);</span>
<span class="k">return</span> <span class="n">newValue</span><span class="p">;</span>
<span class="p">}</span>
<span class="k">const</span> <span class="kt">uint32_t</span> <span class="n">kTlsDataValue</span> <span class="o">=</span> <span class="mi">42</span><span class="p">;</span>
<span class="k">static</span> <span class="k">thread_local</span> <span class="n">SimpleHolder</span><span class="o"><</span><span class="kt">uint32_t</span><span class="o">></span> <span class="n">sTlsData</span><span class="p">;</span>
<span class="kr">__declspec</span><span class="p">(</span><span class="n">noinline</span><span class="p">)</span> <span class="kt">bool</span> <span class="nf">TestThreadLocalStorageHead</span><span class="p">()</span> <span class="p">{</span>
<span class="k">auto</span> <span class="n">origTlsHead</span> <span class="o">=</span> <span class="n">SwapThreadLocalStoragePointer</span><span class="p">(</span><span class="nb">nullptr</span><span class="p">);</span>
<span class="kt">bool</span> <span class="n">isExceptionThrown</span> <span class="o">=</span> <span class="nb">false</span><span class="p">;</span>
<span class="kr">__try</span> <span class="p">{</span>
<span class="n">sTlsData</span><span class="p">.</span><span class="n">set</span><span class="p">(</span><span class="o">~</span><span class="n">kTlsDataValue</span><span class="p">);</span>
<span class="p">}</span>
<span class="kr">__except</span> <span class="p">(</span><span class="n">GetExceptionCode</span><span class="p">()</span> <span class="o">==</span> <span class="n">EXCEPTION_ACCESS_VIOLATION</span>
<span class="o">?</span> <span class="n">EXCEPTION_EXECUTE_HANDLER</span>
<span class="o">:</span> <span class="n">EXCEPTION_CONTINUE_SEARCH</span><span class="p">)</span> <span class="p">{</span>
<span class="n">isExceptionThrown</span> <span class="o">=</span> <span class="nb">true</span><span class="p">;</span>
<span class="p">}</span>
<span class="n">SwapThreadLocalStoragePointer</span><span class="p">(</span><span class="n">origTlsHead</span><span class="p">);</span>
<span class="n">sTlsData</span><span class="p">.</span><span class="n">set</span><span class="p">(</span><span class="n">kTlsDataValue</span><span class="p">);</span>
<span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">isExceptionThrown</span><span class="p">)</span> <span class="p">{</span>
<span class="n">printf</span><span class="p">(</span><span class="s">"[%s] No exception from setter!</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">__FUNCTION__</span><span class="p">);</span>
<span class="k">return</span> <span class="nb">false</span><span class="p">;</span>
<span class="p">}</span>
<span class="k">if</span> <span class="p">(</span><span class="n">sTlsData</span> <span class="o">!=</span> <span class="n">kTlsDataValue</span><span class="p">)</span> <span class="p">{</span>
<span class="n">printf</span><span class="p">(</span><span class="s">"[%s] TLS is broken!</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">__FUNCTION__</span><span class="p">);</span>
<span class="k">return</span> <span class="nb">false</span><span class="p">;</span>
<span class="p">}</span>
<span class="n">printf</span><span class="p">(</span><span class="s">"[%s] Passed!</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">__FUNCTION__</span><span class="p">);</span>
<span class="n">fflush</span><span class="p">(</span><span class="n">stdout</span><span class="p">);</span>
<span class="k">return</span> <span class="nb">true</span><span class="p">;</span>
<span class="p">}</span>
<span class="kt">int</span> <span class="nf">main</span><span class="p">(</span><span class="kt">int</span> <span class="n">argc</span><span class="p">,</span> <span class="kt">char</span><span class="o">*</span> <span class="n">argv</span><span class="p">[])</span> <span class="p">{</span>
<span class="n">TestThreadLocalStorageHead</span><span class="p">();</span>
<span class="k">return</span> <span class="mi">0</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>
<p>アセンブリはこうなりました。今度は <code class="language-plaintext highlighter-rouge">sTlsData</code> にアクセスするための TLS Head の値を <code class="language-plaintext highlighter-rouge">SwapThreadLocalStoragePointer</code> より後の +103b でキャッシュしているため、Ordering の問題は回避成功です。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>0:000> uf t!TestThreadLocalStorageHead
t!TestThreadLocalStorageHead:
00007ff7`03a11020 55 push rbp
00007ff7`03a11021 56 push rsi
00007ff7`03a11022 4883ec28 sub rsp,28h
00007ff7`03a11026 488d6c2420 lea rbp,[rsp+20h]
00007ff7`03a1102b 31c9 xor ecx,ecx
00007ff7`03a1102d e8ceffffff call t!SwapThreadLocalStoragePointer (00007ff7`03a11000)
00007ff7`03a11032 4889c6 mov rsi,rax
00007ff7`03a11035 8b0505300000 mov eax,dword ptr [t!_tls_index (00007ff7`03a14040)]
00007ff7`03a1103b 65488b0c2558000000 mov rcx,qword ptr gs:[58h]
00007ff7`03a11044 488b04c1 mov rax,qword ptr [rcx+rax*8]
00007ff7`03a11048 488d8804000000 lea rcx,[rax+4]
00007ff7`03a1104f bad5ffffff mov edx,0FFFFFFD5h
00007ff7`03a11054 e8b7000000 call t!SimpleHolder<unsigned int>::set (00007ff7`03a11110)
00007ff7`03a11059 4889f1 mov rcx,rsi
00007ff7`03a1105c e89fffffff call t!SwapThreadLocalStoragePointer (00007ff7`03a11000)
00007ff7`03a11061 8b05d92f0000 mov eax,dword ptr [t!_tls_index (00007ff7`03a14040)]
00007ff7`03a11067 65488b0c2558000000 mov rcx,qword ptr gs:[58h]
00007ff7`03a11070 488b04c1 mov rax,qword ptr [rcx+rax*8]
00007ff7`03a11074 c780040000002a000000 mov dword ptr [rax+4],2Ah
00007ff7`03a1107e 488d0d961f0000 lea rcx,[t!`string' (00007ff7`03a1301b)]
00007ff7`03a11085 488d15741f0000 lea rdx,[t!`string' (00007ff7`03a13000)]
00007ff7`03a1108c e88f000000 call t!printf (00007ff7`03a11120)
00007ff7`03a11091 31c0 xor eax,eax
00007ff7`03a11093 4883c428 add rsp,28h
00007ff7`03a11097 5e pop rsi
00007ff7`03a11098 5d pop rbp
00007ff7`03a11099 c3 ret
0:000> uf t!SwapThreadLocalStoragePointer
t!SwapThreadLocalStoragePointer:
00007ff7`03a11000 65488b142530000000 mov rdx,qword ptr gs:[30h]
00007ff7`03a11009 488b4258 mov rax,qword ptr [rdx+58h]
00007ff7`03a1100d 48894a58 mov qword ptr [rdx+58h],rcx
00007ff7`03a11011 c3 ret
</code></pre></div></div>
<h2 id="問題-2-wrong-seh-context">問題 2: Wrong SEH Context</h2>
<p>実はここからが本題です。<code class="language-plaintext highlighter-rouge">SwapThreadLocalStoragePointer</code> のインライン展開を無効にした上記プログラムを実行すると、今度は SEH しているはずの初回の <code class="language-plaintext highlighter-rouge">sTlsData.set</code> 呼び出しで Second chance exception が発生します。なぜか SEH が機能していません。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>0:000> g
(2b80.1890): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
*** WARNING: Unable to verify checksum for t.exe
rax=0000000000000000 rbx=000002ac11694130 rcx=0000000000000000
rdx=000000d311b64000 rsi=000002ac116939a0 rdi=000002ac1169b400
rip=00007ff703a11044 rsp=000000d31194fa60 rbp=000000d31194fa80
r8=000002ac1169b400 r9=00007ff975e31ec0 r10=0000000000000012
r11=000002ac11696f40 r12=0000000000000000 r13=0000000000000000
r14=0000000000000000 r15=0000000000000000
iopl=0 nv up ei pl zr na po nc
cs=0033 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00010246
t!TestThreadLocalStorageHead+0x24:
00007ff7`03a11044 488b04c1 mov rax,qword ptr [rcx+rax*8] ds:00000000`00000000=????????????????
0:000> g
(2b80.1890): Access violation - code c0000005 (!!! second chance !!!)
rax=0000000000000000 rbx=000002ac11694130 rcx=0000000000000000
rdx=000000d311b64000 rsi=000002ac116939a0 rdi=000002ac1169b400
rip=00007ff703a11044 rsp=000000d31194fa60 rbp=000000d31194fa80
r8=000002ac1169b400 r9=00007ff975e31ec0 r10=0000000000000012
r11=000002ac11696f40 r12=0000000000000000 r13=0000000000000000
r14=0000000000000000 r15=0000000000000000
iopl=0 nv up ei pl zr na po nc
cs=0033 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00010244
t!TestThreadLocalStorageHead+0x24:
00007ff7`03a11044 488b04c1 mov rax,qword ptr [rcx+rax*8] ds:00000000`00000000=????????????????
</code></pre></div></div>
<p>ここでも基本に忠実に、First chance exception からの動作をデバッグすることにします。まずは !exchain で例外ハンドラーのアドレスを調べて、ハンドラーが実行されるかどうかを確認します。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>0:000> g
(2bbc.e0c): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
*** WARNING: Unable to verify checksum for t.exe
t!TestThreadLocalStorageHead+0x24:
00007ff7`03a11044 488b04c1 mov rax,qword ptr [rcx+rax*8] ds:00000000`00000000=????????????????
0:000> !exchain
4 stack frames, scanning for handlers...
Frame 0x00: t!TestThreadLocalStorageHead+0x24 (00007ff7`03a11044)
ehandler t!_C_specific_handler (00007ff7`03a12060)
Frame 0x02: error getting module for 000000000000001f
Frame 0x03: error getting module for 0000000000000001
0:000> bp (00007ff7`03a12060)
0:000> g
Breakpoint 0 hit
t!_C_specific_handler:
00007ff7`03a12060 ff25d2140000 jmp qword ptr [t!_imp___C_specific_handler (00007ff7`03a13538)] ds:00007ff7`03a13538={VCRUNTIME140!__C_specific_handler (00007ff9`6123b830)}
0:000> p
VCRUNTIME140!__C_specific_handler:
00007ff9`6123b830 48895c2408 mov qword ptr [rsp+8],rbx ss:00000013`98efe8c0={t!__favor <PERF> (t+0x5000) (00007ff7`03a15000)}
0:000> kn
# Child-SP RetAddr Call Site
00 00000013`98efe8b8 00007ff9`78dc11ff VCRUNTIME140!__C_specific_handler [d:\agent\_work\2\s\src\vctools\crt\vcruntime\src\eh\riscchandler.cpp @ 175]
01 00000013`98efe8c0 00007ff9`78d8a289 ntdll!RtlpExecuteHandlerForException+0xf
02 00000013`98efe8f0 00007ff9`78dbfe6e ntdll!RtlDispatchException+0x219
03 00000013`98eff000 00007ff7`03a11044 ntdll!KiUserExceptionDispatch+0x2e
04 00000013`98eff7a0 00007ff7`03a11109 t!TestThreadLocalStorageHead+0x24
05 00000013`98eff7e0 00000000`0000001f t!main+0x9
06 00000013`98eff7e8 00000000`00000001 0x1f
07 00000013`98eff7f0 00000000`00000000 0x1
</code></pre></div></div>
<p>ハンドラーは <code class="language-plaintext highlighter-rouge">VCRUNTIME140!__C_specific_handler</code> で確実に実行されており、ここまでは問題なさそうです。この <code class="language-plaintext highlighter-rouge">__C_specific_handler</code> 関数については MSDN のページがあります。</p>
<p>__C_specific_handler function - Win32 apps | Microsoft Docs<br />
<a href="https://docs.microsoft.com/en-us/windows/win32/devnotes/--c-specific-handler2">https://docs.microsoft.com/en-us/windows/win32/devnotes/–c-specific-handler2</a></p>
<p>実はソースコードが MSVC とともにインストールされているはずです。私の Visual Studio 2019 の環境では、<code class="language-plaintext highlighter-rouge">C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Tools\MSVC\14.23.28105\crt\src\vcruntime\riscchandler.cpp</code> というファイルに定義がありました。</p>
<p>ファイルが見つかればロジックを見るのは簡単です。<code class="language-plaintext highlighter-rouge">__C_specific_handler</code> の第四パラメーター<code class="language-plaintext highlighter-rouge">DispatcherContext</code> の中に <code class="language-plaintext highlighter-rouge">ScopeTable</code> という配列が保持されていて、この配列の各要素が例外の捕捉開始アドレス、捕捉終了アドレス、例外フィルターのアドレス、そして例外ハンドラーのアドレスを RVA として保持しています。<code class="language-plaintext highlighter-rouge">__C_specific_handler</code> は、配列をシーケンシャルにループして、例外の発生場所を範囲に含む <code class="language-plaintext highlighter-rouge">ScopeTable</code> の要素が見つかれば、該当する例外フィルターを実行し、その結果が <code class="language-plaintext highlighter-rouge">EXCEPTION_EXECUTE_HANDLER</code> であればハンドラーを実行するというシンプルな実装になっています。では、<code class="language-plaintext highlighter-rouge">__C_specific_handler</code> が呼ばれた時点での <code class="language-plaintext highlighter-rouge">ScopeTable</code> の値を見てみます。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>0:000> r
rax=00007ff703a12060 rbx=0000000000000000 rcx=0000001398eff4f0
rdx=0000001398eff7a0 rsi=0000001398eff4f0 rdi=0000000000000000
rip=00007ff96123b830 rsp=0000001398efe8b8 rbp=0000001398efee30
r8=0000001398eff000 r9=0000001398efee80 r10=00007ff703a10000
r11=0000000000000001 r12=00007ff703a12060 r13=0000001398eff000
r14=0000001398efe930 r15=0000000000000000
iopl=0 nv up ei pl nz na po nc
cs=0033 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000206
VCRUNTIME140!__C_specific_handler:
00007ff9`6123b830 48895c2408 mov qword ptr [rsp+8],rbx ss:00000013`98efe8c0={t!__favor <PERF> (t+0x5000) (00007ff7`03a15000)}
0:000> dv
ExceptionRecord = 0x00000013`98eff4f0
EstablisherFrame = 0x00000013`98eff7a0
ContextRecord = 0x00000013`98eff000
DispatcherContext = 0x00000013`98efee80
ImageBase = <value unavailable>
ExceptionFilter = <value unavailable>
Index = <value unavailable>
TargetIndex = <value unavailable>
TargetPc = <value unavailable>
TerminationHandler = <value unavailable>
ScopeTable = <value unavailable>
Handler = <value unavailable>
ExceptionPointers = struct _EXCEPTION_POINTERS
Value = <value unavailable>
ControlPc = <value unavailable>
0:000> dt DispatcherContext HandlerData
Local var @ r9 Type _DISPATCHER_CONTEXT*
+0x038 HandlerData : 0x00007ff7`03a13b50 Void
0:000> dt vcruntime140!SCOPE_TABLE_AMD64 0x00007ff7`03a13b50
+0x000 Count : 1
+0x004 ScopeRecord : [1] _SCOPE_TABLE_AMD64::<unnamed-type-ScopeRecord>
0:000> dt vcruntime140!SCOPE_TABLE_AMD64 0x00007ff7`03a13b50 ScopeRecord[0].
+0x004 ScopeRecord : [0]
+0x000 BeginAddress : 0x1050
+0x004 EndAddress : 0x105a
+0x008 HandlerAddress : 0x10f0
+0x00c JumpTarget : 0x109a
</code></pre></div></div>
<p>それっぽいレコードが一件ありました。</p>
<p>ところで、riscchandler.cpp の判定条件は以下のようになっています。</p>
<div class="language-c++ highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="k">for</span> <span class="p">(</span><span class="n">Index</span> <span class="o">=</span> <span class="n">DispatcherContext</span><span class="o">-></span><span class="n">ScopeIndex</span><span class="p">;</span> <span class="n">Index</span> <span class="o"><</span> <span class="n">ScopeTable</span><span class="o">-></span><span class="n">Count</span><span class="p">;</span> <span class="n">Index</span> <span class="o">+=</span> <span class="mi">1</span><span class="p">)</span> <span class="p">{</span>
<span class="k">if</span> <span class="p">((</span><span class="n">ControlPc</span> <span class="o">>=</span> <span class="n">ScopeTable</span><span class="o">-></span><span class="n">ScopeRecord</span><span class="p">[</span><span class="n">Index</span><span class="p">].</span><span class="n">BeginAddress</span><span class="p">)</span> <span class="o">&&</span>
<span class="p">(</span><span class="n">ControlPc</span> <span class="o"><</span> <span class="n">ScopeTable</span><span class="o">-></span><span class="n">ScopeRecord</span><span class="p">[</span><span class="n">Index</span><span class="p">].</span><span class="n">EndAddress</span><span class="p">)</span> <span class="o">&&</span>
</code></pre></div></div>
<p>したがって、上記デバッグログから見つかった <code class="language-plaintext highlighter-rouge">ScopeRecord</code> が定義する [+1050, +105a) の範囲で例外が発生した場合、フィルター +10f0 を実行してからハンドラー +0x109a にジャンプします。</p>
<p>なお、フィールドの名前が <code class="language-plaintext highlighter-rouge">HandlerAddress</code> となっていますがこれは間違った名称で、本来は <code class="language-plaintext highlighter-rouge">FilterAddress</code> とすべきです。無意味な難読化が施されています。</p>
<p>RVA が分かったので、アセンブリを確認します。<code class="language-plaintext highlighter-rouge">BeginAddress</code> と <code class="language-plaintext highlighter-rouge">EndAddress</code> の RVA は、<code class="language-plaintext highlighter-rouge">__try</code> ブロックに基づいてコンパイラーが導出しているはずで、当然 <code class="language-plaintext highlighter-rouge">t!TestThreadLocalStorageHead</code> の範囲内にあります。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>0:000> u t!TestThreadLocalStorageHead t+105a
t!TestThreadLocalStorageHead:
00007ff7`03a11020 55 push rbp
00007ff7`03a11021 56 push rsi
00007ff7`03a11022 4883ec28 sub rsp,28h
00007ff7`03a11026 488d6c2420 lea rbp,[rsp+20h]
00007ff7`03a1102b 31c9 xor ecx,ecx
00007ff7`03a1102d e8ceffffff call t!SwapThreadLocalStoragePointer (00007ff7`03a11000)
00007ff7`03a11032 4889c6 mov rsi,rax
00007ff7`03a11035 8b0505300000 mov eax,dword ptr [t!_tls_index (00007ff7`03a14040)]
00007ff7`03a1103b 65488b0c2558000000 mov rcx,qword ptr gs:[58h]
00007ff7`03a11044 488b04c1 mov rax,qword ptr [rcx+rax*8]
00007ff7`03a11048 488d8804000000 lea rcx,[rax+4]
00007ff7`03a1104f bad5ffffff mov edx,0FFFFFFD5h
00007ff7`03a11054 e8b7000000 call t!SimpleHolder<unsigned int>::set (00007ff7`03a11110)
00007ff7`03a11059 4889f1 mov rcx,rsi
</code></pre></div></div>
<p>[+1050, +105a) という範囲に含まれるのは、+1054 の <code class="language-plaintext highlighter-rouge">call</code> と +1059 の <code class="language-plaintext highlighter-rouge">mov</code> のみであり、例外が発生した +1044 は範囲外です。これが、Second chance exception の発生した原因です。Clang が間違っている!</p>
<p>Clang が間違った ScopeRecord を生成する理由はちょっと推測できません。Thread local というイレギュラーは除外するにしても、パラメーター渡しのために <code class="language-plaintext highlighter-rouge">rcx</code> や <code class="language-plaintext highlighter-rouge">edx</code> をセットする命令は <code class="language-plaintext highlighter-rouge">__try</code> の中に含まれていると考えるのが自然なのですが、それらを行う +1048 や +104f の <code class="language-plaintext highlighter-rouge">mov</code> すらも例外捕捉の範囲外となっています。</p>
<h2 id="msvc-の場合">MSVC の場合</h2>
<p>全く同じコードを MSVC でコンパイルした場合、いずれの問題も発生しません。まずは全体のアセンブリ。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>0:000> uf t!TestThreadLocalStorageHead
t!TestThreadLocalStorageHead [D:\src\msvc-nmake-template\src\tls.cpp @ 22]:
22 00007ff6`aa5fdd60 4883ec28 sub rsp,28h
23 00007ff6`aa5fdd64 65488b042530000000 mov rax,qword ptr gs:[30h]
23 00007ff6`aa5fdd6d 4c8b4058 mov r8,qword ptr [rax+58h]
23 00007ff6`aa5fdd71 4c89442430 mov qword ptr [rsp+30h],r8
23 00007ff6`aa5fdd76 48c7405800000000 mov qword ptr [rax+58h],0
24 00007ff6`aa5fdd7e 4532db xor r11b,r11b
26 00007ff6`aa5fdd81 448b0d04430700 mov r9d,dword ptr [t!_tls_index (00007ff6`aa67208c)]
26 00007ff6`aa5fdd88 65488b0c2558000000 mov rcx,qword ptr gs:[58h]
26 00007ff6`aa5fdd91 41ba04010000 mov r10d,104h
26 00007ff6`aa5fdd97 418bc2 mov eax,r10d
26 00007ff6`aa5fdd9a 4a0304c9 add rax,qword ptr [rcx+r9*8]
26 00007ff6`aa5fdd9e c700d5ffffff mov dword ptr [rax],0FFFFFFD5h
27 00007ff6`aa5fdda4 eb15 jmp t!TestThreadLocalStorageHead+0x5b (00007ff6`aa5fddbb)
t!TestThreadLocalStorageHead+0x5b [D:\src\msvc-nmake-template\src\tls.cpp @ 33]:
33 00007ff6`aa5fddbb 65488b042530000000 mov rax,qword ptr gs:[30h]
33 00007ff6`aa5fddc4 4c894058 mov qword ptr [rax+58h],r8
34 00007ff6`aa5fddc8 418bd1 mov edx,r9d
34 00007ff6`aa5fddcb 65488b042558000000 mov rax,qword ptr gs:[58h]
34 00007ff6`aa5fddd4 418bca mov ecx,r10d
34 00007ff6`aa5fddd7 488b04d0 mov rax,qword ptr [rax+rdx*8]
34 00007ff6`aa5fdddb c704012a000000 mov dword ptr [rcx+rax],2Ah
36 00007ff6`aa5fdde2 488d1577f40500 lea rdx,[t!`string' (00007ff6`aa65d260)]
36 00007ff6`aa5fdde9 4584db test r11b,r11b
36 00007ff6`aa5fddec 7513 jne t!TestThreadLocalStorageHead+0xa1 (00007ff6`aa5fde01)
t!TestThreadLocalStorageHead+0x8e [D:\src\msvc-nmake-template\src\tls.cpp @ 37]:
37 00007ff6`aa5fddee 488d0d93f40500 lea rcx,[t!`string' (00007ff6`aa65d288)]
37 00007ff6`aa5fddf5 e8b947ffff call t!ILT+5550(_vfprintf_l) (00007ff6`aa5f25b3)
38 00007ff6`aa5fddfa 32c0 xor al,al
47 00007ff6`aa5fddfc 4883c428 add rsp,28h
47 00007ff6`aa5fde00 c3 ret
t!TestThreadLocalStorageHead+0xa1 [D:\src\msvc-nmake-template\src\tls.cpp @ 44]:
44 00007ff6`aa5fde01 488d0da8f40500 lea rcx,[t!`string' (00007ff6`aa65d2b0)]
44 00007ff6`aa5fde08 e8a647ffff call t!ILT+5550(_vfprintf_l) (00007ff6`aa5f25b3)
45 00007ff6`aa5fde0d b901000000 mov ecx,1
45 00007ff6`aa5fde12 e8b533ffff call t!ILT+455(__acrt_iob_func) (00007ff6`aa5f11cc)
45 00007ff6`aa5fde17 488bc8 mov rcx,rax
45 00007ff6`aa5fde1a e85158ffff call t!ILT+9835(fflush) (00007ff6`aa5f3670)
46 00007ff6`aa5fde1f b001 mov al,1
47 00007ff6`aa5fde21 4883c428 add rsp,28h
47 00007ff6`aa5fde25 c3 ret
</code></pre></div></div>
<p>ここで重要なのは +dd88 の <code class="language-plaintext highlighter-rouge">mov</code> が +dd76 の <code class="language-plaintext highlighter-rouge">mov</code> より後に実行されることです。MSVC の場合、Memory Ordering の問題は <code class="language-plaintext highlighter-rouge">SwapThreadLocalStoragePointer</code> がインライン展開されたとしても発生しないことが分かります。</p>
<p>次に ScopeRecord の確認。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>0:000> g
(9d0.1984): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
t!TestThreadLocalStorageHead+0x3a:
00007ff6`aa5fdd9a 4a0304c9 add rax,qword ptr [rcx+r9*8] ds:00000000`00000000=????????????????
0:000> !exchain
5 stack frames, scanning for handlers...
Frame 0x00: t!TestThreadLocalStorageHead+0x3a (00007ff6`aa5fdd9a)
ehandler t!ILT+7905(__C_specific_handler) (00007ff6`aa5f2ee6)
Frame 0x02: t!__scrt_common_main_seh+0x10c (00007ff6`aa5f98b4)
ehandler t!ILT+7905(__C_specific_handler) (00007ff6`aa5f2ee6)
Frame 0x04: ntdll!RtlUserThreadStart+0x21 (00007ff9`78d8ced1)
ehandler ntdll!_C_specific_handler (00007ff9`78dac640)
0:000> g (00007ff6`aa5f2ee6)
t!ILT+7905(__C_specific_handler):
00007ff6`aa5f2ee6 e999780000 jmp t!__C_specific_handler (00007ff6`aa5fa784)
0:000> p
t!__C_specific_handler:
00007ff6`aa5fa784 48895c2408 mov qword ptr [rsp+8],rbx ss:0000006d`33f9eb00={t!__dyn_tls_dtor_callback <PERF> (t+0x8554c) (00007ff6`aa67554c)}
0:000> dv
ExceptionRecord = 0x0000006d`33f9f730
EstablisherFrame = 0x0000006d`33f9f9e0
ContextRecord = 0x0000006d`33f9f240
DispatcherContext = 0x0000006d`33f9f0c0
ImageBase = <value unavailable>
ExceptionFilter = <value unavailable>
Index = <value unavailable>
TargetIndex = <value unavailable>
TargetPc = <value unavailable>
TerminationHandler = <value unavailable>
ScopeTable = <value unavailable>
Handler = <value unavailable>
ExceptionPointers = struct _EXCEPTION_POINTERS
Value = <value unavailable>
ControlPc = <value unavailable>
0:000> dt DispatcherContext HandlerData
Local var @ r9 Type _DISPATCHER_CONTEXT*
+0x038 HandlerData : 0x00007ff6`aa66a3c8 Void
0:000> dt t!SCOPE_TABLE_AMD64
+0x000 Count : Uint4B
+0x004 ScopeRecord : [1] _SCOPE_TABLE_AMD64::<unnamed-type-ScopeRecord>
0:000> dt t!SCOPE_TABLE_AMD64 0x00007ff6`aa66a3c8
+0x000 Count : 1
+0x004 ScopeRecord : [1] _SCOPE_TABLE_AMD64::<unnamed-type-ScopeRecord>
0:000> dt t!SCOPE_TABLE_AMD64 0x00007ff6`aa66a3c8 ScopeRecord[0].
+0x004 ScopeRecord : [0]
+0x000 BeginAddress : 0xdd81
+0x004 EndAddress : 0xdda6
+0x008 HandlerAddress : 0x6a010
+0x00c JumpTarget : 0xdda6
</code></pre></div></div>
<p>範囲は [+dd81, +dda6) で、この範囲には例外発生場所もしっかり含まれています。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> 26 00007ff6`aa5fdd81 448b0d04430700 mov r9d,dword ptr [t!_tls_index (00007ff6`aa67208c)]
26 00007ff6`aa5fdd88 65488b0c2558000000 mov rcx,qword ptr gs:[58h]
26 00007ff6`aa5fdd91 41ba04010000 mov r10d,104h
26 00007ff6`aa5fdd97 418bc2 mov eax,r10d
26 00007ff6`aa5fdd9a 4a0304c9 add rax,qword ptr [rcx+r9*8] <<<< Exception!
26 00007ff6`aa5fdd9e c700d5ffffff mov dword ptr [rax],0FFFFFFD5h
27 00007ff6`aa5fdda4 eb15 jmp t!TestThreadLocalStorageHead+0x5b (00007ff6`aa5fddbb)
</code></pre></div></div>
<h2 id="clang-でグローバル変数の場合">Clang でグローバル変数の場合</h2>
<p>Thread local というイレギュラーを除外した場合、Clang が生成する Scope Record がどうなるのか気になるところです。次のコードで試します。<code class="language-plaintext highlighter-rouge">SwapThreadLocalStoragePointer</code> の <code class="language-plaintext highlighter-rouge">noinline</code> と <code class="language-plaintext highlighter-rouge">sTlsData</code> の <code class="language-plaintext highlighter-rouge">thread_local</code> を削除しました。</p>
<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#include <algorithm>
#include <stdio.h>
#include <windows.h>
#include <winnt.h>
#include <winternl.h>
</span>
<span class="k">template</span> <span class="o"><</span><span class="k">typename</span> <span class="nc">T</span><span class="p">></span>
<span class="k">struct</span> <span class="nc">SimpleHolder</span> <span class="p">{</span>
<span class="n">T</span> <span class="n">val_</span> <span class="o">=</span> <span class="p">{};</span>
<span class="kt">void</span> <span class="n">set</span><span class="p">(</span><span class="k">const</span> <span class="n">T</span> <span class="n">val</span><span class="p">)</span> <span class="p">{</span> <span class="n">val_</span> <span class="o">=</span> <span class="n">val</span><span class="p">;</span> <span class="p">}</span>
<span class="k">operator</span> <span class="k">const</span> <span class="n">T</span><span class="o">&</span><span class="p">()</span> <span class="k">const</span> <span class="p">{</span> <span class="k">return</span> <span class="n">val_</span><span class="p">;</span> <span class="p">}</span>
<span class="p">};</span>
<span class="n">PVOID</span> <span class="nf">SwapThreadLocalStoragePointer</span><span class="p">(</span><span class="n">PVOID</span> <span class="n">newValue</span><span class="p">)</span> <span class="p">{</span>
<span class="n">std</span><span class="o">::</span><span class="n">swap</span><span class="p">(</span><span class="o">::</span><span class="n">NtCurrentTeb</span><span class="p">()</span><span class="o">-></span><span class="n">Reserved1</span><span class="p">[</span><span class="mi">11</span><span class="p">],</span> <span class="n">newValue</span><span class="p">);</span>
<span class="k">return</span> <span class="n">newValue</span><span class="p">;</span>
<span class="p">}</span>
<span class="k">const</span> <span class="kt">uint32_t</span> <span class="n">kTlsDataValue</span> <span class="o">=</span> <span class="mi">42</span><span class="p">;</span>
<span class="k">static</span> <span class="n">SimpleHolder</span><span class="o"><</span><span class="kt">uint32_t</span><span class="o">></span> <span class="n">sTlsData</span><span class="p">;</span>
<span class="kr">__declspec</span><span class="p">(</span><span class="n">noinline</span><span class="p">)</span> <span class="kt">bool</span> <span class="nf">TestThreadLocalStorageHead</span><span class="p">()</span> <span class="p">{</span>
<span class="k">auto</span> <span class="n">origTlsHead</span> <span class="o">=</span> <span class="n">SwapThreadLocalStoragePointer</span><span class="p">(</span><span class="nb">nullptr</span><span class="p">);</span>
<span class="kt">bool</span> <span class="n">isExceptionThrown</span> <span class="o">=</span> <span class="nb">false</span><span class="p">;</span>
<span class="kr">__try</span> <span class="p">{</span>
<span class="n">sTlsData</span><span class="p">.</span><span class="n">set</span><span class="p">(</span><span class="o">~</span><span class="n">kTlsDataValue</span><span class="p">);</span>
<span class="p">}</span>
<span class="kr">__except</span> <span class="p">(</span><span class="n">GetExceptionCode</span><span class="p">()</span> <span class="o">==</span> <span class="n">EXCEPTION_ACCESS_VIOLATION</span>
<span class="o">?</span> <span class="n">EXCEPTION_EXECUTE_HANDLER</span>
<span class="o">:</span> <span class="n">EXCEPTION_CONTINUE_SEARCH</span><span class="p">)</span> <span class="p">{</span>
<span class="n">isExceptionThrown</span> <span class="o">=</span> <span class="nb">true</span><span class="p">;</span>
<span class="p">}</span>
<span class="n">SwapThreadLocalStoragePointer</span><span class="p">(</span><span class="n">origTlsHead</span><span class="p">);</span>
<span class="n">sTlsData</span><span class="p">.</span><span class="n">set</span><span class="p">(</span><span class="n">kTlsDataValue</span><span class="p">);</span>
<span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">isExceptionThrown</span><span class="p">)</span> <span class="p">{</span>
<span class="n">printf</span><span class="p">(</span><span class="s">"[%s] No exception from setter!</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">__FUNCTION__</span><span class="p">);</span>
<span class="k">return</span> <span class="nb">false</span><span class="p">;</span>
<span class="p">}</span>
<span class="k">if</span> <span class="p">(</span><span class="n">sTlsData</span> <span class="o">!=</span> <span class="n">kTlsDataValue</span><span class="p">)</span> <span class="p">{</span>
<span class="n">printf</span><span class="p">(</span><span class="s">"[%s] TLS is broken!</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">__FUNCTION__</span><span class="p">);</span>
<span class="k">return</span> <span class="nb">false</span><span class="p">;</span>
<span class="p">}</span>
<span class="n">printf</span><span class="p">(</span><span class="s">"[%s] Passed!</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">__FUNCTION__</span><span class="p">);</span>
<span class="n">fflush</span><span class="p">(</span><span class="n">stdout</span><span class="p">);</span>
<span class="k">return</span> <span class="nb">true</span><span class="p">;</span>
<span class="p">}</span>
<span class="kt">int</span> <span class="nf">main</span><span class="p">(</span><span class="kt">int</span> <span class="n">argc</span><span class="p">,</span> <span class="kt">char</span><span class="o">*</span> <span class="n">argv</span><span class="p">[])</span> <span class="p">{</span>
<span class="n">TestThreadLocalStorageHead</span><span class="p">();</span>
<span class="k">return</span> <span class="mi">0</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>
<p>TLS は無関係になるので、このプログラムを実行しても例外は発生しません。<del><code class="language-plaintext highlighter-rouge">DispatcherContext</code> の <code class="language-plaintext highlighter-rouge">HandlerData</code> は対象イメージの <code class="language-plaintext highlighter-rouge">.rdata</code> セクションにハードコードされているので、それを目視で見つけることにします。</del></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>0:000> !dh t
(...snip...)
SECTION HEADER #2
.rdata name
CD4 virtual size
3000 virtual address
E00 size of raw data
1800 file pointer to raw data
0 file pointer to relocation table
0 file pointer to line numbers
0 number of relocations
0 number of line numbers
40000040 flags
Initialized Data
(no align specified)
Read Only
(...snip...)
0:000> dd t+3000 t+3cd4
(...snip...)
00007ff6`f3573ad0 7472632d 6165682d 316c2d70 302d312d
00007ff6`f3573ae0 6c6c642e 00000000 00000000 00000000
00007ff6`f3573af0 00000000 00000000 00000000 00000000
00007ff6`f3573b00 00000000 00000000 25040b19 4206030b
00007ff6`f3573b10 50016002 00002040 00000001 00001041
00007ff6`f3573b20 00001052 000010d0 00001084 00010401
00007ff6`f3573b30 00004204 00010401 00004204 00020601
00007ff6`f3573b40 30023206 00040a01 0006340a 7006320a
00007ff6`f3573b50 00020601 30023206 00010401 00004204
(...snip...)
0:000> dt vcruntime140!SCOPE_TABLE_AMD64 00007ff6`f3573b18
+0x000 Count : 1
+0x004 ScopeRecord : [1] _SCOPE_TABLE_AMD64::<unnamed-type-ScopeRecord>
0:000> dt vcruntime140!SCOPE_TABLE_AMD64 00007ff6`f3573b18 ScopeRecord[0].
+0x004 ScopeRecord : [0]
+0x000 BeginAddress : 0x1041
+0x004 EndAddress : 0x1052
+0x008 HandlerAddress : 0x10d0
+0x00c JumpTarget : 0x1084
0:000> u t!TestThreadLocalStorageHead t+1052
t!TestThreadLocalStorageHead:
00007ff6`f3571020 55 push rbp
00007ff6`f3571021 56 push rsi
00007ff6`f3571022 4883ec28 sub rsp,28h
00007ff6`f3571026 488d6c2420 lea rbp,[rsp+20h]
00007ff6`f357102b 65488b042530000000 mov rax,qword ptr gs:[30h]
00007ff6`f3571034 488b7058 mov rsi,qword ptr [rax+58h]
00007ff6`f3571038 48c7405800000000 mov qword ptr [rax+58h],0
00007ff6`f3571040 488d0ded2f0000 lea rcx,[t!__scrt_ucrt_dll_is_in_use+0x4 (00007ff6`f3574034)]
00007ff6`f3571047 bad5ffffff mov edx,0FFFFFFD5h
00007ff6`f357104c e89f000000 call t!SimpleHolder<unsigned int>::set (00007ff6`f35710f0)
00007ff6`f3571051 65488b042530000000 mov rax,qword ptr gs:[30h]
</code></pre></div></div>
<p><del>範囲は [+1041, +1052) でした。面白いことに、グローバル変数の場合はパラメーター渡しである +1047 の mov は例外捕捉範囲に含まれています。しかし依然として、<code class="language-plaintext highlighter-rouge">sTlsData</code> を設定する +1040 の mov は範囲外です。疑問の残る結果です。</del></p>
<p>[2019/11/30 追記]</p>
<p>さすがに <code class="language-plaintext highlighter-rouge">.rdata</code> セクションを目視する方法は汎用性、及び実用性に欠けるので、x64 における SEH の構造をダンプするエクステンションを <a href="https://github.com/msmania/bangon">on.dll</a> の !ex コマンドとして追加しました。</p>
<p>上記 C++ コードをデバッガーで実行してから、例外が捕捉され得るアドレスを取得します。この場合は <code class="language-plaintext highlighter-rouge">__try</code> で囲まれた <code class="language-plaintext highlighter-rouge">sTlsData.set(~kTlsDataValue);</code> です。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>0:000> uf t!TestThreadLocalStorageHead
t!TestThreadLocalStorageHead:
00007ff6`59f01020 55 push rbp
00007ff6`59f01021 56 push rsi
00007ff6`59f01022 4883ec28 sub rsp,28h
00007ff6`59f01026 488d6c2420 lea rbp,[rsp+20h]
00007ff6`59f0102b 65488b042530000000 mov rax,qword ptr gs:[30h]
00007ff6`59f01034 488b7058 mov rsi,qword ptr [rax+58h]
00007ff6`59f01038 48c7405800000000 mov qword ptr [rax+58h],0
00007ff6`59f01040 488d0ded2f0000 lea rcx,[t!sTlsData (00007ff6`59f04034)]
00007ff6`59f01047 bad5ffffff mov edx,0FFFFFFD5h
00007ff6`59f0104c e89f000000 call t!SimpleHolder<unsigned int>::set (00007ff6`59f010f0)
00007ff6`59f01051 65488b042530000000 mov rax,qword ptr gs:[30h]
00007ff6`59f0105a 48897058 mov qword ptr [rax+58h],rsi
00007ff6`59f0105e c705cc2f00002a000000 mov dword ptr [t!sTlsData (00007ff6`59f04034)],2Ah
00007ff6`59f01068 488d0dac1f0000 lea rcx,[t!`string' (00007ff6`59f0301b)]
00007ff6`59f0106f 488d158a1f0000 lea rdx,[t!`string' (00007ff6`59f03000)]
00007ff6`59f01076 e885000000 call t!printf (00007ff6`59f01100)
00007ff6`59f0107b 31c0 xor eax,eax
00007ff6`59f0107d 4883c428 add rsp,28h
00007ff6`59f01081 5e pop rsi
00007ff6`59f01082 5d pop rbp
00007ff6`59f01083 c3 ret
0:000> .load on
0:000> !ex t 00007ff6`59f0104c
@00007ff6`59f05000
UNWIND_INFO[0] 00007ff6`59f03b00 [ 00007ff6`59f01020 00007ff6`59f010c4 )
Version = 1
Flags = 3
SizeOfProlog = 11
FrameRegister = 5
FrameOffset = 2
UnwindCode[0] = {CodeOffset:11 UnwindOp:3 OpInfo:0}
UnwindCode[1] = {CodeOffset:6 UnwindOp:2 OpInfo:4}
UnwindCode[2] = {CodeOffset:2 UnwindOp:0 OpInfo:6}
UnwindCode[3] = {CodeOffset:1 UnwindOp:0 OpInfo:5}
ExceptionHandler = 00007ff6`59f02040 t!_C_specific_handler
HandlerData = 00007ff6`59f03b10
ScopeRecord[0] 00007ff6`59f03b14 = {
[ 00007ff6`59f01041 00007ff6`59f01052 )
Filter: 00007ff6`59f010d0 t!TestThreadLocalStorageHead+0xb0
Handler: 00007ff6`59f01084 t!TestThreadLocalStorageHead+0x64
}
</code></pre></div></div>
<p>上記出力から、ScopeRecord の範囲は [ 00007ff6`59f01041 00007ff6`59f01052 ) となっており、この範囲にはパラメーターをセットする +1047 の <code class="language-plaintext highlighter-rouge">mov</code> は含まれていますが <code class="language-plaintext highlighter-rouge">this</code> ポインターをセットする +1040 の <code class="language-plaintext highlighter-rouge">lea</code> が含まれていません。やはり疑問の残る結果です。</p>
<h1 id="おわりに">おわりに</h1>
<p>というわけで、Windows の SEH と thread local 変数にまつわる Clang の不思議な動作についてまとめました。<del>もう少し情報を集めて本当に怪しかったら LLVM にバグ登録しようかと思います。</del></p>
<p>LLVM にバグ登録しました。</p>
<p>44174 – A range of ScopeRecord does not match the code enclosed by __try<br />
<a href="https://bugs.llvm.org/show_bug.cgi?id=44174">https://bugs.llvm.org/show_bug.cgi?id=44174</a></p>
<p>本記事で引用したコードは コンパイル パラメーターなども含めて NMAKE プロジェクトとしてここに置きました。</p>
<p><a href="https://github.com/msmania/seh-debug/tree/191124-blogpost">https://github.com/msmania/seh-debug/tree/191124-blogpost</a></p>はじめにCPU を買った2019-11-23T20:00:00+00:002019-11-23T20:00:00+00:00https://msmania.github.io/2019/11/23/desktop-pc<h3 id="計画と購入">計画と購入</h3>
<p>家で仕事をするようになったため、メインの作業はもともと使っていたデスクトップ PC で行うようになりました。会社からは Lenovo X280 が支給されたのですが、私はモニターが二枚ないと作業効率が著しく落ちる人間なので、ラップトップはビルドを並行したいときや、自宅以外で使っています。ただし、デスクトップ PC の CPU はいわゆる第二世代 (Sandy Bridge) の i7-2600 なので、第八世代 (Kaby Lake) の Core i7-8650U を積んでいる Lenovo と比べると明らかにビルド時間などが遅いのが不満でした。さらに、友人から使わなくなったモニターを譲ってもらったので、これを期に二台構成だった環境を三台構成にしたくなり、パソコンの買い替えを検討することにしました。過去のメール履歴を調べると、デスクトップ PC は 2011/10 にドスパラで注文したものでした。当時 80,000 円だったようです。8 年の間に国を跨って 6 回ぐらい引越ししてますが、よく故障せずに済んだものです。</p>
<p>仕事効率にも直結するのであまりケチらず、CPU は第九世代の i7 + メモリ 32GB の構成で値段を調べると、Dell で $1,600、HP で $1,700 ぐらいすることが分かりました。さすがに高すぎる。そこで、今使っている PC のパーツを変更することにしました。買うのは CPU、GPU、マザーボード、メモリ、電源です。これなら $1,000 以内で済みます。で、Amazon と BestBuy を組み合わせて買ったのは↓。トータル $832.59。</p>
<ul>
<li>Core i7-9700 - $335.99</li>
<li>NVIDIA GeForce GTX 1660 - $235.39</li>
<li>Gigabyte B365M DS3H - $68.75</li>
<li>DDR4-2666 16GB x2 - $138.59</li>
<li>Thermaltake SMART 600W $53.87</li>
</ul>
<p>CPU。さすがに Core i9 は高すぎる。できれば 9700F が欲しいところだったのですが、どこにも在庫がない。普通は 9700K を買うべきなのでしょうが、冷却ファンを別に買うのが面倒くさいという消極的な理由で 9700 にしました。なんだかんだ AMD より Intel が好きです。</p>
<p>GPU。RTX シリーズは高すぎる上にレイトレなんぞ要らん。Radeon の RX 580 あたりも気になりましたが、結局 NVIDIA をチョイス。別にアンチ AMD なわけではない。</p>
<p>マザボ。Q や Z シリーズは高すぎるので検討していませんが、B360/B365/H370 のどれにするかはけっこう迷いました。値段もそんなに変わらない。特に H370 の USB 3.1 は魅力的でしたが、今のところ 3.1 のデバイスを持っていないので B365 にしました。正直 H370 の方がよかったかもしれない。</p>
<p>電源。今まで使っていたのが 350W で、RX 580 だと推奨電源が 500W だったので一応購入。だが GTX 1660 は 350W でも動くっぽいので、買わなくてもよかったかもしれない。というか Radeon は電力を消費し過ぎなのでは。</p>
<h3 id="ハードウェア">ハードウェア</h3>
<p>実はマザーボードを交換するのは初めてなのと、わりと勢いでパーツを選んだので、かなり不安のある作業でした。</p>
<p>まず電源を交換。</p>
<p><img src="/assets/2019-11-23-01-power.jpg" alt="" /><br /></p>
<p>マザーボードを交換。この細かいケーブル (Power, Reset, Power LED, HDD LED, Chassis Speaker) の処理がミスを誘ってくる。</p>
<p><img src="/assets/2019-11-23-02-cables.jpg" alt="" /><br /></p>
<p>取れた。</p>
<p><img src="/assets/2019-11-23-03-oldboard.jpg" alt="" /><br /></p>
<p>これが新しいボード。ヒートシンクが小さくなった。</p>
<p><img src="/assets/2019-11-23-04-newboard.jpg" alt="" /><br /></p>
<p>いろいろ繋いだ後。COM ポートというロマン溢れるカードに注目してほしい。</p>
<p><img src="/assets/2019-11-23-05-done.jpg" alt="" /><br /></p>
<h3 id="ソフトウェア">ソフトウェア</h3>
<p>事前の懸念としてあったのは、BIOS が第九世代の CPU に対応しているかどうかです。B365 が出た当時はまだ第九世代がリリースされていなかったらしく、古い BIOS では第九世代が動かないという情報があり、現在出荷時の BIOS が古かったら動作しないことになります。この場合は第八世代の CPU を買って BIOS だけ更新してから返品するコース。面倒な上、店にも迷惑に違いない。</p>
<p>結果的には、下記の F4 バージョンがインストール済みで問題なしでした。</p>
<p>B365M DS3H (rev. 1.0) | Motherboard - GIGABYTE U.S.A.<br />
<a href="https://www.gigabyte.com/us/Motherboard/B365M-DS3H-rev-10/support#support-dl-bios">https://www.gigabyte.com/us/Motherboard/B365M-DS3H-rev-10/support#support-dl-bios</a></p>
<p>一応 F5 にアップデートしておきます。アメリカは電圧がけっこう不安定なので、このタイミングも不安です。そもそも平日の夕方じゃなくて週末にやれよって話ですが。</p>
<p><img src="/assets/2019-11-23-06-bios-update.jpg" alt="" /><br /></p>
<p>一つだけ問題が発生。グラフィック カードを差すと Windows が起動しなくなる現象に遭遇。エラーメッセージは “BlInitializeLibrary failed 0xc00000bb”。まさかカーネルデバッグの出番か。</p>
<p><img src="/assets/2019-11-23-07-boot-error.jpg" alt="" /><br /></p>
<p>一応 KB はあったが、解決策がよく分からない。</p>
<p>“BlInitializeLibrary failed XXX” error when you install or start an operating system on a 64-bit UEFI-based computer<br />
<a href="https://support.microsoft.com/en-us/help/4020050/blinitializelibrary-failed-xxx-error-when-you-install-or-start-an-oper">https://support.microsoft.com/en-us/help/4020050/blinitializelibrary-failed-xxx-error-when-you-install-or-start-an-oper</a></p>
<p>BIOS の設定が怪しいと睨んでそれっぽい設定をいろいろ弄ったところ “Above 4G Decoding” を Enabled にしたことで解決しました。何だったんだ一体。</p>
<p><img src="/assets/2019-11-23-08-bios-4g-decoding.jpg" alt="" /><br /></p>
<p>というわけで作業完了。モニターを縦置きで使うのは初めてなのですが、これは素晴らしい。Windows をマルチ DPI 構成で使うといろいろおかしな動作をするのは相変わらずですが、我慢できるレベルです。</p>
<p><img src="/assets/2019-11-23-09-desktop.jpg" alt="" /><br /></p>
<p>作業前では、4K モニターで Firefox を使うとレンダリングが極度に遅くなる現象 (たぶんこれ・・・? <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1549901">https://bugzilla.mozilla.org/show_bug.cgi?id=1549901</a>) に遭遇していたのですが、作業後は解消されました。交換前のグラフィック カードが Quadro FX 580 で、GPU のスペックが足りなかったのが原因かもしません。</p>
<p>江添氏の 「<a href="https://cpplover.blogspot.com/2016/07/pc.html">効率化のためには眼球にできるだけ多くのピクセルを叩き込むべきである。</a>」 という言葉は名言だと思います。</p>計画と購入いわゆる転職エントリ、その後2019-08-16T20:00:00+00:002019-08-16T20:00:00+00:00https://msmania.github.io/2019/08/16/jobchange2<p>入社して 3 週間、在宅勤務 2 週間目が終わろうとしています。まだ小さめのバグをもらっている段階ですが、前回の記事に続いてその後の経過を少し追記しておきます。</p>
<h2 id="在宅勤務について">在宅勤務について</h2>
<p>在宅勤務、素晴らしいです。いまどきオフィスに通勤とか、ぷっ。品川でサラリーマン大行進の一員だった時代が懐かしいです。</p>
<p>まだ本格的なプロジェクトに参加しているわけではないこともあり、定期的なミーティングは月曜の朝と木曜の朝の 2 つだけです。それ以外のコミュニケーションは Slack のチャットで済ませることが多いです。ツールに関する質問や、修正のアプローチに関する質問もチャットで十分です。いまどき会議室とか、ぷぷっ。</p>
<p>Slack 以外にメールも使っています。Mozilla は G Suite を採用していて、メールは GMail、ドキュメントは Google Doc、カレンダーは Google Calendar です。Outlook、SharePoint なんて要らない子。</p>
<p>時間の縛りは全くないので、好きな時間に PC を付けて、好きな時間に休憩して、好きな時間に寝る感じです。車通勤に慣れると、東京の満員電車に戻れなくなりますが、在宅に慣れると車通勤にも戻れなくなりそうです。渋滞によるストレスや、交通事故のリスクがなくなります。大雪が降っても、電気とインターネットが止まらない限りは何の支障もでません。早朝に目が覚めたときとか、夜遅くにアイディアが浮かんだときに家に開発環境があるのもメリットですね。</p>
<p>人によっては会話する相手がいないと寂しいと感じるのかもしれませんが、私はそういうタイプではないので今のところ支障はないです。車で 20 分くらいかけてシアトルまで行けば共同オフィスがあるので、好きな時に気分転換を兼ねて行くことができます。さらに、交通費は出ないでしょうが、バンクーバーとかポートランドに突然行って作業することもできると思います。</p>
<p>もちろんデメリットもあります。ランチ、ドリンク、その他文房具などが有料。これはオフィス ワーカーの特権ですね。</p>
<h2 id="開発環境について">開発環境について</h2>
<p>入社するとラップトップが支給されます。機種は幾つかの選択肢から選べ、Macbook も選べます。私は仕事内容が Windows 向けの Firefox 開発なので、Windows にしました。機種は Thinkpad X280 です。わりと頻繁に機内に持ち込むことを考えるとやはり 12.5 インチがちょうどよいです。</p>
<p>社員用の LDAP アカウントがあり、それを使って社員用サイトほぼ全てに SSO できます。Active Diretory は要らない子。VPN も不要です。したがって、家では入社前に使っていたデスクトップ PC をそのまま開発環境として使っています。申請すればデスクトップ PC も買ってくれそうな気がしますが、今のところ必要性は感じません。ポートランドオフィスにいたときは、多くの人が大部屋でラップトップを使ってごりごりコード書いてました。個人的には開発作業にモニター 2 枚は欲しいですね。ラップトップだと作業効率が落ちます。</p>
<p>PC 本体以外についても、入社時に何が必要かを聞かれて、申請したものは支給されます。私の場合は Webcam と Headset、あと 27” モニターをもらいました。個人で持っていたものよりもグレードが高いものなので有難いです。</p>
<p>Firefox のソースコード、コードレビュー、テスト、バグ管理のシステムなどは全てオープンなので、誰でもアクセスできます。基本的にブラウザー ベースなので、特別にアプリケーションをインストールする必要もありません。私は今まで通り、秀丸でコードを書いて cdb/ntsd でデバッグしています。電話会議だけは、Google Hangout ではなく Zoom というアプリケーションをインストールして使っています。</p>
<p>開発環境のデメリットについて。</p>
<p>バージョン管理が Mercurial。Git 人間には辛いです。大体のコマンドが 1:1 で対応するように見せかけて、例えばブランチという概念がけっこう違ったりします。慣れるまで数か月かかりそうです。</p>
<p>次、Windows ライセンスが有料。まあ当たり前なんですが、Microsoft 特権に入り浸っていた身としてはかなり辛いです。Windows 高すぎ。Microsoft 社員は Visual Studio Subscription (昔の MSDN Subscription) が無料で使えるので、業務目的なら Windows や Office などほぼインストールし放題です。あと当たり前ですが、Windows チームにいると、Windows のソースコードが見れます。ただし、ライセンスや暗号化アルゴリズムに関する部分など、一部見れないところはありました。さらに、Windows だけでなく Office 製品のソースコードの閲覧権限もあります。Visual Studio のソースコードは別途申請が必要だった気がします。ソースコード以外では、社内ネットワークに接続できれば、主要な製品のプライベート シンボルにはアクセスできます。これらがいきなり使えなくなるのは大きなハンデです。辞める直前にソースコードを全部持ち出すという犯罪は魅力的でしたが、バレたときのリスクが高すぎるのでやってません。</p>
<h2 id="開発プロセスについて">開発プロセスについて</h2>
<p>まず Microsoft について少し内部事情を書いておきます。</p>
<p>Windows の Servicing プロセスは、規模やインパクトを考えると不可避な部分もありますが、やはり遅いです。ご存じの通り、基本的に修正は Windows Update というチャネルで、毎月第二火曜日のいわゆる Patch Tuesday にリリースされます。が、これは過去のモデルで、実は Patch Tuesday 以外の日にもリリースが行われています。以下のブログで、比較的詳細にリリースの仕組みが説明されています。</p>
<p>Windows monthly security and quality updates overview | Windows Experience Blog<br />
<a href="https://blogs.windows.com/windowsexperience/2018/12/10/windows-monthly-security-and-quality-updates-overview/">https://blogs.windows.com/windowsexperience/2018/12/10/windows-monthly-security-and-quality-updates-overview/</a></p>
<p>Windows では、エンジニアが変更をチェックインすると、その翌々月にリリースされる、というのが大まかなタイムラインです。さらに 2019 年 7 月時点では、正式リリース済みの最新の Windows 10 への修正は、他のプラットフォーム経由で修正をリリースして最低でも一ヶ月様子を見てからリリースするという仕組みになっています。例えば現時点で最新の Windows 10 は 1903 と呼ばれるビルドです。この場合、その他の Windows (Win7, Win8.1, Win10 1507, 1607, 1703, 1709, 1803, 1809, そして Insider Preview) への修正がまずリリースされ、早くてその翌月に Win10 1903 の修正がリリースされます。このような奇妙な仕組みは、最新の Windows 10 でのリグレッションを極力避けるためです。それ以前に Windows 10 の種類多すぎ。もちろんセキュリティの場合は、リリースを遅らせることはせず、一気にリリースします。</p>
<p>チェックインまでの大まかなプロセスは、バグが報告された後、1) 優先度やビジネスインパクトを見て、エンジニアをアサインするかどうかを決定、2) エンジニアがアサインされて、修正を作る、3) 修正のリスクや規模に基づいて、どのプラットフォームを直すか、いつリリースするかを決定 (= shiproom approval)、4) チェックイン、という流れになっています。このプロセスは、プログラム マネージャーという人たちが仕切っており、エンジニアリング チームは基本的に彼らの決めたルールに従っている状態です。</p>
<p>次に、これは Internet Explorer の場合に限られますが、チェックインのたびに自動化されたテストが開始され、基本的にはテストが全部通らないとチェックインできません。これは良い仕組みのように聞こえますが、自分でテストを取捨選択することはできず、いちいち全テストが走るので相当時間の無駄です。仮に mshtml.dll の修正であってもなぜか edgehtml.dll 向けのテストも実行されるという頭の悪いシステムになっていました。修正が望まれます。所要時間の目安としては、インクリメンタルビルドが使えない状態で Internet Explorer + Edge を同時にビルドすると大体 3-4 時間、テストが全部実行されるのに 2-3 時間かかります。これは Windows 全体ではなくブラウザーだけの話です、念のため。</p>
<p>一方 Mozilla の場合、まずプログラム マネージャー的な立場の人がいません。毎週のチーム ミーティングの中で、Bugzilla でウォッチしているエリアの新しいバグ全てに対してエンジニアをアサインして終了です。所要時間 5 分ぐらいです。</p>
<p>アサイン後のコードレビュー、チェックイン (Mozilla ではコードをメインのリポジトリに入れることを「check in the code」とは言わず、「land the code」と言います) については、ツールは違うものの、作業はそれほど変わりません。ただし IE と違って、チェックイン前のゲートキーパーみたいなのはなく、レビューが終わったらそれでチェックインの要件を満たしたことになります。</p>
<p>エンジニアの立場としては、余計なことに煩わされることがほぼない Mozilla のやり方が断然楽です。始めに書いたように Microsoft の Windows ビジネスが複雑怪奇になり過ぎているだけかもしれませんが。Windows 内部のエンジニアの不平不満の中で、エンジニア業務を妨げる不要なプロセスが多すぎる、というのは常にトップに挙げられると思います。ただ、Mozilla でももっとバグを直すにつれ、多少は悪いところも見えてくると思います。</p>
<h2 id="諸手続き">諸手続き</h2>
<p>アメリカで転職するのは初めてだったので、各手続きについて。</p>
<p><strong>退職願</strong></p>
<p>オンラインで resignation の手続きができて、そこに最終出社日、住所、有給休暇の残り日数を入力して終了です。有給はけっこう残っていて、$20,000 ぐらいに換算されました。まあ Unvested RSU が $214,850 あったことを考えると雀の涙・・・。</p>
<p><strong>401K</strong></p>
<p>管理会社が Microsoft も Mozilla も Fidelity だったので助かりました。Rollover するという手もありますが、とりあえずそのままにしています。ちなみに過去 1 年の運用益は 6% ぐらい。悪くないのでは。</p>
<p><strong>健康保険</strong></p>
<p>結局この 6 年間歯医者以外で病院には行かなかったので、アメリカの医療保険制度は未だに謎です。例えばいわゆる主治医みたいなのを設定しないといけないのですが、設定したことないです。いや、そう言えばグリーンカード申請の際の予防接種で、病院を巡ってかなり面倒な手続きをした記憶がありますが、まあそれはまた別の話。</p>
<p>健康保険も歯医者もともに保険会社が変わるので、特に歯医者に関して今までと同じところに通えるか不安だったのですが、Mozilla で扱っている保険にも対応しているみたいなので安心しました。</p>
<p><strong>スポーツジム</strong></p>
<p>Microsoft では、社員の健康のため、フィットネス費用として指定されたジムのメンバーシップ費用がタダになるプランか、もしくは年間何ドルかの健康関連の出費を経費で落とせるプランのどちらかを選べます。Mozilla でも似たような制度があり、ジムはないですが、年間 $1,700 までの健康関連費用は経費になります。これは Microsoft の上限よりかなり高いはずです。が、私はジム通いを選んで週 2 で通っていて、そのジムが一般人には法外な価格で、まさかの月額 $168.50 でした。高すぎ。$1,700 では全然足りないのでどうしたものか思案中です。</p>
<h2 id="最後に">最後に</h2>
<p>全く話の流れが変わりますが、Debugging Teams という本があります。</p>
<p>Debugging Teams: Better Productivity through Collaboration 1, Brian W. Fitzpatrick, Ben Collins-Sussman, eBook - Amazon.com</p>
<p><a href="https://www.amazon.com/Debugging-Teams-Productivity-through-Collaboration-ebook-dp-B016NDL1QE/dp/B016NDL1QE/ref=mt_kindle?_encoding=UTF8&me=&qid=1565990212">https://www.amazon.com/Debugging-Teams-Productivity-through-Collaboration-ebook-dp-B016NDL1QE/dp/B016NDL1QE/ref=mt_kindle?_encoding=UTF8&me=&qid=1565990212</a></p>
<p>日本語訳もあります。</p>
<p>Team Geek ―Googleのギークたちはいかにしてチームを作るのか | Brian W. Fitzpatrick, Ben Collins-Sussman, 及川 卓也, 角 征典 |本 | 通販 | Amazon<br />
<a href="https://www.amazon.co.jp/Team-Geek-―Googleのギークたちはいかにしてチームを作るのか-Brian-Fitzpatrick/dp/4873116309/ref=sr_1_1?__mk_ja_JP=カタカナ&keywords=Team+Geek&qid=1565994389&s=gateway&sr=8-1">https://www.amazon.co.jp/Team-Geek-―Googleのギークたちはいかにしてチームを作るのか-Brian-Fitzpatrick/dp/4873116309/ref=sr_1_1?__mk_ja_JP=カタカナ&keywords=Team+Geek&qid=1565994389&s=gateway&sr=8-1</a></p>
<p>もともとはソフトスキルを磨こうと思って買った本なのですが、今働いているチームに不満がある人にはぜひ読んでもらいたい本です。一部サンプルとして無料で読める部分があり、その中に一番印象に残った部分が含まれていたので紹介します。</p>
<p><a href="https://www.oreilly.com/library/view/debugging-teams/9781491932049/ch01.html">https://www.oreilly.com/library/view/debugging-teams/9781491932049/ch01.html</a></p>
<blockquote>
<p>Leave Time for Learning</p>
<p>Cindy was a superstar—a software engineer who had truly mastered her specialized area. She was promoted to technical lead, saw her responsibilities increase, and rose to the challenge. Before long, she was mentoring everyone around her and teaching them the ropes. She was speaking at conferences on her subject and pretty soon ended up in charge of multiple teams. She absolutely loved being the “expert” all the time. And yet, she started to get bored. Somewhere along the way she stopped learning new things. The novelty of being the wisest, most experienced expert in the room started to wear thin. Despite all of the outward signs of mastery and success, something was missing. One day she got to work and realized that her chosen field simply wasn’t so relevant anymore; people had moved on to other topics of interest. Where did she go wrong?</p>
<p>Let’s face it: it is fun to be the most knowledgeable person in the room, and mentoring others can be incredibly rewarding. The problem is that once you reach a local maximum on your team, you stop learning. And when you stop learning, you get bored. Or accidentally become obsolete. It’s really easy to get addicted to being a leading player; but only by giving up some ego will you ever change directions and get exposed to new things. Again, it’s about increasing humility and being willing to learn as much as teach. Put yourself outside your comfort zone now and then; find a fishbowl with bigger fish than you and rise to whatever challenges they hand out to you. You’ll be much happier in the long run.</p>
</blockquote>
<p>書きたいことは大体書いたので、次からはテクニカルな内容をちゃんと書きます。</p>入社して 3 週間、在宅勤務 2 週間目が終わろうとしています。まだ小さめのバグをもらっている段階ですが、前回の記事に続いてその後の経過を少し追記しておきます。いわゆる転職エントリ2019-08-04T22:12:00+00:002019-08-04T22:12:00+00:00https://msmania.github.io/2019/08/04/jobchange<h2 id="はじめに">はじめに</h2>
<p>2019 年 7 月末で、9 年間働いた Microsoft を退職しました。2010/10 から 2013/07 まで日本支社、その後 2019/07 までレドモンド本社在籍でした。7/29 からは、Mozilla Corporation で Firefox のエンジニアとして在籍しています。</p>
<p>最近は、転職に際して退職/転職エントリなるものを書くのがトレンドみたいなので、それに乗っかることにしました。自分の気持ちを整理しつつ記録に残しておきたいというのと、私自身が他の人の転職エントリを読むのが比較的好きだからというのが理由です。</p>
<p>つい最近では、理三出身の内科医の方が Google に転職した記事が話題になりました。理三 SUGEEEE、という安直な第一印象はもちろんありましたが、医者になってからも勉強に対する工夫と努力を (といってもその方の実際の努力は全く分かりませんが) 維持できている点で器が違いますね。私は高校生の時のモチベーションや馬力は明らかに維持できていません。</p>
<p>【転職エントリ】Googleに入社します|Lillian|note<br />
<a href="https://note.mu/neko_chan0214/n/n3a64bc1e1412">https://note.mu/neko_chan0214/n/n3a64bc1e1412</a></p>
<h2 id="日本マイクロソフト時代">日本マイクロソフト時代</h2>
<p>2010 年 10 月に、PFE Japan というサポート組織内のエンジニアとして中途入社しました。その中でも Platform PFE というチームで、Windows 担当になりました。当時の製品で言うと、Windows 7 がリリースされてしばらくしてからで、入社して数か月して SP1 が GA になる時期です。</p>
<p>PFE では、仕事内容、先輩方に恵まれ、技術的に一番成長できた時期でした。入社当時は、Win32 のプログラミングがちょろっとできるぐらいでしたが、ここでの 2-3 年で、OS カーネルや CPU に関する知識、アセンブリ レベルでのデバッグという、現在でも通用しているコアなスキルを身に着けることができました。</p>
<p>私が入社した頃から PFE ビジネスの方向性は少しずつ変わり、現在の PFE のミッションが当初の PFE とは大きく異なってしまっているのは残念です。</p>
<p>2 年ほど経って 2012 年の 12 月頃、本社で Hiring Manager をしている日本人の方と知り合うきっかけがあり、その方経由で US のポジションに応募して、WinSE の SDET としてオファーをもらうことができました。</p>
<h2 id="microsoft-corporation-時代">Microsoft Corporation 時代</h2>
<p>US 本社で最初に所属になったのは、Windows のバグ修正をする WinSE という部署のテスト エンジニア (SDET) で、その中でも FSC (File Server & Cluster) というチームに配属になりました。Job Level でいうと 59 で、ジュニア レベルの新卒という位置づけですが、それでも PFE のときと比べて給料は 1.5 倍ぐらいになり驚きました。</p>
<p>勤務開始は 2013 年 7 月の後半で、Windows 8.1 が GA になる直前でした。サーバーのチームなので、Windows Server 2012 R2 が OS 開発チームから WinSE に移譲されている時期で、ビジネスが大きく変わるタイミングだったことを覚えています。留学や海外在住経験が全くなかったので、当然英語で苦労したのに加えて、新 OS のサポートが始まるに伴ってビジネス プロセスにも変化が生じるので、そもそも旧ビジネスさえも知らない私は半年ぐらい苦労しました。</p>
<p>その後仕事的には FSC で大したことはしていないのですが、ここで転機となる出来事が 2 つほど起きました。</p>
<p>1 つ目は、2014 年 8-9 月頃に、Internet Explorer の Servicing ビジネスが WinSE の配下に移譲されて、さらにエンジニアの入れ替えが発生したことです。元の IE エンジニアがどうなったかはよく知りませんが、FSC にいた私を含めて何人かの SDE/SDET が無期限レンタル移籍することで新生 IE チームが結成されました。後で知った情報によると、どうやら上司の上司が推薦してくれたらしいのですが、当時詳細は全く知らされず、当日になっていきなり辞令が出た感じでした。最初は追い出し部屋的な左遷を疑ったぐらいです。ちなみにレンタル移籍だったくせに、なし崩し的に全員 IE に異動になり、私が覚えている限り元のチームに戻った人はいません。</p>
<p>FSC 時代に Internet Explorer 11 が Windows 8.1 とともにリリースされており、2015 年 7 月頃に (後に TH1 と呼ばれることになる) 最初の Windows 10 が Microsoft Edge とともにリリースされます。なお、この後 Microsoft を辞める前年の 2018 年後半に、Edge が Chromium ベースになるという Project Anaheim が発表されるので、奇しくも Edgehtml が生まれてから死亡宣告されるまでを見届けることになりました。</p>
<p>2つ目の転機は、”combined engineering” と称する、SDET/SDE を統合して Software Engineer (SWE) という職種に一本化する波が WinSE に来たことです。時期は 2015 年の 8-9 月頃でした。</p>
<p>これらの転機により、面接などを受けることなく自動的に Internet Explorer チームの Software Engineer になりました。もともと SDET として入ったのも、将来的に SDE になるための布石として (というか SDET の多くはそんな感じだったはず) なので、このチャンスは完全にラッキーなものでした。加えて担当エリアがサーバーのコンポーネントから IE になったのも嬉しい変化でした。</p>
<p>技術的には、FSC の方がカーネル モードのモジュールも含んでおり、かつ、大体のバグがマルチスレッド絡みの競合問題であるため、IE より難解だと思いますが、いかんせん閉鎖的で地味なコンポーネントです。つまり、クラスターだったらクラスター以外の技術とあまり関りがありません。その点ブラウザーである IE は、オープンな Web 標準の技術と密接な関りがあり、より「潰しが効く」製品で、キャリアとして良い変化だと思いました。また、サーバー製品よりも明らかにコンシューマーに知名度があり、ユーザーの目に触れることが多いのもモチベーションになります。</p>
<p>さらに、IE に限らずブラウザーというエリアは外部のコードを実行するという性質上、脆弱性の数が他のコンポーネントと比べて圧倒的に多く、セキュリティー関連の知識を学ぶ多くの機会を与えてくれました。アセンブリやカーネルなど、下位レイヤーの技術に興味がある私としては、セキュリティー分野をキャリアの選択肢の一つとしても考えるようになりました。IE 以前でのセキュリティの知識は、Buffer overrun とかの仕組みをちょっと知っているぐらいで、深掘りしようと考えたことがありませんでした。</p>
<p>この後辞めるまでの 6 年間、ずっと IE チームに所属することになります。</p>
<h2 id="辞めた理由">辞めた理由</h2>
<p>理由はシンプルで、Internet Explorer という製品、及びその製品を担当するチームに将来性を感じられなかったからです。</p>
<p>何人かの同僚から、製品のクォリティー、ビジネス プロセス、もしくは周りのエンジニアの技術レベルに不満があったから辞めるのだと勘違いされたのですが、これは明確に違います。</p>
<p>実際に組織に対して不満は大いにあり、常日頃から文句を言っていたのは事実ですが、辞める直接の理由ではありません。なぜなら、製品に魅力や将来性があるのであれば、組織の問題を解決しようというモチベーションは保てると考えるからです。</p>
<p>現実的にあり得ない世界線ですが、Microsoft が edgehtml の開発を諦めて Chromium を使うと決めたときに、Edge チームを解散して Edge ブランドを捨て、Internet Explorer 12 のような製品名で全く新しいブラウザーを IE チームが作る、みたいなことになるのであれば、組織に不満があっても辞めなかったと思います。</p>
<p>2017 年か 2018 年だったか定かではありませんが、Twitter 社が twitter.com を IE で開いた際、IE はサポートされないから云々、というバナーを出す事件がありました。これは IE チーム内で騒ぎになり、今後の IE の方針に関して幾つかの選択肢を検討することになったのですが、結局は何の大きな変化もないまま有耶無耶に終わってしまいました。詳細は私もよく知らされていませんが、Twitter 社は、IE は Web 標準が中途半端にしかサポートされていないことを問題としていたようです。したがって選択肢としては、今後は IE も Web 標準には完全に準拠していく、というような方針転換も現実的ではないにしろあり得たわけです。ここでの結論によっては、もう少し IE に残ろうという気持ちも芽生えたと思うのですが、そうはなりませんでした。</p>
<p>さらに追加するならば、Windows OS に対する IT 業界内、及びマイクロソフト内での立ち位置も、かつてのような黄金時代を迎えるようなイメージがあまり湧かない、という理由も挙げられます。ただしこれはメインの理由ではありません。もし NT カーネルの開発チームからオファーをもらえるならば、そっちに異動していたと思います。</p>
<p>NT カーネルじゃないにしても、Microsoft 社内でどこか IE 以外に異動するという選択肢も模索し、何人かの Hiring manager と話をしましたが、いまいちいい反応が返って来なかったので、断念しました。</p>
<h2 id="mozilla-との出会い">Mozilla との出会い</h2>
<p>Microsoft 以外の選択肢では、アクティブに行きたいところがあってその会社を受ける、というスタンスは取っていませんでした。いざ Microsoft 以外で、自信を持ってこれまでの経験を活かせそうなフィールドを見つけられていなかったことが大きな理由です。</p>
<p>LinkedIn 経由でそれなりにプロフィールを埋めておくと、毎月 1, 2 回はどこかしらのリクルーターからメッセージが来ます。それを見て面白そうなポジションに応募してみるのものの面接を通過できず、ということが 2 年間で数回ありました。</p>
<p>Mozilla も同様のチャネルで、LinkedIn 経由でリクルーターからメッセージが来ていたのがきっかけです。そもそもどこに Mozilla の拠点があるのかも知らないので、転職先として検討したこともありませんでした、</p>
<p>紹介されたのは、Firefox の Platform Security というチームで、Windows の Firefox で動くサンドボックスの実装をするエンジニアでした。今まで Windows でブラウザーをやってきていて、セキュリティーにも興味があり、直近の BlackHat でコード インジェクションのツールを発表した自分向けに作られたとしか思えない程の運命的な出会いでした。</p>
<h2 id="mozilla-の採用プロセス">Mozilla の採用プロセス</h2>
<p>面接で聞かれた具体的な質問は公表しませんが、面接のカラーというか、その傾向がユニークで Mozilla らしく、自分にはとても理想的な採用プロセスと思えたので、それが伝わるように書いておきたいと思います。</p>
<p>時系列的にまとめるとこんな感じです。</p>
<hr />
<p><strong>May-6</strong><br />
LinkedIn でメッセージ。その日のうちに返信。</p>
<p><strong>May-7</strong><br />
リクルーターと電話。</p>
<p>JD が既に具体的に書かれていて不明な内容はなかったので、自分の経歴といかにマッチしているかを伝えて、是非応募したいです、みたいなことを言ったはず。リクルーターからは、JD の Requirement をどのぐらい満たしているかの簡単な確認と、
在宅でも OK か、年に何回か All-hands で海外出張があるけど問題ないか、などを確認されてそれで終了。あと compensation の希望について聞かれたが、このときは具体的な数値は提示せずに回避。</p>
<p>そのあとのメールのやり取りで、1 回目のスクリーニング インタビューが 5/14 に設定される。(なお、全てのインタビューはオンラインで実施され、長さは 45 分。)</p>
<p>が、5/14 になぜかインタビューをすっぽかされる事態が発生して焦る。超有望な志望者が出てきてポジション埋まったんじゃないかと思い憂鬱になるが、普通に Mozilla 側のミスだったらしく、謝罪とともに再度 1 回目のスクリーニングが 5/28 に設定される。</p>
<p><strong>May-28: 1st screening</strong><br />
後に直属の上司となるマネージャーとのインタビュー。コーディングの質問どころか、技術的なことを一つも聞かれず、いわゆる Behavioral question だけで 45 分が終了。後述する準備のおかげもあって、終始いい感じの雰囲気で進められた。</p>
<p>インタビューは 9am からだったが、その日の午後にすぐに通過の連絡が来て、2 回目のスクリーニングを 6/3 に設定。</p>
<p><strong>Jun-3: 2nd screening</strong><br />
応募しているチームのエンジニアとのインタビュー。前回とは打って変わって、今度は Behavioral question は一切省かれ (tell me about yourself すらなし)、ひたすら Windows について聞かれる。実際に聞かれた質問ではないが、似たような質問を挙げるなら「VirtualAlloc と HeapAlloc の違いは」みたいな感じ。</p>
<p>45 分みっちりなのでけっこう緊張したが、たぶん想定されていたキーワードは全部抑えて答えられたのと、加えてそれを発展させるような質問をすることで建設的な議論ができたと思う。</p>
<p>前回は同日中に連絡が来たが、このときは 6/4 になっても連絡が来なくて、ここで冒険が終わってしまうのか、と意気消沈していたところ、6/5 に無事通過の連絡。3 回目のスクリーニングを 6/11 に設定。</p>
<p><strong>Jun-11: 3rd screening</strong><br />
応募しているチームのエンジニアとのインタビュー。このときも Behavioral question は一切なく、ひたすら技術的な質問に 45 分間答え続ける。コーディングもなし。このときのテーマは code injection で、BlackHat でも発表したそれなりに得意なエリアでもあるので楽しく議論しつつ終了。</p>
<p>一回目と同様、インタビューと同日の夕方には通過の連絡が来て、次が final round なのでその説明をしたい、ということで同日の 6/11 にリクルーターと電話。そこで、以下の衝撃な話を聞く。</p>
<ul>
<li>Final round は 6 回のインタビューです</li>
<li>普通はオフィスにオンサイトしてもらうのですが、今回は全部オンラインでやります</li>
<li>6/17 の週は Mozilla の All hands がウィスラーで開催され、みんなそれに参加するので、できればその前に全てのインタビューを終えたい (= 6/12-14 の 3 日しか猶予がない)</li>
</ul>
<p>このとき compensation について具体的な数値を求められ、初めて希望を提示。が、案の定 Mozilla お金なさそうな印象。さらにリクルーターから、面接で聞かれる可能性のあるテーマを幾つか教えてもらう。</p>
<p>日程調整の結果、Jun-13 に 4 つ、Jun-14 に 2 つ面接が設定された。オンサイト面接並みにきつい。</p>
<p><strong>Jun-13 9:30am-10:15am: 1st interview</strong><br />
一回目のスクリーニングの時のマネージャーと再度。ついに初めてコーディングを求められた。オンラインのホワイトボード的なのを使うやつ。けっこう簡単だったのでそつなく終了。</p>
<p><strong>Jun 13 10:15am-11:00am: 2nd interview</strong><br />
応募しているチームのエンジニアとのインタビュー。セキュリティ関連の質問だった。これは 9 回のインタビューの中で一番難しく、幾つかはしっくりこない答えになってしまった。ちょっとやばい。</p>
<p><strong>Jun 13 11:00am-11:45am: 3rd interview</strong><br />
上司の上司となる人とのインタビュー。おそらく Hiring manager。この回は再び Behavioral question。IE についても聞かれたと思う。いい感じに終わって、彼は「君のことは推薦しておくよー。残りの技術的な質問頑張ってね。」的なことを言われる。</p>
<p>面接官がどんなポジションの人か事前に教えてもらったわけではないが、このときは面接官がけっこう上のポジションの人っぽかったので LinkedIn で見てそれを確信。</p>
<p>このあと会社に行って仕事して、午後戻ってきてこの日最後の面接。</p>
<p><strong>Jun 13 5:00pm-5:45pm: 4th interview</strong><br />
応募しているチーム外のエンジニアとのインタビュー。Amazon で言うところの Bar-raiser 的なポジションかどうかは不明。単なる人数合わせの可能性が高い。この回も Behavioral question で、過去の経験について聞かれた。OSS 貢献の話題で、GDB に機能を追加したときの話をけっこうした気がする。最後にちょっとだけ簡単な技術的質問をされた。</p>
<p><strong>Jun 14 8:00am-8:45am 5th: interview</strong><br />
応募しているチームのエンジニアとのインタビュー。確かこの人はベルギー在住。二度目のコーディング。これもけっこう簡単で、楽しく会話しながら進められたと思う。</p>
<p><strong>Jun 14 9:00am-9:45am 6th: interview</strong><br />
応募しているチームのエンジニアとのインタビュー。確かこの人は UK 在住。最後のテーマは Windows におけるサンドボックスについて。主要なキーワードは答えられたが、知らないところもあって、逆にいろいろ教えてもらった。</p>
<p>というわけで、大きなミスはなく final round 終了。まあ順当に行けば受かるような気はするのだが・・・という感じ。この後彼らは 6/17-21 の期間 All-hands なので、その間連絡はなし。これは予期できたことだが、どっちつかずの状態が長くて精神衛生上よくなかった。で、ついに 6/24、リクルーターから「お話したいことがあります」的なメールが来る。</p>
<p><strong>6/24: offer call</strong><br />
リクルーターからの電話で、6 人とも recommend もしくは strongly recommend だったのでオファー出します、と連絡を受ける。compensation と start date をその電話の中で決めて、後は offer letter 待ち。</p>
<p>確か 6/27 に offer letter が DocuSign で送られてきて、6/28 にサイン、その日の 6/28 のうちに Microsoft の直属の上司に辞意を伝えるなどしてゴール。</p>
<hr />
<p>業務開始日は 7/29 に設定したので、全体のプロセスは 3 ヶ月弱。これが長すぎることはないと思いますが、スクリーニング 3 回と、本番の面接 6 回は多いと思います。そのぶんしっかり見ているということでもあるので、自分の英語力、技術力の裏付けになったという点ではかなりの自身になりました。Microsoft 本社に異動したときにも面接を受けましたが (スクリーニング x1、final round は x4。これらも全部オンライン。) 、実は Final round は自分の力だけで受かった気がせず、英語の技術面接にはかなり苦手意識がありました。それもある程度は克服できたと思います。</p>
<p>上述のように、ほとんどの面接が知識を問う内容で、コーディングが 2 回しかなかったのには驚きますが、これも好印象でした。面接の中で議論をすることで、その人の考え方がカルチャーにマッチするかどうかを重視しているのだと思います。いわゆる “think out loud” というのは実践的なスキルじゃないですし。</p>
<p>ちなみに、Mozilla の面接でググると以下のような記事が見つかり、私が経験したプロセスと大差ない気がします。ただ、Glassdoor で Mozilla の面接エピソードを見ると、わりと評価が低いです。この違いはなんなんでしょうかね。</p>
<p><strong>2014-Jan-13</strong><br />
This is my first day at Mozilla | daniel.haxx.se<br />
<a href="https://daniel.haxx.se/blog/2014/01/13/this-is-my-first-day-at-mozilla/">https://daniel.haxx.se/blog/2014/01/13/this-is-my-first-day-at-mozilla/</a></p>
<p><strong>2014-Mar</strong><br />
Interviewing with Mozilla - My Journey | #include Akshay<br />
<a href="http://akshaycode.blogspot.com/2014/03/interviewing-with-mozilla.html">http://akshaycode.blogspot.com/2014/03/interviewing-with-mozilla.html</a></p>
<p><strong>2014-Aug-03</strong><br />
My journey to Mozilla | Wilson Page<br />
<a href="http://wilsonpage.co.uk/my-journey-to-mozilla/">http://wilsonpage.co.uk/my-journey-to-mozilla/</a></p>
<p><strong>2014-Oct-12</strong><br />
Answers and Questions » Blog Archive » How I Hire at Mozilla<br />
<a href="https://benjamin.smedbergs.us/blog/2014-10-02/how-i-hire-at-mozilla/">https://benjamin.smedbergs.us/blog/2014-10-02/how-i-hire-at-mozilla/</a></p>
<p><strong>2015-Jul-06</strong><br />
Mozilla Onboarding — edunham<br />
<a href="https://edunham.net/2015/07/06/mozilla_onboarding.html">https://edunham.net/2015/07/06/mozilla_onboarding.html</a></p>
<p><strong>2016-Mar-28</strong><br />
My interview with Mozilla<br />
<a href="http://www.grigory.ca/2016/03/my-interview-with-mozilla/">http://www.grigory.ca/2016/03/my-interview-with-mozilla/</a></p>
<h2 id="面接準備">面接準備</h2>
<p>私の行なった準備が他の人の役立つかは分かりませんが、恥ずかしながら書いておきます。凡人向けです。</p>
<p>正直近道ってないと思うんですよね。王道をしっかりやるのが一番いいと思います。</p>
<h3 id="behavioral-question-対策">Behavioral question 対策</h3>
<p>最も力を入れたのはこれです。正直技術的なところは数週間でどうにかなるものではなく、Behavioral が最も即効性があると考えられるからです。あとは 6 年経っても英語は苦手で、単語がすらすら出てこないことが多々あるため、定番の質問ぐらいはスムーズに答えて第一印象を変えたいという狙いがありました。英語が得意な人、喋りが得意な人には該当しないかもしれません。</p>
<p>最も定番の質問となるのは “tell me about yourself” です。ネイティブ スピーカーの間でも苦手とする人は多いらしく、”tell me about yourself interview question” でググると多くのテクニックが出てきます。STAR method が有名な例ですが、日本で習った記憶はありません。今はどうなんでしょう。</p>
<p>準備として具体的には、複数の例文を組み合わせて自分に合ったパターンを作り、そこにエピソードを載せてスピーチの原稿を作りました。当然そのためには、自分のこれまでのエピソードを洗い出して、主要なものは英語で軽く喋れるようにしておく必要があります。エピソードを決める際には、Job Description と照らし合わせながら応募ポジションに最も刺さるエピソードから順番に使います。</p>
<p>原稿ができたら、暗記できるまで実際に音読して練習しました。このテクニックは、大学の卒業研究の時に教えてもらったもので、仕事の場面でもけっこう使えます。私の場合は、最終的に全体で 3 分ぐらいの tell me about yourself を作って覚えました。
この程度の長さであれば、前日の夜に少し練習するだけで覚えるのは簡単です。</p>
<p>“Tell me about yourself” は大体冒頭で聞かれるので、ここでスムーズに 3 分過ごすことができれば、第一印象は良くなりますし、残りの時間を生き延びる自信になります。仮に “tell me about yourself” と直接聞かれなくても、それ以外の Behavioral question でもどうせ自分が何者で今何やっているかは説明する必要が出てくるので、部分的に流用することができます。</p>
<h3 id="コーディング対策">コーディング対策</h3>
<p>結果的に Mozilla では役に立ちませんでしたが、LeetCode の Easy と Medium を中心に 30 問ぐらいはやりました。Hard は時間もかかるし、一部のポジションを除いて面接では聞かれないんじゃないかと思っていたので、汎用的なものを数問解いただけです。</p>
<p>ユニークなところでは、2 年ほど前に Clean Code という本を読んだことをきっかけとして、自分で kata programming の問題を作って 2 年間毎日やるようにしています。これに即効性は全くなく、実際に役立ったかどうかも不明ですが、今後も続けると思います。参考までに。</p>
<h3 id="ドメイン-ナレッジ対策">ドメイン ナレッジ対策</h3>
<p>Mozilla の場合、面接ごとに技術的テーマがあることが分かってきたので、JD を見て予想されるテーマは一通り復習して、簡単な実証コードも書いたりしました。例えば、COM、マルチスレッド、排他処理の種類、インジェクション手法など。面接の中で新たに覚えたことについても、次回はより深く答えられるようにもしておきました。応募する会社やポジションによっては、ドメインナレッジを聞かれることが無いこともあるはずなので、ググって傾向を調べておくことは必須だと思います。</p>
<h2 id="mozilla-に入社しての印象">Mozilla に入社しての印象</h2>
<p>シアトル エリアに Mozilla オフィスは無いため、基本は在宅勤務になりますが、入社して最初の一週間はオリエンテーションも含めてポートランド オフィスでの作業でした。といっても、同じチームの人はそこに 2 名だけです。さらに言えば人事のオリエンテーションも conf call でした。これぞ 21 世紀の働き方です。</p>
<p>Onboarding の動画を見てプロセスや Firefox について学びつつ、記念すべき最初のバグもアサインされました。</p>
<p>1568610 - Change PEHeaders::GetIATThunksForModule to return Maybe<Span<IMAGE_THUNK_DATA>><br />
<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1568610">https://bugzilla.mozilla.org/show_bug.cgi?id=1568610</a></p>
<p>やはりどうしても Microsoft や Internet Explorer と比較してしまうのですが、今のところいい印象しかないです。例えば、以下の Three Pillars や、</p>
<p>Three Pillars<br />
<a href="https://mail.mozilla.org/pipermail/firefox-dev/2015-July/003062.html">https://mail.mozilla.org/pipermail/firefox-dev/2015-July/003062.html</a></p>
<p>以下のセキュリティ プロセスにおける assume the worst など、</p>
<p>Security/Bug Approval Process - MozillaWiki<br />
<a href="https://wiki.mozilla.org/Security/Bug_Approval_Process">https://wiki.mozilla.org/Security/Bug_Approval_Process</a></p>
<p>製品を改善することに前向きな姿勢が全面に出ているのが素晴らしいと思います。</p>
<p>Onboarding の中でも、「既存のコードはベストだと思わなくていい。駄目なコードはたくさんあるから、それを見つけたらバグをファイルすればいい。」みたいなことを言っていて、とても同意できました。Internet Explorer でもそれがやりたかったんですけどね。ある程度そういう流れを作ろうともしたんですが、力不足でした。</p>
<h2 id="おわりに">おわりに</h2>
<p>Microsoft に不平不満はあるものの、2010 年、2013 年のときにはおそらく書類すら通らなかったであろう Mozilla に行けるまでに成長させてもらったという点では、とてもお世話になりました。もちろん、US で働くための雇用ビザを取得してもらい、さらに永住権をもらえたことに対しても感謝の気持ちしかありません。また月並みな言葉ですが、日本、アメリカ両方で知り合えた多くの方にお世話になり、助けられたことにも感謝です。今後も keep in touch していきたいです。今は LinkedIn や Facebook があるので便利ですね。</p>
<p>Microsoft から Mozilla への転職が、今後の人生でどういう変化をもたらすかは全く未知数です。しかし現時点で、Microsoft Japan への転職と、Microsoft Corporation への移籍が自分のスキルを大きく成長させるきっかけとなったことは確かです。</p>
<p>最後に給料に関して言えば、Base は 10-15% ぐらい下がりました。Mozilla で成績トップを取ってボーナスの上限がもらえたとしてようやく、Microsoft で最後の年にもらっていた給料と同じぐらい、というレベルです。もちろん、Microsoft で残っていた RSU は全部放棄です。Ouch.</p>
<p>逆に IE に留まればおそらく Principal には数年で昇進できる自信はありましたが、エンジニアとして local maximum に到達してデッドエンドな気がします。ここで給料を下げてでも、鶏口から牛後になれば、そこから大きな飛躍ができる可能性に賭けました。ある意味夢を追いかけたとも言います。</p>
<p>交渉すれば RSU 分の補填は多少あったんじゃないか、と何人かの人に言われたのですが、GAFA のような大企業ならともかく Mozilla じゃ希望薄だと思います。面接で得た印象は本当によく、チームでの仕事内容も楽しみにしているので、給料は、まあ、多いに越したことはないですが、生活に困らなければ文句は言いません。一応 Glassdoor で出てくる Senior Engineer の平均よりはちょっと上なので、Microsoft と比較しなければ悪くはないんじゃないでしょうか。</p>
<p>というわけで以上!</p>はじめに「作りながら学ぶ OS カーネル」本のすゝめ2019-02-18T18:18:36+00:002019-02-18T18:18:36+00:00https://msmania.github.io/2019/02/18/oskernel-practice<h1 id="作りながら学ぶ-os-カーネル本について">「作りながら学ぶ OS カーネル」本について</h1>
<p>もう 7 年ぐらい前の話になりますが、本屋で適当に技術書を眺めていたところ「作りながら学ぶ OS カーネル -保護モードプログラミングの基本と実践-」という本を見つけて衝動買いしました。</p>
<p>作りながら学ぶOSカーネル保護モードプログラミングの基本と実践 | Amazon<br />
<a href="https://www.amazon.co.jp/%E4%BD%9C%E3%82%8A%E3%81%AA%E3%81%8C%E3%82%89%E5%AD%A6%E3%81%B6OS%E3%82%AB%E3%83%BC%E3%83%8D%E3%83%AB%E4%BF%9D%E8%AD%B7%E3%83%A2%E3%83%BC%E3%83%89%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0%E3%81%AE%E5%9F%BA%E6%9C%AC%E3%81%A8%E5%AE%9F%E8%B7%B5-%E9%87%91-%E5%87%A1%E5%B3%BB/dp/4798022543">https://www.amazon.co.jp/%E4%BD%9C%E3%82%8A%E3%81%AA%E3%81%8C%E3%82%89%E5%AD%A6%E3%81%B6OS%E3%82%AB%E3%83%BC%E3%83%8D%E3%83%AB%E4%BF%9D%E8%AD%B7%E3%83%A2%E3%83%BC%E3%83%89%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0%E3%81%AE%E5%9F%BA%E6%9C%AC%E3%81%A8%E5%AE%9F%E8%B7%B5-%E9%87%91-%E5%87%A1%E5%B3%BB/dp/4798022543</a></p>
<p>タイトルにある通り、Intel x86 の保護モードで動くシンプルな OS カーネルをアセンブリで書いていく内容です。内容は素晴らしいのですが、致命的な欠点があります。以下のブログや Amazon のレビューでも悉く指摘されていますが、誤植の量が半端ないのです。誤植と言っても、本文の記述が間違っているだけでなく、肝心なカーネルのコードも間違っていて、そのままコピペしても動作しないレベルの間違いが複数ヶ所あります。</p>
<p>読書メモ/「作りながら学ぶOSカーネル - 保護モードプログラミングの基本と実践」 - Glamenv-Septzen.net<br />
<a href="https://www.glamenv-septzen.net/view/1342">https://www.glamenv-septzen.net/view/1342</a></p>
<p>今読んでる本 - 『作りながら学ぶOSカーネル』<br />
<a href="http://qune.jp/archive/001252/index.html">http://qune.jp/archive/001252/index.html</a></p>
<p>無差別に技術をついばむ鳥 書籍をつつく102-作りながら学ぶOSカーネル。題名どおりの名著♪<br />
<a href="http://indori.blog32.fc2.com/blog-entry-748.html">http://indori.blog32.fc2.com/blog-entry-748.html</a></p>
<p>本を買わずとも、以下のページからコードを無料でダウンロードすることができます。が、誤植を加味しても本を買う価値はあると思います。</p>
<p>作りながら学ぶOSカーネル 保護モードプログラミングの基本と実践|サポート|秀和システム<br />
<a href="https://www.shuwasystem.co.jp/support/7980html/2254.html">https://www.shuwasystem.co.jp/support/7980html/2254.html</a></p>
<p>著者が日本の方ではないので日本語と英語が微妙に拙いというのもありますが、これは許容範囲です。むしろ我々日本人が英語で書いた論文や記事に対して、英語のネイティブ スピーカーが抱くであろう感情を想起してくれる貴重な資料です。そもそも韓国語で書かれていたら全く読めないので、日本語で出版されたことはとても有難いですね。</p>
<p>ところで、上記ブログでも指摘があるように、本書と似たような趣旨の本に「30日でできる! OS自作入門」という本があります。この本は<a href="https://turingcomplete.fm/">某ポッドキャスト</a>でも話題になりました。残念ながらこちらは読んだことがありません。Kindle 本になっているのですぐ買えるんですけど。高いし。</p>
<p>30日でできる! OS自作入門 | 川合 秀実 | 工学 | Kindleストア | Amazon<br />
<a href="https://www.amazon.co.jp/dp/B00IR1HYI0/ref=dp-kindle-redirect?_encoding=UTF8&btkr=1">https://www.amazon.co.jp/dp/B00IR1HYI0/ref=dp-kindle-redirect?_encoding=UTF8&btkr=1</a></p>
<p>なぜ数年ぶりにブログを更新するかといえば、最近ようやくこの本のコードを全て動かすことに成功したからです。この記事の主旨は本とコードの宣伝です。玄人志向ではありますが、カーネル開発のトラブルシューティングを余儀なくされるという点でとても画期的な本なので、初心者向きではないですが、ある程度アセンブリが分かってデバッグが好きな人にはとても刺さる内容だと思います。Amazon のレビューでは ★ 4 つにしました。</p>
<p>msmania/oskernel-practice: 作りながら学ぶ OS カーネル<br />
<a href="https://github.com/msmania/oskernel-practice">https://github.com/msmania/oskernel-practice</a></p>
<h2 id="動作環境">動作環境</h2>
<p>本書で作るカーネルは、Windows 上にインストールした Bochs という IA-32 エミュレーターで動かすことが想定されています。この構成は宗教的な理由であまり好きになれなかったので、最初は Windows で Bochs の代わりに Hyper-V を使って動かしていました。具体的には、フロッピーの代わりに仮想ハードディスクとなる VHD ファイルの MBR を書き換え、そこからカーネルが起動するようにしていました。</p>
<p>VHD には、Windows で動く <a href="http://www.chrysocome.net/dd">dd.exe</a> を使ってセクタ単位の Direct I/O で書き込んでいたのですが、一度ディスク構成が変わってディスク番号がずれ、別の外付け HDD のデータを飛ばすというありがちなミスをしました。そんなリスクはありますが、Windows + Hyper-V の力技で 8 章までは行けます。しかし最後の 9 章で躓きます。</p>
<p>9 章では、カーネルの一部を C 言語で書くことになり、コンパイラーとして GCC を使うことが想定されています。これも宗教的な理由ですが、Windows に GCC を入れるのは好きになれません。かといって Visual C++ のコンパイラー/リンカーの仕様には制限があり、出力形式は PE イメージに限定されています。OS カーネルを書くにはフラットな Raw イメージが必要なため、Visual C++ は使えません。というわけで、9 章のために Linux で GCC を使う手法に変えました。エミュレーターは本の通りに Bochs を使ってもいいのですが、QEMU の勉強も兼ねて QEMU を使うことにしました。QEMU についても某ポッドキャストで出てきましたね。好きな回です。</p>
<p>上記をふまえ、動作環境は以下の通りです。</p>
<ul>
<li>OS: Ubuntu 18.04.2 LTS</li>
<li>Emulator: QEMU emulator version 2.11.1(Debian 1:2.11+dfsg-1ubuntu7.9)</li>
<li>C: gcc (Ubuntu 7.3.0-27ubuntu1~18.04) 7.3.0</li>
<li>Assembler: NASM version 2.13.02</li>
</ul>
<h2 id="9-章c-言語でカーネルを作るのコードについて">9 章「C 言語でカーネルを作る」のコードについて</h2>
<p>一番苦労したのは、やはり最後の 9 章です。8 章までの間違いは printf デバッグみたいな原始的な手法で見つけて直せますが、9 章はデバッガーを使ってまともにデバッグしないとたぶん無理です。そういう意味で本書は、カーネルデバッグの実践機会まで提供してくれるのです。大半の人は諦めてしまいそうですが。</p>
<p>9 章の内容に関する問題は以下の通りです。本に書かれているのと全く同じバージョンの環境を用意すれば、幾つかは問題ないかもしれません。</p>
<ul>
<li>本に書かれているコマンドではカーネル イメージが正しくコンパイルできない</li>
<li>本に書かれているコマンドではカーネル イメージが正しくリンクできない</li>
<li>本に書かれたアセンブリのコードでは C 言語の関数が呼べない</li>
<li>カーネル イメージのセクター数が合わない</li>
</ul>
<p>本書ではコマンドを普通に手打ちする流れですが、一発で make できるように Makefile を書きました。詳細はコードを見てもらうとして、実際に make run するとこんな風になります。</p>
<p><img src="/assets/2019-02-18-chap9.png" alt="" /> <br /></p>
<p>一応コードを動かすことには成功していますが、仮想フロッピーの特定のセクターからデータを読み込めないという謎の現象に遭遇したため、データのオフセットを適当に弄ってそのセクターを回避するという手段を取っています。これはいつか QEMU そのものをデバッグして原因を究明したいところです。</p>
<p>もう一点、フロッピーのデータを DMA で読み込むときに I/O ポート 0x08 に 0x14 と 0x10 を送信するのですが、QEMU では無効なコマンドらしくコンソールに以下のログが出力されます。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>dma: command 14 not supported
dma: command 10 not supported
</code></pre></div></div>
<p>本書では、これは DMA を無効/有効化するスイッチというように書かれていますが、OSDev wiki によるとポート 0x08 は Status Register で、読み取り専用になっています。この本の謎は尽きません。</p>
<p>ISA DMA - OSDev Wiki<br />
<a href="https://wiki.osdev.org/ISA_DMA">https://wiki.osdev.org/ISA_DMA</a></p>「作りながら学ぶ OS カーネル」本について もう 7 年ぐらい前の話になりますが、本屋で適当に技術書を眺めていたところ「作りながら学ぶ OS カーネル -保護モードプログラミングの基本と実践-」という本を見つけて衝動買いしました。 作りながら学ぶOSカーネル保護モードプログラミングの基本と実践 | Amazon https://www.amazon.co.jp/%E4%BD%9C%E3%82%8A%E3%81%AA%E3%81%8C%E3%82%89%E5%AD%A6%E3%81%B6OS%E3%82%AB%E3%83%BC%E3%83%8D%E3%83%AB%E4%BF%9D%E8%AD%B7%E3%83%A2%E3%83%BC%E3%83%89%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0%E3%81%AE%E5%9F%BA%E6%9C%AC%E3%81%A8%E5%AE%9F%E8%B7%B5-%E9%87%91-%E5%87%A1%E5%B3%BB/dp/4798022543 タイトルにある通り、Intel x86 の保護モードで動くシンプルな OS カーネルをアセンブリで書いていく内容です。内容は素晴らしいのですが、致命的な欠点があります。以下のブログや Amazon のレビューでも悉く指摘されていますが、誤植の量が半端ないのです。誤植と言っても、本文の記述が間違っているだけでなく、肝心なカーネルのコードも間違っていて、そのままコピペしても動作しないレベルの間違いが複数ヶ所あります。 読書メモ/「作りながら学ぶOSカーネル - 保護モードプログラミングの基本と実践」 - Glamenv-Septzen.net https://www.glamenv-septzen.net/view/1342 今読んでる本 - 『作りながら学ぶOSカーネル』 http://qune.jp/archive/001252/index.html 無差別に技術をついばむ鳥 書籍をつつく102-作りながら学ぶOSカーネル。題名どおりの名著♪ http://indori.blog32.fc2.com/blog-entry-748.html 本を買わずとも、以下のページからコードを無料でダウンロードすることができます。が、誤植を加味しても本を買う価値はあると思います。 作りながら学ぶOSカーネル 保護モードプログラミングの基本と実践|サポート|秀和システム https://www.shuwasystem.co.jp/support/7980html/2254.html 著者が日本の方ではないので日本語と英語が微妙に拙いというのもありますが、これは許容範囲です。むしろ我々日本人が英語で書いた論文や記事に対して、英語のネイティブ スピーカーが抱くであろう感情を想起してくれる貴重な資料です。そもそも韓国語で書かれていたら全く読めないので、日本語で出版されたことはとても有難いですね。 ところで、上記ブログでも指摘があるように、本書と似たような趣旨の本に「30日でできる! OS自作入門」という本があります。この本は某ポッドキャストでも話題になりました。残念ながらこちらは読んだことがありません。Kindle 本になっているのですぐ買えるんですけど。高いし。 30日でできる! OS自作入門 | 川合 秀実 | 工学 | Kindleストア | Amazon https://www.amazon.co.jp/dp/B00IR1HYI0/ref=dp-kindle-redirect?_encoding=UTF8&btkr=1 なぜ数年ぶりにブログを更新するかといえば、最近ようやくこの本のコードを全て動かすことに成功したからです。この記事の主旨は本とコードの宣伝です。玄人志向ではありますが、カーネル開発のトラブルシューティングを余儀なくされるという点でとても画期的な本なので、初心者向きではないですが、ある程度アセンブリが分かってデバッグが好きな人にはとても刺さる内容だと思います。Amazon のレビューでは ★ 4 つにしました。 msmania/oskernel-practice: 作りながら学ぶ OS カーネル https://github.com/msmania/oskernel-practice 動作環境 本書で作るカーネルは、Windows 上にインストールした Bochs という IA-32 エミュレーターで動かすことが想定されています。この構成は宗教的な理由であまり好きになれなかったので、最初は Windows で Bochs の代わりに Hyper-V を使って動かしていました。具体的には、フロッピーの代わりに仮想ハードディスクとなる VHD ファイルの MBR を書き換え、そこからカーネルが起動するようにしていました。 VHD には、Windows で動く dd.exe を使ってセクタ単位の Direct I/O で書き込んでいたのですが、一度ディスク構成が変わってディスク番号がずれ、別の外付け HDD のデータを飛ばすというありがちなミスをしました。そんなリスクはありますが、Windows + Hyper-V の力技で 8 章までは行けます。しかし最後の 9 章で躓きます。 9 章では、カーネルの一部を C 言語で書くことになり、コンパイラーとして GCC を使うことが想定されています。これも宗教的な理由ですが、Windows に GCC を入れるのは好きになれません。かといって Visual C++ のコンパイラー/リンカーの仕様には制限があり、出力形式は PE イメージに限定されています。OS カーネルを書くにはフラットな Raw イメージが必要なため、Visual C++ は使えません。というわけで、9 章のために Linux で GCC を使う手法に変えました。エミュレーターは本の通りに Bochs を使ってもいいのですが、QEMU の勉強も兼ねて QEMU を使うことにしました。QEMU についても某ポッドキャストで出てきましたね。好きな回です。 上記をふまえ、動作環境は以下の通りです。 OS: Ubuntu 18.04.2 LTS Emulator: QEMU emulator version 2.11.1(Debian 1:2.11+dfsg-1ubuntu7.9) C: gcc (Ubuntu 7.3.0-27ubuntu1~18.04) 7.3.0 Assembler: NASM version 2.13.02 9 章「C 言語でカーネルを作る」のコードについて 一番苦労したのは、やはり最後の 9 章です。8 章までの間違いは printf デバッグみたいな原始的な手法で見つけて直せますが、9 章はデバッガーを使ってまともにデバッグしないとたぶん無理です。そういう意味で本書は、カーネルデバッグの実践機会まで提供してくれるのです。大半の人は諦めてしまいそうですが。 9 章の内容に関する問題は以下の通りです。本に書かれているのと全く同じバージョンの環境を用意すれば、幾つかは問題ないかもしれません。 本に書かれているコマンドではカーネル イメージが正しくコンパイルできない 本に書かれているコマンドではカーネル イメージが正しくリンクできない 本に書かれたアセンブリのコードでは C 言語の関数が呼べない カーネル イメージのセクター数が合わない 本書ではコマンドを普通に手打ちする流れですが、一発で make できるように Makefile を書きました。詳細はコードを見てもらうとして、実際に make run するとこんな風になります。 一応コードを動かすことには成功していますが、仮想フロッピーの特定のセクターからデータを読み込めないという謎の現象に遭遇したため、データのオフセットを適当に弄ってそのセクターを回避するという手段を取っています。これはいつか QEMU そのものをデバッグして原因を究明したいところです。 もう一点、フロッピーのデータを DMA で読み込むときに I/O ポート 0x08 に 0x14 と 0x10 を送信するのですが、QEMU では無効なコマンドらしくコンソールに以下のログが出力されます。 dma: command 14 not supported dma: command 10 not supported 本書では、これは DMA を無効/有効化するスイッチというように書かれていますが、OSDev wiki によるとポート 0x08 は Status Register で、読み取り専用になっています。この本の謎は尽きません。 ISA DMA - OSDev Wiki https://wiki.osdev.org/ISA_DMAWindows could not update the computer’s boot configuration2016-10-23T23:28:36+00:002016-10-23T23:28:36+00:00https://msmania.github.io/2016/10/23/windows-could-not-update-the-computers-boot-configuration<p>Windows Server 2016 が GA になったので、週末を利用して、仕事で使っているマシン 2 台の OS を Windows Server 2016 TP5 から RTM に入れ替えたのですが、インストーラーの怪しげな動作でハマったので書いておきます。強引にインストールは終わらせましたが、完全に直っていない上に根本原因が確認できていないので、時間を見つけて調べて追記する予定です。</p>
<font color="#0000ff">(2016/10/30 追記</font>
<p>) <br /></p>
<font color="#0000ff">最終確認はまだですが、NVRAM へのアクセスが失敗していることが原因っぽいです。</font>
<font color="#0000ff">(詳細は後述)</font>
<p>インストールを行った二台は HP Z420 Workstation と HP Z440 Workstation。前者がメインの開発機で、コードを書いてビルドする以外に、デバッグ用の Hyper-V 仮想マシンを動かしたり、メールを書いたりもします。後者はサブの開発機で、ほぼビルドと仮想マシンのみ。なお Windows 10 ではなく Server 2016 を入れるのは、Universal App などの煩わしい機能が嫌いだからです。サーバー SKU おすすめです。</p>
<p>家で使うマシンも仕事のマシンも同じですが、OS の入れ替えでは常にクリーン インストールを行います。インプレースアップグレードはいまいち動作が信用できないのです。</p>
<p>作業は、サブ機の Z440 から行いました。これには光学ドライブがないので、USB ドライブで Windows PE を起動して、別の USB ドライブに入れておいた Windows Server 2016 のインストーラー (MSDN からダウンロードした ISO を単にコピーしたもの) である setup.exe を起動して行います。前の OS が入っていたパーティションを消して、新しくパーティションを作って入れるだけです。作業としては普通です。</p>
<p>Z440 の作業は何の問題もなかったのですが、同じ USB ドライブを使って Z420 に Server 2016 をインストールしたところ、インストールの最後のフェーズで “Windows could not update the computer’s boot configuration. Installation cannot proceed.” が出ました。</p>
<p><img src="/assets/2016-10-23-01-error.png" alt="" /> <br />
じゃじゃーん</p>
<p>これけっこう深刻なエラー・・・。メッセージの内容を信用するならば、インストーラーの wim イメージをボリュームにコピーした後、ブート情報を書き換えるところが失敗しているわけす。当然マシンは起動しなくなります。Windows RE も起動できません。ちなみにインストールが失敗したディスクからシステムを起動すると “Your PC/Device needs to be repaired - The Boot Configuration Data file is missing some required information. File: \BCD Error code: 0xc0000034” 画面が出ました。BCD 情報を書けなかったのはお前んところのインストーラーなんだけどな!</p>
<p><img src="/assets/2016-10-23-02-bcd.png" alt="" /> <br />
起動失敗の図</p>
<p>慌てず騒がず、とりあえず以下の作業を順番に試してみました。</p>
<ol>
<li>まったく同じ作業 (同じ USB ドライブを使って、同じハードディスク (以降ディスク A) をインストール先として指定) を試す → 現象変わらず</li>
<li>別のハードディスク (以降ディスク B) を使ってみる → 現象変わらず</li>
<li>ディスク B を diskpart の clear コマンドでクリアしてからインストール → 現象変わらず</li>
</ol>
<p>おいおい・・。一台目の Z440 で上手くいっていることからして、USB ドライブに問題はないはず。ハードディスク側にも問題があるとは思えない。ぱっと思いつくのは、BIOS/UEFI の起動方式とディスク形式の MBR/GPT。ディスクをクリアしても同じ現象が出る時点でインストーラーのバグくさいが、だとすると Z440 でうまくいった説明がつかない。Z420 も Z440 も UEFI のはずなのに。</p>
<p>しばらく悩んだ結果、Z420 と Z440 とで、パーティション構成に唯一の違いがあることに気づきました。Z440 のインストール先ディスクは 150GB なので、まるまる OS 用のパーティションを入れています。一方、Z420 のインストール先ディスク A は 1TB なので、後ろ半分の 500GB はデータ用にして、先頭の 500GB を OS 用にしていました。今回の作業では、前の OS のパーティションだけを消して、インストーラーから新しくパーティションを作り直していました。また、上記作業 2. と 3. の作業でも、インストーラー経由でパーティションを作ってからインストールをしていました。そこで、新しいディスク C を用意して、パーティションを作らず、ディスク全体を指定して OS をインストールしたところ・・・成功。謎は深まるばかり。</p>
<p>そしてすぐに次の問題が発生。インストール後に再起動がかかっても、またも “Your PC/Device needs to be repaired” エラーで Windows 起動しない・・・。インストール成功したんじゃなかったのかよ。</p>
<p>試行錯誤の末、Boot menu を開いてインストール先のディスクを明示的に選択すれば起動することが判明。これで何が起こっているのかは分かった、気がする。</p>
<p><img src="/assets/2016-10-23-03-boot.png" alt="" /></p>
<p>Z420 のブート メニュー <br />
(Legacy Boot Sources の WDC WD2500AA.. を選択すれば起動できた)</p>
<p>起動がうまくいったあと、以下の情報を確認しました。</p>
<ul>
<li>ディスク A は GPT 形式になっていて、EFI System Partition は作成されている <br />
→ インストーラーの最後で BCD 情報の書き換えに失敗した理由が不明</li>
<li>ディスク B は MBR 形式になっている</li>
<li>Z420 の msinfo32 を見ると、BIOS Mode は Legacy</li>
<li>Z440 の起動ディスクは GPT 形式</li>
<li>Z440 の msinfo32 で、BIOS Mode は UEFI</li>
</ul>
<p>上記を整理すると・・</p>
<ol>
<li>Windows インストーラー、及び OS 本体は Z420 を Legacy BIOS だと思っているので、MBR 形式のディスクを対象にしたインストールを試みる。</li>
<li>しかし、インストーラーの中のパーティションを作る部分では、システムを UEFI と認識しているためか、MBR ではなく GPT 形式でパーティションを作る。</li>
<li>Z420 のブートの順番は、UEFI を試してから各ディスクの MBR を使って起動しようとする。このとき、UEFI の Windows Boot Manager がなぜか中途半端に実行できてしまうため、MBR の実行を試そうとしない。</li>
<li>システムは MBR 形式でインストールされているので当然起動できない。</li>
</ol>
<p>不可解なのは以下の点。</p>
<ul>
<li>そもそも最初に試したときに、BCD 情報が更新できなかった理由 <br />
→ 諸悪の根源。これが上手くいっていれば問題はここまで拗れない。</li>
<li>インストーラーでパーティションを作るとディスクが GPT になるくせに、実際にシステムをインストールするときは MBR でディスクを切っている。インストーラーがシステムの BIOS モードをチェックするコードが少なくとも二ヶ所あって、一方は Legacy BIOS、他方は UEFI だと認識してしまうっぽい・・?</li>
</ul>
<p>確か似たような現象を調べたときに、Windows のインストーラーにはディスクをクリアしてディスク形式を変更する機能がないので、インストーラーは Legacy BIOS のシステムでは GPT 形式のディスクを認識せず、逆に UEFI のシステムでは MBR のディスクを認識しなかった記憶があります。この場合、diskpart などを使ってあらかじめディスク形式を変えておく必要があります。</p>
<p>今回の場合は不可解で、GPT と MBR の扱いがかなり混在してしまっている印象がです。そもそもインストーラーやシステムがシステムを Legacy BIOS として認識しているのであれば、GPT で切れたディスクは認識できるべきではないし、インストールが始まる前にエラーになって欲しいものです。イメージのコピーはうまくいって、最後にこけるのは一番タチが悪い。これが Server 2016 のメディアで新しく発生するのか、もっと前の OS のディスクから発生するのかどうかは確かめていません。</p>
<p>今のところ、マシンを再起動するたびに boot menu を起動してディスクを選択しないといけない。超不便。</p>
<font color="#0000ff">(2016/10/30 追記</font>
<p>)</p>
<p>まだ最終確認は取れていませんが、どうやら NVRAM への読み書きができないハードウェア障害のような気がしてきました。調べた内容を以下に記します。</p>
<p>まず、Windows インストール時のログはインストール先ディスクの $WINDOWS.~BT\Sources\Panther\setupact.log に残っているので、失敗している箇所を確認。</p>
<p><img src="/assets/2016-10-23-log.png" alt="" /></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>2016-10-22 16:46:58, Info [0x060228] IBS Callback_UpdateBootFiles:Successfully updated Windows boot files.
2016-10-22 16:46:58, Info IBSLIB ModifyBootEntriesLegacy: Not in first boot. No actions to perform. SetupPhase[2]
2016-10-22 16:46:58, Info IBSLIB ModifyBootEntriesBCD:Setup phase is [2]
2016-10-22 16:46:58, Info IBSLIB BfsInitializeBcdStore flags(0x00000008) RetainElementData:n DelExistinObject:n
2016-10-22 16:46:58, Info IBSLIB VolumePathName for H:\Windows is H:\
2016-10-22 16:46:58, Info IBSLIB Opening template from \Device\HarddiskVolume7\Windows\System32\config\BCD-Template.
2016-10-22 16:46:58, Info IBSLIB System BCD store does not exist, creating.
2016-10-22 16:46:58, Error [0x064230] IBSLIB Failed to create a new system store. Status = [c0000001]
2016-10-22 16:46:58, Error [0x0641b8] IBSLIB ModifyBootEntries: Error modifying bcd boot entries. dwRetCode=[0x1F][gle=0x0000001f]
2016-10-22 16:46:58, Info [0x060216] IBS CallBack_MungeBootEntries:Failed to modify boot entries; GLE = 31
2016-10-22 16:46:58, Info [0x0640ae] IBSLIB PublishMessage: Publishing message [Windows could not update the computer's boot configuration. Installation cannot proceed.]
2016-10-22 16:46:58, Info [0x0a013d] UI Accepting Cancel. Exiting Page Progress.
</code></pre></div></div>
<p>やはり BCD の新規作成に失敗している模様。</p>
<p>とりあえず BIOS の設定 (Advanced –> Device Options) を確認。もともとはこんな設定だった。後で触れるがこの時点でちょっと変。</p>
<p><img src="/assets/2016-10-23-bios01.png" alt="" /></p>
<p>とりあえず Option ROM を Legacy から EFI に変えて試してみる。</p>
<p><img src="/assets/2016-10-23-bios021.png" alt="" /></p>
<p>なお、ここで Video Options ROMS を EFI に変えてはいけない。間違って変えてしまうと、ビープ音が 6 回鳴ってシステムが起動しない悲しい状態になります。</p>
<p>Advisory: HP Z1, Z220, Z420, Z620, Z820 Workstation - 6 Beeps After Changing BIOS Settings <br />
<a href="http://h20564.www2.hp.com/hpsc/doc/public/display?docId=emr_na-c04045903">http://h20564.www2.hp.com/hpsc/doc/public/display?docId=emr_na-c04045903</a></p>
<p>お決まりだが、見事にこの罠を踏んでしまったのだ。上記 Resolution にあるように、CMOS リセットを行って無事復活。PXE Option ROMS と Mass Storage Option ROMS だけを EFI にして再起動。そして同様にインストーラーを動かすが、状況は変わらず、結局 “Windows could not update the computer’s boot configuration. Installation cannot proceed.” で失敗する。</p>
<p>埒が明かなくなってきたので、真面目に setup.exe をデバッグしてみることに。最近の Windows PE では NIC の標準ドライバーが入っているので TCP/IP ネットワークで簡単にユーザーモードのリモート デバッグができます。Windows PE 起動後に以下のコマンドを実行すると、ネットワークが有効になるので dbgsrv.exe を起動できます。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>> wpeutil initializenetwork
> wpeutil disablefirewall
</code></pre></div></div>
<p>詳細は省きますが、問題となっているインストーラーの最終フェーズでは、まず空の BCD を作ってから、インストール イメージ内にある windows\system32\config\BCD-Template と NVRAM の状態を元に BCD にデータを入れていくようなことをやっているようです。空の BCD を作るところは問題なく成功して、NVRAM の内容を取ってくると思われるシステム コールから c0000001 が返ってきていました。カーネル デバッグまではやっていないので、カーネルの中で何が失敗しているのかはまだ不明のままです。</p>
<p>NVRAM がおかしいとすると、Option ROM が Legacy 設定だったにも関わらず “Your PC/Device needs to be repaired - The Boot Configuration Data file is missing some required information. File: \BCD Error code: 0xc0000034” が出る理由も分かるような気もします。NVRAM の内容が Server 2016 を入れる前の状態のまま変わっていない可能性が高く、本来であれば Option ROM を Legacy に変えたら NVRAM はクリアされて、MBR からの起動を自動的に試すのではないだろうか。</p>
<p>デバッガーを使って、NVRAM にアクセスしてエラー コードを返している箇所 (4 箇所あった) で、片っ端からエラーコードを変更してエラーがなかったように見せかける禁断の領域に踏み込んだところ、インストールは終わりましたが、初回起動で OOBE が始まる前の段階で “Windows could not complete the installation. To install Windows on this computer, restart the installation.” というポップアップが出て結局起動できず。どうやら Windows 起動時にも NVRAM へのアクセスを行なっているようだ。まあそりゃそうだろう。</p>
<p><img src="/assets/2016-10-23-image.png" alt="" /></p>
<p>Z420 で NVRAM をクリアする方法を探してみたが、どうにも見つからない。意図せずして行なった CMOS クリアではクリアされなかった。システム設定で ROM をクリアするオプションはあるのだが、何が消えるか分からないためちょっと恐くて試していない。びびり。</p>
<font color="#0000ff">(2016/11/6 追記)</font>
<p>次は、何とかして NVRAM がおかしいという確証が欲しいところです。以下の資料を見ると、実は bcdedit /enum で出力される値は NVRAM variable から来ているらしい。</p>
<p>Presentations and Videos | Unified Extensible Firmware Interface Forum <br />
<a href="http://www.uefi.org/learning_center/presentationsandvideos">http://www.uefi.org/learning_center/presentationsandvideos</a></p>
<p>上記ページからダウンロードできる “Windows Boot Environment” という PDF の 20 ページ目によると</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>- BCD has 1:1 mappings for some UEFI global variables
- Any time {fwbootmgr} is manipulated, NVRAM is automatically updated
</code></pre></div></div>
<p>さらに、以下のページによると</p>
<p>Remove Duplicate Firmware Objects in BCD and NVRAM <br />
<a href="https://technet.microsoft.com/en-us/library/cc749510(v=ws.10).aspx">https://technet.microsoft.com/en-us/library/cc749510(v=ws.10).aspx</a></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>When bcdedit opens the BCD, it compares entries in NVRAM with entries in BCD. Entries in NVRAM that were created by the firmware that do not exist in BCD are added to the system BCD. When bcdedit closes the system BCD, any boot manager entries in BCD that are not in NVRAM are added to NVRAM.
</code></pre></div></div>
<p>同様のことは以下の日本語のブログにもまとめられています。詳しくていい感じ。</p>
<p>PC-UEFI - DXR165の備忘録 <br />
<a href="http://dxr165.blog.fc2.com/blog-category-45.html">http://dxr165.blog.fc2.com/blog-category-45.html</a></p>
<p>何はともあれ、Windows PE を起動して bcdedit を実行してみます。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>X:\windows\system32> bcdedit /enum {fwbootmgr}
The boot configuration data store could not be opened.
A device attached to the system is not functioning.
</code></pre></div></div>
<p>あっさり失敗。インストーラーを起動するまでもないですね。Server 2016 のメディアが悪いのではなく、やはりハードウェアがおかしいくさい。</p>
<p>さて、次のステップはいよいよ Windows PE のカーネル デバッグだろうか。幸い、Z420 と Z440 を 1394 ケーブルで繋げられたので、これでデバッグすることにする。最近だとイーサネットも使えるらしいが、試したことはない。</p>
<p>上記の bcdedit /enum {fwbootmgr} を実行すると、NT カーネルから HAL を経由し、EFI のランタイム サービス テーブルにおける GetNextVariableName が指すアドレスの関数を呼んで変数名を列挙し、GetVariable で値を取ってきます。ランタイム サービス テーブルの定義は、↓ のファイルにおける EFI_RUNTIME_SERVICES という構造体です。</p>
<p>TianoCore EDK2: MdePkg/Include/Uefi/UefiSpec.h Source File <br />
<a href="http://www.bluestop.org/edk2/docs/trunk/_uefi_spec_8h_source.html">http://www.bluestop.org/edk2/docs/trunk/_uefi_spec_8h_source.html</a></p>
<p>ランタイム サービス テーブルは、EFI システム テーブルの一部であり、EFI システム テーブルは efi_main 関数が取る 2 つのパラメーターのうちの一つらしい。</p>
<p>Programming for EFI: Using EFI Services <br />
<a href="http://www.rodsbooks.com/efi-programming/efi_services.html">http://www.rodsbooks.com/efi-programming/efi_services.html</a></p>
<p>GetNextVariableName のプロトタイプ宣言は分かっているので、呼び出し部分から変数名を GUID を確認しました。102 個ありますが、とりあえず全部載せておきます。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ffffd000`208578a0 "CurrentDevicePath"
ffffd000`20857868 8be4df61 11d293ca e0000daa 8c2b0398
ffffd000`208578a0 "BootCurrent"
ffffd000`20857868 8be4df61 11d293ca e0000daa 8c2b0398
ffffd000`208578a0 "LangCodes"
ffffd000`20857868 8be4df61 11d293ca e0000daa 8c2b0398
ffffd000`208578a0 "PlatformLangCodes"
ffffd000`20857868 8be4df61 11d293ca e0000daa 8c2b0398
ffffd000`208578a0 "SSID"
ffffd000`20857868 707c9176 4e27a4c1 371c1d85 c873cab7
ffffd000`208578a0 "UsbMassDevNum"
ffffd000`20857868 ec87d643 4bb5eba4 3e3fe5a1 a90db236
ffffd000`208578a0 "SetupFeatureSupport"
ffffd000`20857868 b6ad93e3 4c8519f7 c58072aa c7db9471
ffffd000`208578a0 "ErrOut"
ffffd000`20857868 8be4df61 11d293ca e0000daa 8c2b0398
ffffd000`208578a0 "ErrOutDev"
ffffd000`20857868 8be4df61 11d293ca e0000daa 8c2b0398
ffffd000`208578a0 "RstSataV"
ffffd000`20857868 193dfefa 4302a445 3aefd899 c6041aad
ffffd000`208578a0 "PNP0510_0_VV"
ffffd000`20857868 560bf58a 4d7e1e0d 80293f95 31e061a2
ffffd000`208578a0 "RstScuV"
ffffd000`20857868 193dfefa 4302a445 3aefd899 c6041aad
ffffd000`208578a0 "BootOptionSupport"
ffffd000`20857868 8be4df61 11d293ca e0000daa 8c2b0398
ffffd000`208578a0 "ConInDev"
ffffd000`20857868 8be4df61 11d293ca e0000daa 8c2b0398
ffffd000`208578a0 "UsbMassDevValid"
ffffd000`20857868 ec87d643 4bb5eba4 3e3fe5a1 a90db236
ffffd000`208578a0 "PNP0501_1_VV"
ffffd000`20857868 560bf58a 4d7e1e0d 80293f95 31e061a2
ffffd000`208578a0 "PNP0400_0_VV"
ffffd000`20857868 560bf58a 4d7e1e0d 80293f95 31e061a2
ffffd000`208578a0 "PNP0501_0_VV"
ffffd000`20857868 560bf58a 4d7e1e0d 80293f95 31e061a2
ffffd000`208578a0 "ConOutDev"
ffffd000`20857868 8be4df61 11d293ca e0000daa 8c2b0398
ffffd000`208578a0 "VgaDeviceInfo"
ffffd000`20857868 8be4df61 11d293ca e0000daa 8c2b0398
ffffd000`208578a0 "VgaDeviceCount"
ffffd000`20857868 8be4df61 11d293ca e0000daa 8c2b0398
ffffd000`208578a0 "RstScuO"
ffffd000`20857868 193dfefa 4302a445 3aefd899 c6041aad
ffffd000`208578a0 "DebuggerSerialPortsEnabledVar"
ffffd000`20857868 97ca1a5b 4d1fb760 90d14ba5 902c0392
ffffd000`208578a0 "SerialPortsEnabledVar"
ffffd000`20857868 560bf58a 4d7e1e0d 80293f95 31e061a2
ffffd000`208578a0 "DriverHlthEnable"
ffffd000`20857868 0885f288 4be1418c ad8bafa6 fe08da61
ffffd000`208578a0 "DriverHealthCount"
ffffd000`20857868 7459a7d4 44806533 e279a7bb c943445a
ffffd000`208578a0 "S3SS"
ffffd000`20857868 4bafc2b4 410402dc f1d636b2 849e8db9
ffffd000`208578a0 "RSCInfoAddresss"
ffffd000`20857868 8be4df61 11d293ca e0000daa 8c2b0398
ffffd000`208578a0 "PlatformLang"
ffffd000`20857868 8be4df61 11d293ca e0000daa 8c2b0398
ffffd000`208578a0 "AMITSESetup"
ffffd000`20857868 c811fa38 457942c8 e960bba9 34fbdd4e
ffffd000`208578a0 "UsbSupport"
ffffd000`20857868 ec87d643 4bb5eba4 3e3fe5a1 a90db236
ffffd000`208578a0 "SlotEnable"
ffffd000`20857868 ec87d643 4bb5eba4 3e3fe5a1 a90db236
ffffd000`208578a0 "FrontUsbEnable"
ffffd000`20857868 ec87d643 4bb5eba4 3e3fe5a1 a90db236
ffffd000`208578a0 "RearUsbEnable"
ffffd000`20857868 ec87d643 4bb5eba4 3e3fe5a1 a90db236
ffffd000`208578a0 "InternalUsbEnable"
ffffd000`20857868 ec87d643 4bb5eba4 3e3fe5a1 a90db236
ffffd000`208578a0 "PowerOnTime"
ffffd000`20857868 ec87d643 4bb5eba4 3e3fe5a1 a90db236
ffffd000`208578a0 "MSDigitalMarker"
ffffd000`20857868 c43c9947 4a343578 f38e5fb9 8e43c7a5
ffffd000`208578a0 "NotFirstBoot"
ffffd000`20857868 70040abc 45886387 cdddb187 f57a7d6c
ffffd000`208578a0 "ONBOARD_DEVS_PRESENT"
ffffd000`20857868 d98397ee 457a7a9d 68e5dfa9 18cc87ae
ffffd000`208578a0 "MemoryInfo"
ffffd000`20857868 7ee396a1 431bff7d cd8c53fa c5447c12
ffffd000`208578a0 "MEMajorVersion"
ffffd000`20857868 59416f8c 48c4b82d cb107e88 bbc38ec4
ffffd000`208578a0 "PciSerialPortsLocationVar"
ffffd000`20857868 560bf58a 4d7e1e0d 80293f95 31e061a2
ffffd000`208578a0 "HeciErrorReset"
ffffd000`20857868 31d2fce0 11e164b3 00086cb8 669a0c20
ffffd000`208578a0 "MeInfoSetup"
ffffd000`20857868 78259433 4db37b6d c436e89a 7da1c3c2
ffffd000`208578a0 "NetworkStackVar"
ffffd000`20857868 d1405d16 46957afc 454112bb a295369d
ffffd000`208578a0 "HSTime"
ffffd000`20857868 ae601ef0 11e0360b 0008429e 669a0c20
ffffd000`208578a0 "LastHDS"
ffffd000`20857868 ae601ef0 11e0360b 0008429e 669a0c20
ffffd000`208578a0 "ConsoleLock"
ffffd000`20857868 368cda0d 4b9bcf31 d1e7f68c 7e15ffbf
ffffd000`208578a0 "SetupAmtFeatures"
ffffd000`20857868 ec87d643 4bb5eba4 3e3fe5a1 a90db236
ffffd000`208578a0 "SlotPresent"
ffffd000`20857868 ec87d643 4bb5eba4 3e3fe5a1 a90db236
ffffd000`208578a0 "MMIOR."
ffffd000`20857868 3b2158f5 48c039d3 530384aa dbc6ba65
ffffd000`208578a0 "SysBuses"
ffffd000`20857868 55e6fc89 40763e74 e9d4c298 10e8c413
ffffd000`208578a0 "ThermalErrorLog"
ffffd000`20857868 707c9176 4e27a4c1 371c1d85 c873cab7
ffffd000`208578a0 "ConOut"
ffffd000`20857868 8be4df61 11d293ca e0000daa 8c2b0398
ffffd000`208578a0 "SetupCpuSockets"
ffffd000`20857868 ec87d643 4bb5eba4 3e3fe5a1 a90db236
ffffd000`208578a0 "FrontUsbPresent"
ffffd000`20857868 ec87d643 4bb5eba4 3e3fe5a1 a90db236
ffffd000`208578a0 "RearUsbPresent"
ffffd000`20857868 ec87d643 4bb5eba4 3e3fe5a1 a90db236
ffffd000`208578a0 "InternalUsbPresent"
ffffd000`20857868 ec87d643 4bb5eba4 3e3fe5a1 a90db236
ffffd000`208578a0 "Lang"
ffffd000`20857868 8be4df61 11d293ca e0000daa 8c2b0398
ffffd000`208578a0 "SBRealRevID"
ffffd000`20857868 707c9176 4e27a4c1 371c1d85 c873cab7
ffffd000`208578a0 "TdtAdvancedSetupDataVar"
ffffd000`20857868 7b77fb8b 4d7e1e0d 80393f95 76e061a2
ffffd000`208578a0 "HP_CTRACE"
ffffd000`20857868 707c9176 4e27a4c1 371c1d85 c873cab7
ffffd000`208578a0 "NBRealRevID"
ffffd000`20857868 707c9176 4e27a4c1 371c1d85 c873cab7
ffffd000`208578a0 "MeBiosExtensionSetup"
ffffd000`20857868 1bad711c 4241d451 3785f3b1 700c2e81
ffffd000`208578a0 "HpWriteOnceMetaData"
ffffd000`20857868 707c9176 4e27a4c1 371c1d85 c873cab7
ffffd000`208578a0 "PBRDevicePath"
ffffd000`20857868 8be4df61 11d293ca e0000daa 8c2b0398
ffffd000`208578a0 "TrueStruct"
ffffd000`20857868 7349bea7 420bc95c 1e6dcd8d a88b4d9d
ffffd000`208578a0 "PNP0501_11_NV"
ffffd000`20857868 560bf58a 4d7e1e0d 80293f95 31e061a2
ffffd000`208578a0 "PNP0501_12_NV"
ffffd000`20857868 560bf58a 4d7e1e0d 80293f95 31e061a2
ffffd000`208578a0 "SetupLtsxFeatures"
ffffd000`20857868 ec87d643 4bb5eba4 3e3fe5a1 a90db236
ffffd000`208578a0 "ucal"
ffffd000`20857868 707c9176 4e27a4c1 371c1d85 c873cab7
ffffd000`208578a0 "HpPassphraseStructureVariable"
ffffd000`20857868 707c9176 4e27a4c1 371c1d85 c873cab7
ffffd000`208578a0 "EfiTime"
ffffd000`20857868 9d0da369 46f8540b 5f2ba085 151e302c
ffffd000`208578a0 "Setup"
ffffd000`20857868 8173aefa 4574adf0 377139a0 1af24aab
ffffd000`208578a0 "PlatformLang"
ffffd000`20857868 8173aefa 4574adf0 377139a0 1af24aab
ffffd000`208578a0 "Timeout"
ffffd000`20857868 8173aefa 4574adf0 377139a0 1af24aab
ffffd000`208578a0 "AMITSESetup"
ffffd000`20857868 8173aefa 4574adf0 377139a0 1af24aab
ffffd000`208578a0 "IDESecDev"
ffffd000`20857868 8173aefa 4574adf0 377139a0 1af24aab
ffffd000`208578a0 "SystemIds"
ffffd000`20857868 8173aefa 4574adf0 377139a0 1af24aab
ffffd000`208578a0 "UsbSupport"
ffffd000`20857868 8173aefa 4574adf0 377139a0 1af24aab
ffffd000`208578a0 "PNP0501_0_NV"
ffffd000`20857868 8173aefa 4574adf0 377139a0 1af24aab
ffffd000`208578a0 "PNP0501_1_NV"
ffffd000`20857868 8173aefa 4574adf0 377139a0 1af24aab
ffffd000`208578a0 "PNP0400_0_NV"
ffffd000`20857868 8173aefa 4574adf0 377139a0 1af24aab
ffffd000`208578a0 "PNP0510_0_NV"
ffffd000`20857868 8173aefa 4574adf0 377139a0 1af24aab
ffffd000`208578a0 "HpMfgData"
ffffd000`20857868 8173aefa 4574adf0 377139a0 1af24aab
ffffd000`208578a0 "SlotEnable"
ffffd000`20857868 8173aefa 4574adf0 377139a0 1af24aab
ffffd000`208578a0 "FrontUsbEnable"
ffffd000`20857868 8173aefa 4574adf0 377139a0 1af24aab
ffffd000`208578a0 "RearUsbEnable"
ffffd000`20857868 8173aefa 4574adf0 377139a0 1af24aab
ffffd000`208578a0 "InternalUsbEnable"
ffffd000`20857868 8173aefa 4574adf0 377139a0 1af24aab
ffffd000`208578a0 "PowerOnTime"
ffffd000`20857868 8173aefa 4574adf0 377139a0 1af24aab
ffffd000`208578a0 "MSDigitalMarker"
ffffd000`20857868 8173aefa 4574adf0 377139a0 1af24aab
ffffd000`208578a0 "PNP0501_0_NV"
ffffd000`20857868 560bf58a 4d7e1e0d 80293f95 31e061a2
ffffd000`208578a0 "PNP0501_1_NV"
ffffd000`20857868 560bf58a 4d7e1e0d 80293f95 31e061a2
ffffd000`208578a0 "PNP0400_0_NV"
ffffd000`20857868 560bf58a 4d7e1e0d 80293f95 31e061a2
ffffd000`208578a0 "PNP0510_0_NV"
ffffd000`20857868 560bf58a 4d7e1e0d 80293f95 31e061a2
ffffd000`208578a0 "HpMor"
ffffd000`20857868 707c9176 4e27a4c1 371c1d85 c873cab7
ffffd000`208578a0 "PBRDevicePath"
ffffd000`20857868 a9b5f8d2 42c2cb6d ffb501bc 5e33e4aa
ffffd000`208578a0 "PetAlertCfg"
ffffd000`20857868 8be4df61 11d293ca e0000daa 8c2b0398
ffffd000`208578a0 "ConIn"
ffffd000`20857868 8be4df61 11d293ca e0000daa 8c2b0398
ffffd000`208578a0 "Timeout"
ffffd000`20857868 8be4df61 11d293ca e0000daa 8c2b0398
ffffd000`208578a0 "CurrentPolicy"
ffffd000`20857868 77fa9abd 4d320359 f42860bd 4b788fe7
ffffd000`208578a0 "a8ff1f3f4a8c94074056b76e9d7fab3a862c68d3"
ffffd000`20857868 ffffffff ffffffff ffffffff ffffffff
</code></pre></div></div>
<p>GetNextVariableName からの戻り値は 0 なのですが、最後の変数は名前、GUID ともに明らかに変です。このあと、最後の “a8ff1f3f4a8c94074056b76e9d7fab3a862c68d3” に対して GetVariable すると、戻り値が 14 (= EFI_NOT_FOUND) になります。呼び出し元は hal!HalEnumerateEnvironmentVariablesEx なのですが、GetVariable が失敗すると、この関数は見慣れたエラー コード c0000001 (= STATUS_UNSUCCESSFUL) を返すようになっていました。</p>
<p>というわけで、EFI の GetNextVariableName で列挙された変数名がなぜか GetVariable できないためインストールが上手くいかない、ことが分かりました。アセンブラでどうやって NVRAM にアクセスしているのか分かりませんが、やはり NVRAM が怪しいです。</p>
<p>それにしても HAL の動作には不満が残ります。GetNextVariableName で列挙された値が GetVariable 出来なかった場合は、全体をエラーにするのではなく、その変数だけをスキップして欲しいですね。</p>Windows Server 2016 が GA になったので、週末を利用して、仕事で使っているマシン 2 台の OS を Windows Server 2016 TP5 から RTM に入れ替えたのですが、インストーラーの怪しげな動作でハマったので書いておきます。強引にインストールは終わらせましたが、完全に直っていない上に根本原因が確認できていないので、時間を見つけて調べて追記する予定です。 (2016/10/30 追記 ) 最終確認はまだですが、NVRAM へのアクセスが失敗していることが原因っぽいです。 (詳細は後述) インストールを行った二台は HP Z420 Workstation と HP Z440 Workstation。前者がメインの開発機で、コードを書いてビルドする以外に、デバッグ用の Hyper-V 仮想マシンを動かしたり、メールを書いたりもします。後者はサブの開発機で、ほぼビルドと仮想マシンのみ。なお Windows 10 ではなく Server 2016 を入れるのは、Universal App などの煩わしい機能が嫌いだからです。サーバー SKU おすすめです。 家で使うマシンも仕事のマシンも同じですが、OS の入れ替えでは常にクリーン インストールを行います。インプレースアップグレードはいまいち動作が信用できないのです。 作業は、サブ機の Z440 から行いました。これには光学ドライブがないので、USB ドライブで Windows PE を起動して、別の USB ドライブに入れておいた Windows Server 2016 のインストーラー (MSDN からダウンロードした ISO を単にコピーしたもの) である setup.exe を起動して行います。前の OS が入っていたパーティションを消して、新しくパーティションを作って入れるだけです。作業としては普通です。 Z440 の作業は何の問題もなかったのですが、同じ USB ドライブを使って Z420 に Server 2016 をインストールしたところ、インストールの最後のフェーズで “Windows could not update the computer’s boot configuration. Installation cannot proceed.” が出ました。 じゃじゃーん これけっこう深刻なエラー・・・。メッセージの内容を信用するならば、インストーラーの wim イメージをボリュームにコピーした後、ブート情報を書き換えるところが失敗しているわけす。当然マシンは起動しなくなります。Windows RE も起動できません。ちなみにインストールが失敗したディスクからシステムを起動すると “Your PC/Device needs to be repaired - The Boot Configuration Data file is missing some required information. File: \BCD Error code: 0xc0000034” 画面が出ました。BCD 情報を書けなかったのはお前んところのインストーラーなんだけどな! 起動失敗の図 慌てず騒がず、とりあえず以下の作業を順番に試してみました。 まったく同じ作業 (同じ USB ドライブを使って、同じハードディスク (以降ディスク A) をインストール先として指定) を試す → 現象変わらず 別のハードディスク (以降ディスク B) を使ってみる → 現象変わらず ディスク B を diskpart の clear コマンドでクリアしてからインストール → 現象変わらず おいおい・・。一台目の Z440 で上手くいっていることからして、USB ドライブに問題はないはず。ハードディスク側にも問題があるとは思えない。ぱっと思いつくのは、BIOS/UEFI の起動方式とディスク形式の MBR/GPT。ディスクをクリアしても同じ現象が出る時点でインストーラーのバグくさいが、だとすると Z440 でうまくいった説明がつかない。Z420 も Z440 も UEFI のはずなのに。 しばらく悩んだ結果、Z420 と Z440 とで、パーティション構成に唯一の違いがあることに気づきました。Z440 のインストール先ディスクは 150GB なので、まるまる OS 用のパーティションを入れています。一方、Z420 のインストール先ディスク A は 1TB なので、後ろ半分の 500GB はデータ用にして、先頭の 500GB を OS 用にしていました。今回の作業では、前の OS のパーティションだけを消して、インストーラーから新しくパーティションを作り直していました。また、上記作業 2. と 3. の作業でも、インストーラー経由でパーティションを作ってからインストールをしていました。そこで、新しいディスク C を用意して、パーティションを作らず、ディスク全体を指定して OS をインストールしたところ・・・成功。謎は深まるばかり。 そしてすぐに次の問題が発生。インストール後に再起動がかかっても、またも “Your PC/Device needs to be repaired” エラーで Windows 起動しない・・・。インストール成功したんじゃなかったのかよ。 試行錯誤の末、Boot menu を開いてインストール先のディスクを明示的に選択すれば起動することが判明。これで何が起こっているのかは分かった、気がする。 Z420 のブート メニュー (Legacy Boot Sources の WDC WD2500AA.. を選択すれば起動できた) 起動がうまくいったあと、以下の情報を確認しました。 ディスク A は GPT 形式になっていて、EFI System Partition は作成されている → インストーラーの最後で BCD 情報の書き換えに失敗した理由が不明 ディスク B は MBR 形式になっている Z420 の msinfo32 を見ると、BIOS Mode は Legacy Z440 の起動ディスクは GPT 形式 Z440 の msinfo32 で、BIOS Mode は UEFI 上記を整理すると・・ Windows インストーラー、及び OS 本体は Z420 を Legacy BIOS だと思っているので、MBR 形式のディスクを対象にしたインストールを試みる。 しかし、インストーラーの中のパーティションを作る部分では、システムを UEFI と認識しているためか、MBR ではなく GPT 形式でパーティションを作る。 Z420 のブートの順番は、UEFI を試してから各ディスクの MBR を使って起動しようとする。このとき、UEFI の Windows Boot Manager がなぜか中途半端に実行できてしまうため、MBR の実行を試そうとしない。 システムは MBR 形式でインストールされているので当然起動できない。 不可解なのは以下の点。 そもそも最初に試したときに、BCD 情報が更新できなかった理由 → 諸悪の根源。これが上手くいっていれば問題はここまで拗れない。 インストーラーでパーティションを作るとディスクが GPT になるくせに、実際にシステムをインストールするときは MBR でディスクを切っている。インストーラーがシステムの BIOS モードをチェックするコードが少なくとも二ヶ所あって、一方は Legacy BIOS、他方は UEFI だと認識してしまうっぽい・・? 確か似たような現象を調べたときに、Windows のインストーラーにはディスクをクリアしてディスク形式を変更する機能がないので、インストーラーは Legacy BIOS のシステムでは GPT 形式のディスクを認識せず、逆に UEFI のシステムでは MBR のディスクを認識しなかった記憶があります。この場合、diskpart などを使ってあらかじめディスク形式を変えておく必要があります。 今回の場合は不可解で、GPT と MBR の扱いがかなり混在してしまっている印象がです。そもそもインストーラーやシステムがシステムを Legacy BIOS として認識しているのであれば、GPT で切れたディスクは認識できるべきではないし、インストールが始まる前にエラーになって欲しいものです。イメージのコピーはうまくいって、最後にこけるのは一番タチが悪い。これが Server 2016 のメディアで新しく発生するのか、もっと前の OS のディスクから発生するのかどうかは確かめていません。 今のところ、マシンを再起動するたびに boot menu を起動してディスクを選択しないといけない。超不便。 (2016/10/30 追記 ) まだ最終確認は取れていませんが、どうやら NVRAM への読み書きができないハードウェア障害のような気がしてきました。調べた内容を以下に記します。 まず、Windows インストール時のログはインストール先ディスクの $WINDOWS.~BT\Sources\Panther\setupact.log に残っているので、失敗している箇所を確認。 2016-10-22 16:46:58, Info [0x060228] IBS Callback_UpdateBootFiles:Successfully updated Windows boot files. 2016-10-22 16:46:58, Info IBSLIB ModifyBootEntriesLegacy: Not in first boot. No actions to perform. SetupPhase[2] 2016-10-22 16:46:58, Info IBSLIB ModifyBootEntriesBCD:Setup phase is [2] 2016-10-22 16:46:58, Info IBSLIB BfsInitializeBcdStore flags(0x00000008) RetainElementData:n DelExistinObject:n 2016-10-22 16:46:58, Info IBSLIB VolumePathName for H:\Windows is H:\ 2016-10-22 16:46:58, Info IBSLIB Opening template from \Device\HarddiskVolume7\Windows\System32\config\BCD-Template. 2016-10-22 16:46:58, Info IBSLIB System BCD store does not exist, creating. 2016-10-22 16:46:58, Error [0x064230] IBSLIB Failed to create a new system store. Status = [c0000001] 2016-10-22 16:46:58, Error [0x0641b8] IBSLIB ModifyBootEntries: Error modifying bcd boot entries. dwRetCode=[0x1F][gle=0x0000001f] 2016-10-22 16:46:58, Info [0x060216] IBS CallBack_MungeBootEntries:Failed to modify boot entries; GLE = 31 2016-10-22 16:46:58, Info [0x0640ae] IBSLIB PublishMessage: Publishing message [Windows could not update the computer's boot configuration. Installation cannot proceed.] 2016-10-22 16:46:58, Info [0x0a013d] UI Accepting Cancel. Exiting Page Progress. やはり BCD の新規作成に失敗している模様。 とりあえず BIOS の設定 (Advanced –> Device Options) を確認。もともとはこんな設定だった。後で触れるがこの時点でちょっと変。 とりあえず Option ROM を Legacy から EFI に変えて試してみる。 なお、ここで Video Options ROMS を EFI に変えてはいけない。間違って変えてしまうと、ビープ音が 6 回鳴ってシステムが起動しない悲しい状態になります。 Advisory: HP Z1, Z220, Z420, Z620, Z820 Workstation - 6 Beeps After Changing BIOS Settings http://h20564.www2.hp.com/hpsc/doc/public/display?docId=emr_na-c04045903 お決まりだが、見事にこの罠を踏んでしまったのだ。上記 Resolution にあるように、CMOS リセットを行って無事復活。PXE Option ROMS と Mass Storage Option ROMS だけを EFI にして再起動。そして同様にインストーラーを動かすが、状況は変わらず、結局 “Windows could not update the computer’s boot configuration. Installation cannot proceed.” で失敗する。 埒が明かなくなってきたので、真面目に setup.exe をデバッグしてみることに。最近の Windows PE では NIC の標準ドライバーが入っているので TCP/IP ネットワークで簡単にユーザーモードのリモート デバッグができます。Windows PE 起動後に以下のコマンドを実行すると、ネットワークが有効になるので dbgsrv.exe を起動できます。 > wpeutil initializenetwork > wpeutil disablefirewall 詳細は省きますが、問題となっているインストーラーの最終フェーズでは、まず空の BCD を作ってから、インストール イメージ内にある windows\system32\config\BCD-Template と NVRAM の状態を元に BCD にデータを入れていくようなことをやっているようです。空の BCD を作るところは問題なく成功して、NVRAM の内容を取ってくると思われるシステム コールから c0000001 が返ってきていました。カーネル デバッグまではやっていないので、カーネルの中で何が失敗しているのかはまだ不明のままです。 NVRAM がおかしいとすると、Option ROM が Legacy 設定だったにも関わらず “Your PC/Device needs to be repaired - The Boot Configuration Data file is missing some required information. File: \BCD Error code: 0xc0000034” が出る理由も分かるような気もします。NVRAM の内容が Server 2016 を入れる前の状態のまま変わっていない可能性が高く、本来であれば Option ROM を Legacy に変えたら NVRAM はクリアされて、MBR からの起動を自動的に試すのではないだろうか。 デバッガーを使って、NVRAM にアクセスしてエラー コードを返している箇所 (4 箇所あった) で、片っ端からエラーコードを変更してエラーがなかったように見せかける禁断の領域に踏み込んだところ、インストールは終わりましたが、初回起動で OOBE が始まる前の段階で “Windows could not complete the installation. To install Windows on this computer, restart the installation.” というポップアップが出て結局起動できず。どうやら Windows 起動時にも NVRAM へのアクセスを行なっているようだ。まあそりゃそうだろう。 Z420 で NVRAM をクリアする方法を探してみたが、どうにも見つからない。意図せずして行なった CMOS クリアではクリアされなかった。システム設定で ROM をクリアするオプションはあるのだが、何が消えるか分からないためちょっと恐くて試していない。びびり。 (2016/11/6 追記) 次は、何とかして NVRAM がおかしいという確証が欲しいところです。以下の資料を見ると、実は bcdedit /enum で出力される値は NVRAM variable から来ているらしい。 Presentations and Videos | Unified Extensible Firmware Interface Forum http://www.uefi.org/learning_center/presentationsandvideos 上記ページからダウンロードできる “Windows Boot Environment” という PDF の 20 ページ目によると - BCD has 1:1 mappings for some UEFI global variables - Any time {fwbootmgr} is manipulated, NVRAM is automatically updated さらに、以下のページによると Remove Duplicate Firmware Objects in BCD and NVRAM https://technet.microsoft.com/en-us/library/cc749510(v=ws.10).aspx When bcdedit opens the BCD, it compares entries in NVRAM with entries in BCD. Entries in NVRAM that were created by the firmware that do not exist in BCD are added to the system BCD. When bcdedit closes the system BCD, any boot manager entries in BCD that are not in NVRAM are added to NVRAM. 同様のことは以下の日本語のブログにもまとめられています。詳しくていい感じ。 PC-UEFI - DXR165の備忘録 http://dxr165.blog.fc2.com/blog-category-45.html 何はともあれ、Windows PE を起動して bcdedit を実行してみます。 X:\windows\system32> bcdedit /enum {fwbootmgr} The boot configuration data store could not be opened. A device attached to the system is not functioning. あっさり失敗。インストーラーを起動するまでもないですね。Server 2016 のメディアが悪いのではなく、やはりハードウェアがおかしいくさい。 さて、次のステップはいよいよ Windows PE のカーネル デバッグだろうか。幸い、Z420 と Z440 を 1394 ケーブルで繋げられたので、これでデバッグすることにする。最近だとイーサネットも使えるらしいが、試したことはない。 上記の bcdedit /enum {fwbootmgr} を実行すると、NT カーネルから HAL を経由し、EFI のランタイム サービス テーブルにおける GetNextVariableName が指すアドレスの関数を呼んで変数名を列挙し、GetVariable で値を取ってきます。ランタイム サービス テーブルの定義は、↓ のファイルにおける EFI_RUNTIME_SERVICES という構造体です。 TianoCore EDK2: MdePkg/Include/Uefi/UefiSpec.h Source File http://www.bluestop.org/edk2/docs/trunk/_uefi_spec_8h_source.html ランタイム サービス テーブルは、EFI システム テーブルの一部であり、EFI システム テーブルは efi_main 関数が取る 2 つのパラメーターのうちの一つらしい。 Programming for EFI: Using EFI Services http://www.rodsbooks.com/efi-programming/efi_services.html GetNextVariableName のプロトタイプ宣言は分かっているので、呼び出し部分から変数名を GUID を確認しました。102 個ありますが、とりあえず全部載せておきます。 ffffd000`208578a0 "CurrentDevicePath" ffffd000`20857868 8be4df61 11d293ca e0000daa 8c2b0398 ffffd000`208578a0 "BootCurrent" ffffd000`20857868 8be4df61 11d293ca e0000daa 8c2b0398 ffffd000`208578a0 "LangCodes" ffffd000`20857868 8be4df61 11d293ca e0000daa 8c2b0398 ffffd000`208578a0 "PlatformLangCodes" ffffd000`20857868 8be4df61 11d293ca e0000daa 8c2b0398 ffffd000`208578a0 "SSID" ffffd000`20857868 707c9176 4e27a4c1 371c1d85 c873cab7 ffffd000`208578a0 "UsbMassDevNum" ffffd000`20857868 ec87d643 4bb5eba4 3e3fe5a1 a90db236 ffffd000`208578a0 "SetupFeatureSupport" ffffd000`20857868 b6ad93e3 4c8519f7 c58072aa c7db9471 ffffd000`208578a0 "ErrOut" ffffd000`20857868 8be4df61 11d293ca e0000daa 8c2b0398 ffffd000`208578a0 "ErrOutDev" ffffd000`20857868 8be4df61 11d293ca e0000daa 8c2b0398 ffffd000`208578a0 "RstSataV" ffffd000`20857868 193dfefa 4302a445 3aefd899 c6041aad ffffd000`208578a0 "PNP0510_0_VV" ffffd000`20857868 560bf58a 4d7e1e0d 80293f95 31e061a2 ffffd000`208578a0 "RstScuV" ffffd000`20857868 193dfefa 4302a445 3aefd899 c6041aad ffffd000`208578a0 "BootOptionSupport" ffffd000`20857868 8be4df61 11d293ca e0000daa 8c2b0398 ffffd000`208578a0 "ConInDev" ffffd000`20857868 8be4df61 11d293ca e0000daa 8c2b0398 ffffd000`208578a0 "UsbMassDevValid" ffffd000`20857868 ec87d643 4bb5eba4 3e3fe5a1 a90db236 ffffd000`208578a0 "PNP0501_1_VV" ffffd000`20857868 560bf58a 4d7e1e0d 80293f95 31e061a2 ffffd000`208578a0 "PNP0400_0_VV" ffffd000`20857868 560bf58a 4d7e1e0d 80293f95 31e061a2 ffffd000`208578a0 "PNP0501_0_VV" ffffd000`20857868 560bf58a 4d7e1e0d 80293f95 31e061a2 ffffd000`208578a0 "ConOutDev" ffffd000`20857868 8be4df61 11d293ca e0000daa 8c2b0398 ffffd000`208578a0 "VgaDeviceInfo" ffffd000`20857868 8be4df61 11d293ca e0000daa 8c2b0398 ffffd000`208578a0 "VgaDeviceCount" ffffd000`20857868 8be4df61 11d293ca e0000daa 8c2b0398 ffffd000`208578a0 "RstScuO" ffffd000`20857868 193dfefa 4302a445 3aefd899 c6041aad ffffd000`208578a0 "DebuggerSerialPortsEnabledVar" ffffd000`20857868 97ca1a5b 4d1fb760 90d14ba5 902c0392 ffffd000`208578a0 "SerialPortsEnabledVar" ffffd000`20857868 560bf58a 4d7e1e0d 80293f95 31e061a2 ffffd000`208578a0 "DriverHlthEnable" ffffd000`20857868 0885f288 4be1418c ad8bafa6 fe08da61 ffffd000`208578a0 "DriverHealthCount" ffffd000`20857868 7459a7d4 44806533 e279a7bb c943445a ffffd000`208578a0 "S3SS" ffffd000`20857868 4bafc2b4 410402dc f1d636b2 849e8db9 ffffd000`208578a0 "RSCInfoAddresss" ffffd000`20857868 8be4df61 11d293ca e0000daa 8c2b0398 ffffd000`208578a0 "PlatformLang" ffffd000`20857868 8be4df61 11d293ca e0000daa 8c2b0398 ffffd000`208578a0 "AMITSESetup" ffffd000`20857868 c811fa38 457942c8 e960bba9 34fbdd4e ffffd000`208578a0 "UsbSupport" ffffd000`20857868 ec87d643 4bb5eba4 3e3fe5a1 a90db236 ffffd000`208578a0 "SlotEnable" ffffd000`20857868 ec87d643 4bb5eba4 3e3fe5a1 a90db236 ffffd000`208578a0 "FrontUsbEnable" ffffd000`20857868 ec87d643 4bb5eba4 3e3fe5a1 a90db236 ffffd000`208578a0 "RearUsbEnable" ffffd000`20857868 ec87d643 4bb5eba4 3e3fe5a1 a90db236 ffffd000`208578a0 "InternalUsbEnable" ffffd000`20857868 ec87d643 4bb5eba4 3e3fe5a1 a90db236 ffffd000`208578a0 "PowerOnTime" ffffd000`20857868 ec87d643 4bb5eba4 3e3fe5a1 a90db236 ffffd000`208578a0 "MSDigitalMarker" ffffd000`20857868 c43c9947 4a343578 f38e5fb9 8e43c7a5 ffffd000`208578a0 "NotFirstBoot" ffffd000`20857868 70040abc 45886387 cdddb187 f57a7d6c ffffd000`208578a0 "ONBOARD_DEVS_PRESENT" ffffd000`20857868 d98397ee 457a7a9d 68e5dfa9 18cc87ae ffffd000`208578a0 "MemoryInfo" ffffd000`20857868 7ee396a1 431bff7d cd8c53fa c5447c12 ffffd000`208578a0 "MEMajorVersion" ffffd000`20857868 59416f8c 48c4b82d cb107e88 bbc38ec4 ffffd000`208578a0 "PciSerialPortsLocationVar" ffffd000`20857868 560bf58a 4d7e1e0d 80293f95 31e061a2 ffffd000`208578a0 "HeciErrorReset" ffffd000`20857868 31d2fce0 11e164b3 00086cb8 669a0c20 ffffd000`208578a0 "MeInfoSetup" ffffd000`20857868 78259433 4db37b6d c436e89a 7da1c3c2 ffffd000`208578a0 "NetworkStackVar" ffffd000`20857868 d1405d16 46957afc 454112bb a295369d ffffd000`208578a0 "HSTime" ffffd000`20857868 ae601ef0 11e0360b 0008429e 669a0c20 ffffd000`208578a0 "LastHDS" ffffd000`20857868 ae601ef0 11e0360b 0008429e 669a0c20 ffffd000`208578a0 "ConsoleLock" ffffd000`20857868 368cda0d 4b9bcf31 d1e7f68c 7e15ffbf ffffd000`208578a0 "SetupAmtFeatures" ffffd000`20857868 ec87d643 4bb5eba4 3e3fe5a1 a90db236 ffffd000`208578a0 "SlotPresent" ffffd000`20857868 ec87d643 4bb5eba4 3e3fe5a1 a90db236 ffffd000`208578a0 "MMIOR." ffffd000`20857868 3b2158f5 48c039d3 530384aa dbc6ba65 ffffd000`208578a0 "SysBuses" ffffd000`20857868 55e6fc89 40763e74 e9d4c298 10e8c413 ffffd000`208578a0 "ThermalErrorLog" ffffd000`20857868 707c9176 4e27a4c1 371c1d85 c873cab7 ffffd000`208578a0 "ConOut" ffffd000`20857868 8be4df61 11d293ca e0000daa 8c2b0398 ffffd000`208578a0 "SetupCpuSockets" ffffd000`20857868 ec87d643 4bb5eba4 3e3fe5a1 a90db236 ffffd000`208578a0 "FrontUsbPresent" ffffd000`20857868 ec87d643 4bb5eba4 3e3fe5a1 a90db236 ffffd000`208578a0 "RearUsbPresent" ffffd000`20857868 ec87d643 4bb5eba4 3e3fe5a1 a90db236 ffffd000`208578a0 "InternalUsbPresent" ffffd000`20857868 ec87d643 4bb5eba4 3e3fe5a1 a90db236 ffffd000`208578a0 "Lang" ffffd000`20857868 8be4df61 11d293ca e0000daa 8c2b0398 ffffd000`208578a0 "SBRealRevID" ffffd000`20857868 707c9176 4e27a4c1 371c1d85 c873cab7 ffffd000`208578a0 "TdtAdvancedSetupDataVar" ffffd000`20857868 7b77fb8b 4d7e1e0d 80393f95 76e061a2 ffffd000`208578a0 "HP_CTRACE" ffffd000`20857868 707c9176 4e27a4c1 371c1d85 c873cab7 ffffd000`208578a0 "NBRealRevID" ffffd000`20857868 707c9176 4e27a4c1 371c1d85 c873cab7 ffffd000`208578a0 "MeBiosExtensionSetup" ffffd000`20857868 1bad711c 4241d451 3785f3b1 700c2e81 ffffd000`208578a0 "HpWriteOnceMetaData" ffffd000`20857868 707c9176 4e27a4c1 371c1d85 c873cab7 ffffd000`208578a0 "PBRDevicePath" ffffd000`20857868 8be4df61 11d293ca e0000daa 8c2b0398 ffffd000`208578a0 "TrueStruct" ffffd000`20857868 7349bea7 420bc95c 1e6dcd8d a88b4d9d ffffd000`208578a0 "PNP0501_11_NV" ffffd000`20857868 560bf58a 4d7e1e0d 80293f95 31e061a2 ffffd000`208578a0 "PNP0501_12_NV" ffffd000`20857868 560bf58a 4d7e1e0d 80293f95 31e061a2 ffffd000`208578a0 "SetupLtsxFeatures" ffffd000`20857868 ec87d643 4bb5eba4 3e3fe5a1 a90db236 ffffd000`208578a0 "ucal" ffffd000`20857868 707c9176 4e27a4c1 371c1d85 c873cab7 ffffd000`208578a0 "HpPassphraseStructureVariable" ffffd000`20857868 707c9176 4e27a4c1 371c1d85 c873cab7 ffffd000`208578a0 "EfiTime" ffffd000`20857868 9d0da369 46f8540b 5f2ba085 151e302c ffffd000`208578a0 "Setup" ffffd000`20857868 8173aefa 4574adf0 377139a0 1af24aab ffffd000`208578a0 "PlatformLang" ffffd000`20857868 8173aefa 4574adf0 377139a0 1af24aab ffffd000`208578a0 "Timeout" ffffd000`20857868 8173aefa 4574adf0 377139a0 1af24aab ffffd000`208578a0 "AMITSESetup" ffffd000`20857868 8173aefa 4574adf0 377139a0 1af24aab ffffd000`208578a0 "IDESecDev" ffffd000`20857868 8173aefa 4574adf0 377139a0 1af24aab ffffd000`208578a0 "SystemIds" ffffd000`20857868 8173aefa 4574adf0 377139a0 1af24aab ffffd000`208578a0 "UsbSupport" ffffd000`20857868 8173aefa 4574adf0 377139a0 1af24aab ffffd000`208578a0 "PNP0501_0_NV" ffffd000`20857868 8173aefa 4574adf0 377139a0 1af24aab ffffd000`208578a0 "PNP0501_1_NV" ffffd000`20857868 8173aefa 4574adf0 377139a0 1af24aab ffffd000`208578a0 "PNP0400_0_NV" ffffd000`20857868 8173aefa 4574adf0 377139a0 1af24aab ffffd000`208578a0 "PNP0510_0_NV" ffffd000`20857868 8173aefa 4574adf0 377139a0 1af24aab ffffd000`208578a0 "HpMfgData" ffffd000`20857868 8173aefa 4574adf0 377139a0 1af24aab ffffd000`208578a0 "SlotEnable" ffffd000`20857868 8173aefa 4574adf0 377139a0 1af24aab ffffd000`208578a0 "FrontUsbEnable" ffffd000`20857868 8173aefa 4574adf0 377139a0 1af24aab ffffd000`208578a0 "RearUsbEnable" ffffd000`20857868 8173aefa 4574adf0 377139a0 1af24aab ffffd000`208578a0 "InternalUsbEnable" ffffd000`20857868 8173aefa 4574adf0 377139a0 1af24aab ffffd000`208578a0 "PowerOnTime" ffffd000`20857868 8173aefa 4574adf0 377139a0 1af24aab ffffd000`208578a0 "MSDigitalMarker" ffffd000`20857868 8173aefa 4574adf0 377139a0 1af24aab ffffd000`208578a0 "PNP0501_0_NV" ffffd000`20857868 560bf58a 4d7e1e0d 80293f95 31e061a2 ffffd000`208578a0 "PNP0501_1_NV" ffffd000`20857868 560bf58a 4d7e1e0d 80293f95 31e061a2 ffffd000`208578a0 "PNP0400_0_NV" ffffd000`20857868 560bf58a 4d7e1e0d 80293f95 31e061a2 ffffd000`208578a0 "PNP0510_0_NV" ffffd000`20857868 560bf58a 4d7e1e0d 80293f95 31e061a2 ffffd000`208578a0 "HpMor" ffffd000`20857868 707c9176 4e27a4c1 371c1d85 c873cab7 ffffd000`208578a0 "PBRDevicePath" ffffd000`20857868 a9b5f8d2 42c2cb6d ffb501bc 5e33e4aa ffffd000`208578a0 "PetAlertCfg" ffffd000`20857868 8be4df61 11d293ca e0000daa 8c2b0398 ffffd000`208578a0 "ConIn" ffffd000`20857868 8be4df61 11d293ca e0000daa 8c2b0398 ffffd000`208578a0 "Timeout" ffffd000`20857868 8be4df61 11d293ca e0000daa 8c2b0398 ffffd000`208578a0 "CurrentPolicy" ffffd000`20857868 77fa9abd 4d320359 f42860bd 4b788fe7 ffffd000`208578a0 "a8ff1f3f4a8c94074056b76e9d7fab3a862c68d3" ffffd000`20857868 ffffffff ffffffff ffffffff ffffffff GetNextVariableName からの戻り値は 0 なのですが、最後の変数は名前、GUID ともに明らかに変です。このあと、最後の “a8ff1f3f4a8c94074056b76e9d7fab3a862c68d3” に対して GetVariable すると、戻り値が 14 (= EFI_NOT_FOUND) になります。呼び出し元は hal!HalEnumerateEnvironmentVariablesEx なのですが、GetVariable が失敗すると、この関数は見慣れたエラー コード c0000001 (= STATUS_UNSUCCESSFUL) を返すようになっていました。 というわけで、EFI の GetNextVariableName で列挙された変数名がなぜか GetVariable できないためインストールが上手くいかない、ことが分かりました。アセンブラでどうやって NVRAM にアクセスしているのか分かりませんが、やはり NVRAM が怪しいです。 それにしても HAL の動作には不満が残ります。GetNextVariableName で列挙された値が GetVariable 出来なかった場合は、全体をエラーにするのではなく、その変数だけをスキップして欲しいですね。Brute-force attack against NTLMv2 Response2016-09-19T01:44:18+00:002016-09-19T01:44:18+00:00https://msmania.github.io/2016/09/19/brute-force-attack-against-ntlmv2-response<p>2016 年 9 月の Windows Update で、NTLM SSO の動作に関連する脆弱性 CVE-2016-3352 が修正されたようです。</p>
<p>Microsoft Security Bulletin MS16-110 - Important <br />
<a href="https://technet.microsoft.com/library/security/MS16-110">https://technet.microsoft.com/library/security/MS16-110</a></p>
<p><em>An information disclosure vulnerability exists when Windows fails to properly validate NT LAN Manager (NTLM) Single Sign-On (SSO) requests during Microsoft Account (MSA) login sessions. An attacker who successfully exploited the vulnerability could attempt to brute force a user’s NTLM password hash.</em></p>
<p><em>To exploit the vulnerability, an attacker would have to trick a user into browsing to a malicious website, or to an SMB or UNC path destination, or convince a user to load a malicious document that initiates an NTLM SSO validation request without the consent of the user.</em></p>
<p>CVE - CVE-2016-3352 <br />
<a href="http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2016-3352">http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2016-3352</a></p>
<p><em>Microsoft Windows 8.1, Windows RT 8.1, and Windows 10 Gold, 1511, and 1607 do not properly check NTLM SSO requests for MSA logins, which makes it easier for remote attackers to determine passwords via a brute-force attack on NTLM password hashes, aka “Microsoft Information Disclosure Vulnerability.”</em></p>
<p>“NTLM SSO” などのキーワードで適当にインターネット検索すると、今年 8 月に書かれた以下の記事が見つかります。</p>
<p>The Register - Reminder: IE, Edge, Outlook etc still cough up your Windows, VPN credentials to strangers <br />
<a href="http://www.theregister.co.uk/2016/08/02/smb_attack_is_back/?mt=1474231650819">http://www.theregister.co.uk/2016/08/02/smb_attack_is_back/?mt=1474231650819</a></p>
<p>画面キャプチャーを見ると、なんと Microsoft アカウントのパスワードが解析されてしまっています。そして Youtube の埋め込み動画では、SMB パケットがブラウザーからリークしていることを示しています。つまり、SMB パケットに埋め込まれた NTLM メッセージに対して brute-force 攻撃をしかけることで Microsoft アカウントのパスワードを解析できる、ことを示しているようです。</p>
<p>これは理論的に可能でしょう。が、もしそれが 「簡単に」 できるのであれば、それは NTLM プロトコルの死を意味するべきであり、パッチとして一朝一夕に対応できるものではないはずです。SSLv3 や RC4 と同様にそのプロトコルの使用を止めるべきですが、非ドメイン環境において NTLM の代わりとなるような認証プロトコルは Windows には実装されていないはずです。</p>
<p>というわけで、NTLM に対する brute-force がどれぐらい簡単なのかを調べることにしました。まず、NTLM プロトコルがどうやってユーザー認証を行っているかを説明します。と言っても以下の PDF を読み解くだけです。</p>
<p>[MS-NLMP]: NT LAN Manager (NTLM) Authentication Protocol <br />
<a href="https://msdn.microsoft.com/en-us/library/cc236621.aspx">https://msdn.microsoft.com/en-us/library/cc236621.aspx</a></p>
<p>NTLM が混乱を招く点として、一つのプロトコルが複数のプロトコル バージョン (LanMan, NTLMv1, NTLMv2) に対応していることです。Wiki の情報を見ると NTLMv2 は NT 4.0 SP4 から採用されているので、 NTLMv2 だけで現在は問題なく生きていけるはずです。ただし XP などの古い OS において、さらに古い OS との互換性のために NTLMv1 や Lanman に fallback するような動作が有効になっている場合があります。このへんの細かい話は長くなりそうなので、以下の KB に丸投げします。</p>
<p>NT LAN Manager - Wikipedia, the free encyclopedia <br />
<a href="https://en.wikipedia.org/wiki/NT_LAN_Manager">https://en.wikipedia.org/wiki/NT_LAN_Manager</a></p>
<p>How to prevent Windows from storing a LAN manager hash of your password in Active Directory and local SAM databases <br />
<a href="https://support.microsoft.com/en-us/kb/299656">https://support.microsoft.com/en-us/kb/299656</a></p>
<p>Security guidance for NTLMv1 and LM network authentication <br />
<a href="https://support.microsoft.com/en-us/kb/2793313">https://support.microsoft.com/en-us/kb/2793313</a></p>
<p>NTLM は Challenge Reponse Authentication の一つで、簡単に書くとサーバーとクライアントが以下 3 つのメッセージを交換することで認証が行われます。(仕様によると Connectionless モードの場合は Negotiate が存在しないようですが、見たことがないのでパス。)</p>
<blockquote>
<p>Client: “I want you to authenticate me.”
(= Negotiate Message)</p>
</blockquote>
<blockquote>
<p>Server: “Sure. Challenge me. Use 0x0123456789abcdef as a ServerChallenge.”
(= Challenge Message)</p>
</blockquote>
<blockquote>
<p>Client: “My response is !@#$%^&*()_+…”
(= Authenticate Message)</p>
</blockquote>
<p>NTLM は単体で使われるプロトコルではなく、必ず別のプロトコルに埋め込まれて使われます。例えば SMB の中で使われる場合には、SMB Session Setup コマンドに埋め込まれます。ダイレクト SMB ポートである 445/tcp をキャプチャーしたときの Network Monitor 上での見え方は以下の通りです。</p>
<p><img src="/assets/2016-09-18-01.png" alt="" /> <br />
SMB packets over 445/tcp (Lines highlited in purple contain NTLM messages)</p>
<p><img src="/assets/2016-09-18-02.png" alt="" /> <br />
NTLM Negotiate Message</p>
<p><img src="/assets/2016-09-18-03.png" alt="" /> <br />
NTLM Challenge Message</p>
<p><img src="/assets/2016-09-18-04.png" alt="" /> <br />
NTLM Authenticate Message</p>
<p>セクション 3.3.2 NTLMv2 Authentication から、パスワードを解析するのに必要な疑似コードの計算式だけを抜き出すと以下の通りです。</p>
<p>[MS-NLMP]: NTLM v2 Authentication - 3.3.2 NTLM v2 Authentication <br />
<a href="https://msdn.microsoft.com/en-us/library/cc236700.aspx">https://msdn.microsoft.com/en-us/library/cc236700.aspx</a></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Define NTOWFv2(Passwd, User, UserDom) As
HMAC_MD5(MD4(UNICODE(Passwd)),
UNICODE(ConcatenationOf(Uppercase(User), UserDom)))
EndDefine
Set temp to ConcatenationOf(Responserversion,
HiResponserversion,
Z(6),
Time,
ClientChallenge,
Z(4),
ServerName, Z(4))
Set NTProofStr to HMAC_MD5(ResponseKeyNT,
ConcatenationOf(CHALLENGE_MESSAGE.ServerChallenge,
temp))
Set NtChallengeResponse to ConcatenationOf(NTProofStr, temp)
</code></pre></div></div>
<p>使用しているハッシュ関数は HMAC_MD5 と MD4 のみ。入力データはいろいろあって面倒そうに見えますが、それほど難しくありません。特に、疑似コードの中で temp として扱われているバージョンやタイムスタンプなどのメタ情報が、ハッシュ値である NTProofStr と連結してそのまま Authenticate Message の NtChallengeResponse になっているからです。図示したものを ↓ に示します。図中の Metainfo が、上記擬似コードで言うところの temp です。</p>
<p>実際に処理するときは、パスワードを UTF-16 に変換する点と、ユーザー名を大文字に変換する点に注意が必要です。</p>
<p><img src="/assets/2016-09-18-05.png" alt="" /> <br />
Calculation of NtChallengeResponse in NTLMv2</p>
<p>これで材料が出揃ったのでコードを書きます。まずサーバー側のコードとして、Samba サーバーが NTLM メッセージを処理しているときに、brute-force に必要となる情報をテキスト ファイルとして書き出すようにします。これによって、前述の Youtube 動画がやっていることとほぼ同じことができます。</p>
<p>Yandex SMB hash capture on IE with email message - YouTube <br />
<a href="https://www.youtube.com/watch?v=GCDuuY7UDwA">https://www.youtube.com/watch?v=GCDuuY7UDwA</a></p>
<p>msmania/samba at ntlm-hack <br />
<a href="https://github.com/msmania/samba/tree/ntlm-hack">https://github.com/msmania/samba/tree/ntlm-hack</a></p>
<p>テキスト ファイルと同じ内容をレベル 0 のデバッグ メッセージとしても出力するようにしました。gdb を使って smbd を実行しておくと、SMB 経由で NTLM 認証が行なわれたときにコンソールにログが記録されます。出力される情報はこんな感じです。後でまとめて grep できるようにあえてテキスト ファイルにしました。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Domain:
User: ladyg
Client: LIVINGROOM-PC
UserAndDomain=4c004100440059004700
Challenge=ae65c9f0192d64b9
Auth=0101000000000000b62acefc2d11d201ea3017d2f290d64e00..(長いので省略)
Response=39329a4a4e9052fe3d4dea4ea9c79ac5
</code></pre></div></div>
<p>次に、Samba サーバーが書き出したテキスト ファイルに対して実際に brute-force を行うプログラムを書きます。hashcat に新しいモードを付け足すことができればベストだったのですが、OpenCL を勉強する時間がなかったので、OpenSSL の関数を呼び出すだけの簡単なプログラムになりました。</p>
<p>msmania/ntlm-crack <br />
<a href="https://github.com/msmania/ntlm-crack">https://github.com/msmania/ntlm-crack</a></p>
<p>このプログラムは Samba が生成したテキスト ファイルに対して、別の引数として指定したのテキスト ファイルの各行をパスワードとして NTLMv2 Reponse を生成し、Samba が出力したデータと一致するかどうかを比較します。パスワード一覧ファイルは、ネット上で探せば簡単に見つかります。ntlm-crack リポジトリにサンプルとして入れてある 10_million_password_list_top_1000.txt は、以下のリポジトリからコピーしたものです。</p>
<p>GitHub - danielmiessler/SecLists <br />
<a href="https://github.com/danielmiessler/SecLists">https://github.com/danielmiessler/SecLists</a></p>
<p>では実際にコマンドを実行して brute-force の速度を計測します。マシン スペックは以下の通り。Windows マシンであればもっと新しいマシンで hashcat をぶん回せたのですが・・無念。</p>
<ul>
<li>OS: Ubuntu 16.04.1 LTS (GNU/Linux 4.4.0-36-generic x86_64)</li>
<li>CPU: Intel(R) Core(TM) i5-2520M CPU @ 2.50GHz (Sandy Bridge)</li>
</ul>
<p>とりあえず 100 万通りのパスワードを試してみます。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ ./t -f sample.in -l /data/src/SecLists/Passwords/10_million_password_list_top_1000000.txt
No matching password in /data/src/SecLists/Passwords/10_million_password_list_top_1000000.txt.
Tried 999999 strings in 3343 msec.
$ ./t -f sample.in -l /data/src/SecLists/Passwords/10_million_password_list_top_1000000.txt
No matching password in /data/src/SecLists/Passwords/10_million_password_list_top_1000000.txt.
Tried 999999 strings in 3372 msec.
$ ./t -f sample.in -l /data/src/SecLists/Passwords/10_million_password_list_top_1000000.txt
No matching password in /data/src/SecLists/Passwords/10_million_password_list_top_1000000.txt.
Tried 999999 strings in 3344 msec.
</code></pre></div></div>
<p>大体 3 秒ちょいです。特に工夫をしたわけでもないのですが、予想していたより速いです。UTF-16 変換の処理を予め済ませて、かつ並列処理をすれば 1M/s は軽く越えられそう。</p>
<p>もちろん実際に使われているのはこんな子供騙しではありません。参考として 4 年前の記事ですが、25-GPU を使って 95^8 通りのハッシュを 5.5 時間で生成できたと書かれています。ここでいう NTLM cryptographic algorithm が厳密に何を意味するのかは書かれていません。巷では、UTF-16 エンコードしたパスワードの MD4 ハッシュを NTLM ハッシュと呼ぶことが多く、仮にそうだとすると、5.5 時間という数字には HMAC-MD5 を 2 回計算する部分が含まれていません。試しに、今回作った ntlm-crack から HMAC-MD5 の演算を飛ばして再度実行してみます。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ ./t -f sample.in -l /data/src/SecLists/Passwords/10_million_password_list_top_1000000.txt
No matching password in /data/src/SecLists/Passwords/10_million_password_list_top_1000000.txt.
Tried 999999 strings in 347 msec.
$ ./t -f sample.in -l /data/src/SecLists/Passwords/10_million_password_list_top_1000000.txt
No matching password in /data/src/SecLists/Passwords/10_million_password_list_top_1000000.txt.
Tried 999999 strings in 347 msec.
$ ./t -f sample.in -l /data/src/SecLists/Passwords/10_million_password_list_top_1000000.txt
No matching password in /data/src/SecLists/Passwords/10_million_password_list_top_1000000.txt.
Tried 999999 strings in 346 msec.
</code></pre></div></div>
<p>計算時間が約 1/10 で済んでしまいました。この比率が 25-GPU マシンにも適用されるとすると、8 文字のパスワードをクラックするのに 4 年前は 55 時間かかっていたはずです。怪しい概算ですが。</p>
<p>25-GPU cluster cracks every standard Windows password in <6 hours | Ars Technica <br />
<a href="http://arstechnica.com/security/2012/12/25-gpu-cluster-cracks-every-standard-windows-password-in-6-hours/">http://arstechnica.com/security/2012/12/25-gpu-cluster-cracks-every-standard-windows-password-in-6-hours/</a></p>
<p>今年は AlphaGo のニュースが衝撃でしたが、そのときの Nature 論文では 1,920 CPUs + 280 GPUs のマシンで AlphaGo を動かしたという実績が書かれているので、個人での所有は難しいにしても、あるべきところ (Google や NSA?) には 4 桁のプロセッサーを動かせるマシンが存在していると仮定できます。これで 2 桁分稼げるので、10 桁程度のパスワードであれば数日で解ける恐れがあります。12 桁のパスワードにしておけば年単位の時間が必要になるので安心かも・・・?</p>
<p>話が逸れておきましたが、冒頭の CVE-2016-3352 の話に戻ります。Security Bulletin には、以下のような修正がなされたと書かれています。つまり、誰彼構わず NTLM SSO 認証のための SMB パケットを送るのは止めた、ということでしょうか。</p>
<blockquote>
<p>The security update addresses the vulnerability by preventing NTLM SSO authentication to non-private SMB resources when users are signed in to Windows via a Microsoft Account network firewall profile for users who are signed in to Windows via a Microsoft account (https://www.microsoft.com/account) and connected to a “Guest or public networks” firewall profile.</p>
</blockquote>
<p>この修正ができたということは、 「不特定多数のサーバーに SMB パケットを送ってしまう動作があったため、本来できないはずの brute-force 攻撃の標的になってしまう」 ことが問題とされていたわけです。これでようやく、Security Bulletin の “An attacker who successfully exploited the vulnerability could attempt to brute force” という部分が腑に落ちました。この件に関して言えば、brute-force の成功が現実的かどうかは関係なく、brute-force が可能であることそのものが問題だったわけです。NTLM はまだ生きていていいんだ。</p>
<p>あえてケチをつけるならば、CVE の方の記述における “to determine passwords via a brute-force attack on NTLM password hashes” でしょうか。NTLM は複数のハッシュ (MD4 と HMAC-MD5) を使いますが、NTLM password hash と書くと、パスワードのハッシュ、すなわち一段階目の MD4 ハッシュを想定するのが普通です。しかし、MD4 ハッシュは一回目の HMAC-MD5 の鍵として使われるだけで、ネットワーク上を流れることはなく、brute-force の攻撃対象にはなりません。Microsoft 側の Security Bulletin では “attempt to brute force a user’s NTLM password hash” となっており、こちらの記述のほうがより正確な気がします。最近の流行は pass-the-hash 攻撃なので、平文のパスワードに替えて、ハッシュ値が brute-force の標的であってもおかしくはありません。</p>
<p>ところで、なぜ Microsoft アカウントに関する言及があるかというと、冒頭で紹介した The Register の記事にもありますが、Microsoft アカウントのユーザー名がユーザーのメール アドレスであり、OneDrive や MSDN などのサービスのアカウントとしても使われているからです。世界のどこかにあるパソコンのユーザー アカウント “Mike” のパスワードが分かってもできることは限られていますが、Microsoft アカウントのパスワードが解析されると大変なことになります。だからこそ今までこの古いバグが放置されてきたのかもしれません。ただ、ユーザー名が平文で SMB として流れるのは気持ち悪いですが。</p>
<font color="#0000ff">(2016/9/19 追記)</font>
<p>9 月のアップデート後に、LAN ディスクや SAMBA にアクセスできなくなったというツイートやフォーラムを幾つか見つけましたが、おそらく CVE-2016-3352 に対する修正が原因と思われます。どうするんでしょうかね。</p>
<p>update kb3185614の不具合について、LANDISKへの接続やリモートアクセスができなくなる - マイクロソフト コミュニティ <br />
<a href="http://answers.microsoft.com/ja-jp/windows/forum/windows_10-update/update/f5219540-a2a5-4b09-b9b6-e944dcbbed38">http://answers.microsoft.com/ja-jp/windows/forum/windows_10-update/update/f5219540-a2a5-4b09-b9b6-e944dcbbed38</a></p>2016 年 9 月の Windows Update で、NTLM SSO の動作に関連する脆弱性 CVE-2016-3352 が修正されたようです。 Microsoft Security Bulletin MS16-110 - Important https://technet.microsoft.com/library/security/MS16-110 An information disclosure vulnerability exists when Windows fails to properly validate NT LAN Manager (NTLM) Single Sign-On (SSO) requests during Microsoft Account (MSA) login sessions. An attacker who successfully exploited the vulnerability could attempt to brute force a user’s NTLM password hash. To exploit the vulnerability, an attacker would have to trick a user into browsing to a malicious website, or to an SMB or UNC path destination, or convince a user to load a malicious document that initiates an NTLM SSO validation request without the consent of the user. CVE - CVE-2016-3352 http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2016-3352 Microsoft Windows 8.1, Windows RT 8.1, and Windows 10 Gold, 1511, and 1607 do not properly check NTLM SSO requests for MSA logins, which makes it easier for remote attackers to determine passwords via a brute-force attack on NTLM password hashes, aka “Microsoft Information Disclosure Vulnerability.” “NTLM SSO” などのキーワードで適当にインターネット検索すると、今年 8 月に書かれた以下の記事が見つかります。 The Register - Reminder: IE, Edge, Outlook etc still cough up your Windows, VPN credentials to strangers http://www.theregister.co.uk/2016/08/02/smb_attack_is_back/?mt=1474231650819 画面キャプチャーを見ると、なんと Microsoft アカウントのパスワードが解析されてしまっています。そして Youtube の埋め込み動画では、SMB パケットがブラウザーからリークしていることを示しています。つまり、SMB パケットに埋め込まれた NTLM メッセージに対して brute-force 攻撃をしかけることで Microsoft アカウントのパスワードを解析できる、ことを示しているようです。 これは理論的に可能でしょう。が、もしそれが 「簡単に」 できるのであれば、それは NTLM プロトコルの死を意味するべきであり、パッチとして一朝一夕に対応できるものではないはずです。SSLv3 や RC4 と同様にそのプロトコルの使用を止めるべきですが、非ドメイン環境において NTLM の代わりとなるような認証プロトコルは Windows には実装されていないはずです。 というわけで、NTLM に対する brute-force がどれぐらい簡単なのかを調べることにしました。まず、NTLM プロトコルがどうやってユーザー認証を行っているかを説明します。と言っても以下の PDF を読み解くだけです。 [MS-NLMP]: NT LAN Manager (NTLM) Authentication Protocol https://msdn.microsoft.com/en-us/library/cc236621.aspx NTLM が混乱を招く点として、一つのプロトコルが複数のプロトコル バージョン (LanMan, NTLMv1, NTLMv2) に対応していることです。Wiki の情報を見ると NTLMv2 は NT 4.0 SP4 から採用されているので、 NTLMv2 だけで現在は問題なく生きていけるはずです。ただし XP などの古い OS において、さらに古い OS との互換性のために NTLMv1 や Lanman に fallback するような動作が有効になっている場合があります。このへんの細かい話は長くなりそうなので、以下の KB に丸投げします。 NT LAN Manager - Wikipedia, the free encyclopedia https://en.wikipedia.org/wiki/NT_LAN_Manager How to prevent Windows from storing a LAN manager hash of your password in Active Directory and local SAM databases https://support.microsoft.com/en-us/kb/299656 Security guidance for NTLMv1 and LM network authentication https://support.microsoft.com/en-us/kb/2793313 NTLM は Challenge Reponse Authentication の一つで、簡単に書くとサーバーとクライアントが以下 3 つのメッセージを交換することで認証が行われます。(仕様によると Connectionless モードの場合は Negotiate が存在しないようですが、見たことがないのでパス。) Client: “I want you to authenticate me.” (= Negotiate Message) Server: “Sure. Challenge me. Use 0x0123456789abcdef as a ServerChallenge.” (= Challenge Message) Client: “My response is !@#$%^&*()_+…” (= Authenticate Message) NTLM は単体で使われるプロトコルではなく、必ず別のプロトコルに埋め込まれて使われます。例えば SMB の中で使われる場合には、SMB Session Setup コマンドに埋め込まれます。ダイレクト SMB ポートである 445/tcp をキャプチャーしたときの Network Monitor 上での見え方は以下の通りです。 SMB packets over 445/tcp (Lines highlited in purple contain NTLM messages) NTLM Negotiate Message NTLM Challenge Message NTLM Authenticate Message セクション 3.3.2 NTLMv2 Authentication から、パスワードを解析するのに必要な疑似コードの計算式だけを抜き出すと以下の通りです。 [MS-NLMP]: NTLM v2 Authentication - 3.3.2 NTLM v2 Authentication https://msdn.microsoft.com/en-us/library/cc236700.aspx Define NTOWFv2(Passwd, User, UserDom) As HMAC_MD5(MD4(UNICODE(Passwd)), UNICODE(ConcatenationOf(Uppercase(User), UserDom))) EndDefine Set temp to ConcatenationOf(Responserversion, HiResponserversion, Z(6), Time, ClientChallenge, Z(4), ServerName, Z(4)) Set NTProofStr to HMAC_MD5(ResponseKeyNT, ConcatenationOf(CHALLENGE_MESSAGE.ServerChallenge, temp)) Set NtChallengeResponse to ConcatenationOf(NTProofStr, temp) 使用しているハッシュ関数は HMAC_MD5 と MD4 のみ。入力データはいろいろあって面倒そうに見えますが、それほど難しくありません。特に、疑似コードの中で temp として扱われているバージョンやタイムスタンプなどのメタ情報が、ハッシュ値である NTProofStr と連結してそのまま Authenticate Message の NtChallengeResponse になっているからです。図示したものを ↓ に示します。図中の Metainfo が、上記擬似コードで言うところの temp です。 実際に処理するときは、パスワードを UTF-16 に変換する点と、ユーザー名を大文字に変換する点に注意が必要です。 Calculation of NtChallengeResponse in NTLMv2 これで材料が出揃ったのでコードを書きます。まずサーバー側のコードとして、Samba サーバーが NTLM メッセージを処理しているときに、brute-force に必要となる情報をテキスト ファイルとして書き出すようにします。これによって、前述の Youtube 動画がやっていることとほぼ同じことができます。 Yandex SMB hash capture on IE with email message - YouTube https://www.youtube.com/watch?v=GCDuuY7UDwA msmania/samba at ntlm-hack https://github.com/msmania/samba/tree/ntlm-hack テキスト ファイルと同じ内容をレベル 0 のデバッグ メッセージとしても出力するようにしました。gdb を使って smbd を実行しておくと、SMB 経由で NTLM 認証が行なわれたときにコンソールにログが記録されます。出力される情報はこんな感じです。後でまとめて grep できるようにあえてテキスト ファイルにしました。 Domain: User: ladyg Client: LIVINGROOM-PC UserAndDomain=4c004100440059004700 Challenge=ae65c9f0192d64b9 Auth=0101000000000000b62acefc2d11d201ea3017d2f290d64e00..(長いので省略) Response=39329a4a4e9052fe3d4dea4ea9c79ac5 次に、Samba サーバーが書き出したテキスト ファイルに対して実際に brute-force を行うプログラムを書きます。hashcat に新しいモードを付け足すことができればベストだったのですが、OpenCL を勉強する時間がなかったので、OpenSSL の関数を呼び出すだけの簡単なプログラムになりました。 msmania/ntlm-crack https://github.com/msmania/ntlm-crack このプログラムは Samba が生成したテキスト ファイルに対して、別の引数として指定したのテキスト ファイルの各行をパスワードとして NTLMv2 Reponse を生成し、Samba が出力したデータと一致するかどうかを比較します。パスワード一覧ファイルは、ネット上で探せば簡単に見つかります。ntlm-crack リポジトリにサンプルとして入れてある 10_million_password_list_top_1000.txt は、以下のリポジトリからコピーしたものです。 GitHub - danielmiessler/SecLists https://github.com/danielmiessler/SecLists では実際にコマンドを実行して brute-force の速度を計測します。マシン スペックは以下の通り。Windows マシンであればもっと新しいマシンで hashcat をぶん回せたのですが・・無念。 OS: Ubuntu 16.04.1 LTS (GNU/Linux 4.4.0-36-generic x86_64) CPU: Intel(R) Core(TM) i5-2520M CPU @ 2.50GHz (Sandy Bridge) とりあえず 100 万通りのパスワードを試してみます。 $ ./t -f sample.in -l /data/src/SecLists/Passwords/10_million_password_list_top_1000000.txt No matching password in /data/src/SecLists/Passwords/10_million_password_list_top_1000000.txt. Tried 999999 strings in 3343 msec. $ ./t -f sample.in -l /data/src/SecLists/Passwords/10_million_password_list_top_1000000.txt No matching password in /data/src/SecLists/Passwords/10_million_password_list_top_1000000.txt. Tried 999999 strings in 3372 msec. $ ./t -f sample.in -l /data/src/SecLists/Passwords/10_million_password_list_top_1000000.txt No matching password in /data/src/SecLists/Passwords/10_million_password_list_top_1000000.txt. Tried 999999 strings in 3344 msec. 大体 3 秒ちょいです。特に工夫をしたわけでもないのですが、予想していたより速いです。UTF-16 変換の処理を予め済ませて、かつ並列処理をすれば 1M/s は軽く越えられそう。 もちろん実際に使われているのはこんな子供騙しではありません。参考として 4 年前の記事ですが、25-GPU を使って 95^8 通りのハッシュを 5.5 時間で生成できたと書かれています。ここでいう NTLM cryptographic algorithm が厳密に何を意味するのかは書かれていません。巷では、UTF-16 エンコードしたパスワードの MD4 ハッシュを NTLM ハッシュと呼ぶことが多く、仮にそうだとすると、5.5 時間という数字には HMAC-MD5 を 2 回計算する部分が含まれていません。試しに、今回作った ntlm-crack から HMAC-MD5 の演算を飛ばして再度実行してみます。 $ ./t -f sample.in -l /data/src/SecLists/Passwords/10_million_password_list_top_1000000.txt No matching password in /data/src/SecLists/Passwords/10_million_password_list_top_1000000.txt. Tried 999999 strings in 347 msec. $ ./t -f sample.in -l /data/src/SecLists/Passwords/10_million_password_list_top_1000000.txt No matching password in /data/src/SecLists/Passwords/10_million_password_list_top_1000000.txt. Tried 999999 strings in 347 msec. $ ./t -f sample.in -l /data/src/SecLists/Passwords/10_million_password_list_top_1000000.txt No matching password in /data/src/SecLists/Passwords/10_million_password_list_top_1000000.txt. Tried 999999 strings in 346 msec. 計算時間が約 1/10 で済んでしまいました。この比率が 25-GPU マシンにも適用されるとすると、8 文字のパスワードをクラックするのに 4 年前は 55 時間かかっていたはずです。怪しい概算ですが。 25-GPU cluster cracks every standard Windows password in <6 hours | Ars Technica http://arstechnica.com/security/2012/12/25-gpu-cluster-cracks-every-standard-windows-password-in-6-hours/ 今年は AlphaGo のニュースが衝撃でしたが、そのときの Nature 論文では 1,920 CPUs + 280 GPUs のマシンで AlphaGo を動かしたという実績が書かれているので、個人での所有は難しいにしても、あるべきところ (Google や NSA?) には 4 桁のプロセッサーを動かせるマシンが存在していると仮定できます。これで 2 桁分稼げるので、10 桁程度のパスワードであれば数日で解ける恐れがあります。12 桁のパスワードにしておけば年単位の時間が必要になるので安心かも・・・? 話が逸れておきましたが、冒頭の CVE-2016-3352 の話に戻ります。Security Bulletin には、以下のような修正がなされたと書かれています。つまり、誰彼構わず NTLM SSO 認証のための SMB パケットを送るのは止めた、ということでしょうか。 The security update addresses the vulnerability by preventing NTLM SSO authentication to non-private SMB resources when users are signed in to Windows via a Microsoft Account network firewall profile for users who are signed in to Windows via a Microsoft account (https://www.microsoft.com/account) and connected to a “Guest or public networks” firewall profile. この修正ができたということは、 「不特定多数のサーバーに SMB パケットを送ってしまう動作があったため、本来できないはずの brute-force 攻撃の標的になってしまう」 ことが問題とされていたわけです。これでようやく、Security Bulletin の “An attacker who successfully exploited the vulnerability could attempt to brute force” という部分が腑に落ちました。この件に関して言えば、brute-force の成功が現実的かどうかは関係なく、brute-force が可能であることそのものが問題だったわけです。NTLM はまだ生きていていいんだ。 あえてケチをつけるならば、CVE の方の記述における “to determine passwords via a brute-force attack on NTLM password hashes” でしょうか。NTLM は複数のハッシュ (MD4 と HMAC-MD5) を使いますが、NTLM password hash と書くと、パスワードのハッシュ、すなわち一段階目の MD4 ハッシュを想定するのが普通です。しかし、MD4 ハッシュは一回目の HMAC-MD5 の鍵として使われるだけで、ネットワーク上を流れることはなく、brute-force の攻撃対象にはなりません。Microsoft 側の Security Bulletin では “attempt to brute force a user’s NTLM password hash” となっており、こちらの記述のほうがより正確な気がします。最近の流行は pass-the-hash 攻撃なので、平文のパスワードに替えて、ハッシュ値が brute-force の標的であってもおかしくはありません。 ところで、なぜ Microsoft アカウントに関する言及があるかというと、冒頭で紹介した The Register の記事にもありますが、Microsoft アカウントのユーザー名がユーザーのメール アドレスであり、OneDrive や MSDN などのサービスのアカウントとしても使われているからです。世界のどこかにあるパソコンのユーザー アカウント “Mike” のパスワードが分かってもできることは限られていますが、Microsoft アカウントのパスワードが解析されると大変なことになります。だからこそ今までこの古いバグが放置されてきたのかもしれません。ただ、ユーザー名が平文で SMB として流れるのは気持ち悪いですが。 (2016/9/19 追記) 9 月のアップデート後に、LAN ディスクや SAMBA にアクセスできなくなったというツイートやフォーラムを幾つか見つけましたが、おそらく CVE-2016-3352 に対する修正が原因と思われます。どうするんでしょうかね。 update kb3185614の不具合について、LANDISKへの接続やリモートアクセスができなくなる - マイクロソフト コミュニティ http://answers.microsoft.com/ja-jp/windows/forum/windows_10-update/update/f5219540-a2a5-4b09-b9b6-e944dcbbed38SUDO for Windows2016-06-30T17:29:14+00:002016-06-30T17:29:14+00:00https://msmania.github.io/2016/06/30/sudo-for-windows<p>Windows で動く Linux の sudo コマンド的なものを作って GitHub で公開しました。</p>
<p>msmania/sudo: SUDO for Windows <br />
<a href="https://github.com/msmania/sudo">https://github.com/msmania/sudo</a></p>
<p>Windows でコンソールを使った作業をする場合、必要がない限りは、管理者権限のない制限付きトークンのコマンド プロンプト (もしくは PowerShell) で作業すると思います。Windows の困ったところは、管理者権限が必要なコマンドを実行するときには、新しいコンソールを別途起動しないといけないことです。しかし、1 コマンド実行したいだけなのに、わざわざ新しくコンソールを開いて使い終わったら閉じるのは時間がもったいないのです。というわけで、通常のコンソールと管理者権限のコンソールの二窓体制で作業する、というのが日常かと思います。しかし、最近の Windows は無駄にグラフィカルで、ウィンドウを切り替えるときの Alt+Tab やタスクバー アイコンのプレビューがいちいち大袈裟です。今度はこれをレジストリで無効にしておく、というような具合に、理不尽なカスタマイズが次々と必要になります。そこで、sudo を作ることにしました。</p>
<p>代替策も探しましたが、意外とありません。例えば、runas.exe を使って built-in の Administrator ユーザーでログオンすれば管理者権限は使えます。しかし次の 2 点において不満があり使えません。</p>
<ul>
<li>runas.exe で cmd.exe を起動すると、結局新しいコンソールが起動する</li>
<li>Build-in の Administrator 以外では管理者権限にならない</li>
</ul>
<p>SysInternals の psexec.exe を使うと runas みたいなことができますが、こちらはそもそも管理者権限がないと実行できないので本末転倒です。</p>
<p>sudo に求めたい要件は以下の通りです。</p>
<ol>
<li>通常のコンソール上でそのまま作業できる (= 新しいコンソールは開かない)</li>
<li>Administrator ではなく、管理者グループに所属している作業ユーザーで管理者権限を使える</li>
<li>.NET やスクリプト経由ではなく、ネイティブの sudo.exe が欲しい</li>
</ol>
<p>GitHub 上を “sudo windows” などで検索すると、幾つかそれっぽいプロジェクトは見つかりますが、1. の要件を満たしてくれません。</p>
<p>maxpat78/Sudo: Executes a command requesting for elevated privileges in Windows Vista and newer OS. <br />
<a href="https://github.com/maxpat78/Sudo">https://github.com/maxpat78/Sudo</a></p>
<p>jpassing/elevate: elevate – start elevated processes from the command line <br />
<a href="https://github.com/jpassing/elevate">https://github.com/jpassing/elevate</a></p>
<p>どちらも動作原理は同じで、ShellExecute API を lpVerb=”runas” で実行しています。UAC プロンプトを表示させて管理者権限を得るには一番簡単な方法なのですが、コンソールから cmd.exe を起動すると別のコンソールが開いてしまうのでこれは使えません。</p>
<p>ShellExecute function (Windows) <br />
<a href="https://msdn.microsoft.com/en-us/library/windows/desktop/bb762153(v=vs.85).aspx">https://msdn.microsoft.com/en-us/library/windows/desktop/bb762153(v=vs.85).aspx</a></p>
<p>ShellExecute が駄目となると CreateProcess でプロセスを作るしかありません (他に WinExec という化石のような API もありますが)。が、以下のブログに記載があるように、CreateProcess で権限昇格が必要なプロセスを起動することはできません。</p>
<p>Dealing with Administrator and standard user’s context | Windows SDK Support Team Blog <br />
<a href="https://blogs.msdn.microsoft.com/winsdk/2010/05/31/dealing-with-administrator-and-standard-users-context/">https://blogs.msdn.microsoft.com/winsdk/2010/05/31/dealing-with-administrator-and-standard-users-context/</a></p>
<blockquote>
<p>CreateProcess() and CreateProcessWithLogonW do not have new flags to launch the child process as elevated. Internally, CreateProcess() checks whether the target application requires elevation by looking for a manifest, determining if it is an installer, or if it has an app compat shim. If CreateProcess() determines the target application requires elevation, it simply fails with ERROR_ELEVATION_REQUIRED(740). It will not contact the AIS to perform the elevation prompt or run the app.</p>
</blockquote>
<p>最後の AIS というのは Application Information Service (AppInfo サービス) という UAC を司る憎いやつです。いや、実際はお世話になっているのですが。</p>
<p>Understanding and Configuring User Account Control in Windows Vista <br />
<a href="https://technet.microsoft.com/en-us/library/cc709628(v=ws.10).aspx">https://technet.microsoft.com/en-us/library/cc709628(v=ws.10).aspx</a></p>
<p>となると残された道は 1 つしかなく、sudo.exe を昇格させて実行し、そこから CreateProcess で子プロセスを作る方法です。権限は自動的に継承されるので、子プロセスも昇格されたままになるはずです。</p>
<p>さらに要件 1. を満たすためには、もう一捻り必要です。sudo.exe をコンソール アプリケーション、すなわち /SUBSYSTEM:CONSOLE オプションを使ってリンクした場合、そのコンソール プログラムを昇格していないコンソール上から起動すると、UAC プロンプトの後にやはり新たなコンソールが起動して、プログラム終了時にコンソールが破棄される動作になるので、標準出力が見えません。したがって、sudo.exe は /SUBSYSTEM:WINDOWS を使ってリンクしなければなりません。この場合、単純にプログラムから printf などで標準出力に文字を出力しても、データは破棄されるだけでどこにも表示されません。そこで、プログラムの標準出力を親プロセスのコンソールに関連付けて、小プロセスからの標準出力を受け取って親プロセスの標準出力にリダイレクトするようにします。うう面倒くさい・・。</p>
<p>小プロセスの標準入出力をパイプとして受け取る部分は、以下のサンプルのコードを流用できます。</p>
<p>Creating a Child Process with Redirected Input and Output (Windows) <br />
<a href="https://msdn.microsoft.com/en-us/library/windows/desktop/ms682499(v=vs.85).aspx">https://msdn.microsoft.com/en-us/library/windows/desktop/ms682499(v=vs.85).aspx</a></p>
<p>親プロセスのコンソールを自分の標準出力に関連付ける部分は、AttachConsole API に ATTACH_PARENT_PROCESS を渡すことで簡単に実現できます。</p>
<p>AttachConsole function (Windows) <br />
<a href="https://msdn.microsoft.com/en-us/library/windows/desktop/ms681952(v=vs.85).aspx">https://msdn.microsoft.com/en-us/library/windows/desktop/ms681952(v=vs.85).aspx</a></p>
<p>sudo.exe 起動時に UAC プロンプトを表示させるには、マニフェスト XML で requestedExecutionLevel を highestAvailable に設定するだけです。Mt.exe を使うと、exe にマニフェストを埋め込むことが出来るので、Makefile の中でリンカーの後に Mt.exe を実行するように記述します。</p>
<p>Mt.exe (Windows) <br />
<a href="https://msdn.microsoft.com/en-us/library/aa375649(v=vs.85).aspx">https://msdn.microsoft.com/en-us/library/aa375649(v=vs.85).aspx</a></p>
<p>これらを組み合わせれば、基本の動きはほぼ達成できますが、最初の MSDN のサンプルには若干問題があります。サンプルをそのままコピーして標準出力に文字を出力するだけの簡単なプロセス (ipconfig.exe など) を実行すると、文字は出力されるのですが、ipconfig.exe が終了しても sudo.exe が終わりません。ReadFromPipe における ReadFile の呼び出しで、子プロセスが終了しているにも関わらず制御が返ってこないためです。原因は、STARTUPINFO 構造体に渡した子プロセス用のパイプへのハンドル、すなわち g_hChildStd_OUT_Wr と g_hChildStd_IN_Rd を閉じていないためと考えられます。ちゃんと確かめていないのですが、STARTUPINFO 構造体に渡したハンドルは、複製されてから小プロセスに渡されるので、子プロセス側で標準入出力のハンドルを破棄しても、複製元の g_hChildStd_OUT_Wr と g_hChildStd_IN_Rd を自分で保持している限りパイプが有効で、ReadFile はそれを待ち続けているのだと思います。じゃあ単純に CreateProcess の後でをさっさとハンドルをクローズしてしまえばよいかというと、大方のシナリオでは動くとは思いますが、微妙だと思います。子プロセスによっては、すぐに標準出力にデータを出力しない動作をするものがあってもおかしくありません。しかし、sudo.exe 側はパイプにデータが残っているかどうかだけで出力を続けるかどうかを判断しているので、子プロセスが動作中にも関わらずループを抜けてしまうかもしれません。</p>
<p>現時点のバージョンの sudo.exe では、単純に CreateProcess のあと子プロセスが終了するまで WaitForSingleObject で待って、その後でじっくりとパイプの中を読み出すようにしました。この実装も微妙で、子プロセスの出力量が膨大であったときにパイプが溢れる可能性がありますし、そもそも子プロセスが終わるまで待っているのは美しくありません。そのうち、子プロセスの状態を監視するようなスレッドを作って対応します。</p>
<p>WaitForSingleObject は使うものの、INIFINITE を渡して永遠に待ち続けるのはさすがに嫌だったので、タイムアウト値を設けることにしました。初め、このタイムアウト値は環境変数経由で設定しようと思っていました。そうすれば、CreateProcess の第二引数には WinMain の引数の pCmdLine をそのまま渡すだけで済むので、楽ができるのです。しかし、ここでも Windows のコマンド プロンプトの困った仕様が立ちはだかります。コマンドを実行している時だけに有効になる一時的な環境変数が使えないのです。正式に何というのかわかりませんが、つまり以下のようなことができません。make を実行するときによく使いますね。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ cat test.sh
echo $HOGE
$ echo $HOGE
$ HOGE=1 ./test.sh
1
$ echo $HOGE
$
</code></pre></div></div>
<p>一応、似たようなことはできます。それが以下のフォーラムで出てきているやり方で、cmd /C を使って子プロセスの中で set コマンドを実行してから目的のコマンドを実行する方法です。</p>
<p>Setting environment variable for just one command in Windows cmd.exe - Super User <br />
<a href="http://superuser.com/questions/223104/setting-environment-variable-for-just-one-command-in-windows-cmd-exe">http://superuser.com/questions/223104/setting-environment-variable-for-just-one-command-in-windows-cmd-exe</a></p>
<p>ちなみにこの中で紹介されている、/V オプションを使った環境変数の遅延評価は、子プロセスでは必要ですが孫プロセスでは不要です。マニアックですが、こんな感じです。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>> cmd /C "set HOGE=1 && echo %HOGE% !HOGE! && cmd /c echo %HOGE% !HOGE!"
%HOGE% !HOGE!
1 !HOGE!
> cmd /V /C "set HOGE=1 && echo %HOGE% !HOGE! && cmd /c echo %HOGE% !HOGE!"
%HOGE% 1
1 1
</code></pre></div></div>
<p>したがって、sudo.exe を使って cmd /c “set TIMEOUT=10 && sudo ipconfig” 的なことをやれば一応は環境変数からタイムアウト値を取れる、と思えますが結局駄目でした。UAC 昇格を要求するコマンドを起動した場合、環境変数が引き継がれません。これは Linux の sudo のデフォルト動作と同じですが、sudo に -E オプションを付加すると環境変数を引き継ぐことができます。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ echo $HOGE
$ HOGE=1 sudo ./test.sh
$ HOGE=1 sudo -E ./test.sh
1
</code></pre></div></div>
<p>上記の理由で、環境変数を使うのは諦めて引数を取ることにしました。空白などの扱いを自分で決めたかったので pCmdLine をパースするステート マシンを 1 から書きました。それが CCommandOptions クラスですが、無駄に長いです。たぶんもっとまともな実装方法があると思います。アルゴリズムは苦手・・。</p>
<p>とりあえずこれで要件を満たすコマンドができました。出力例は ↓ の通りです。ここまで書いて言うのもなんですが、UAC ポップアップの表示が遅いので、もっさりした動作にしかなりません。なんだかんだスピードを求めるなら二窓体制が無難かも。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>> sudo /t 10 powershell "get-vm | ?{$_.State -eq 'Running'} | select -ExpandProperty networkadapters | select vmname, macaddress, switchname, ipaddresses | fl *"
[sudo] START
Spawning a process (Timeout = 10 sec.)
VMName : VM1
MacAddress : 00155DFE7A01
SwitchName : External
IPAddresses : {10.124.252.117, fe80::8d2d:7dca:4498:82f9, 2001:4898:200:13:fc0e:945e:ffce:2bb5,
2001:4898:200:13:8d2d:7dca:4498:82f9}
[sudo] END
Press Enter to exit ...
</code></pre></div></div>Windows で動く Linux の sudo コマンド的なものを作って GitHub で公開しました。 msmania/sudo: SUDO for Windows https://github.com/msmania/sudo Windows でコンソールを使った作業をする場合、必要がない限りは、管理者権限のない制限付きトークンのコマンド プロンプト (もしくは PowerShell) で作業すると思います。Windows の困ったところは、管理者権限が必要なコマンドを実行するときには、新しいコンソールを別途起動しないといけないことです。しかし、1 コマンド実行したいだけなのに、わざわざ新しくコンソールを開いて使い終わったら閉じるのは時間がもったいないのです。というわけで、通常のコンソールと管理者権限のコンソールの二窓体制で作業する、というのが日常かと思います。しかし、最近の Windows は無駄にグラフィカルで、ウィンドウを切り替えるときの Alt+Tab やタスクバー アイコンのプレビューがいちいち大袈裟です。今度はこれをレジストリで無効にしておく、というような具合に、理不尽なカスタマイズが次々と必要になります。そこで、sudo を作ることにしました。 代替策も探しましたが、意外とありません。例えば、runas.exe を使って built-in の Administrator ユーザーでログオンすれば管理者権限は使えます。しかし次の 2 点において不満があり使えません。 runas.exe で cmd.exe を起動すると、結局新しいコンソールが起動する Build-in の Administrator 以外では管理者権限にならない SysInternals の psexec.exe を使うと runas みたいなことができますが、こちらはそもそも管理者権限がないと実行できないので本末転倒です。 sudo に求めたい要件は以下の通りです。 通常のコンソール上でそのまま作業できる (= 新しいコンソールは開かない) Administrator ではなく、管理者グループに所属している作業ユーザーで管理者権限を使える .NET やスクリプト経由ではなく、ネイティブの sudo.exe が欲しい GitHub 上を “sudo windows” などで検索すると、幾つかそれっぽいプロジェクトは見つかりますが、1. の要件を満たしてくれません。 maxpat78/Sudo: Executes a command requesting for elevated privileges in Windows Vista and newer OS. https://github.com/maxpat78/Sudo jpassing/elevate: elevate – start elevated processes from the command line https://github.com/jpassing/elevate どちらも動作原理は同じで、ShellExecute API を lpVerb=”runas” で実行しています。UAC プロンプトを表示させて管理者権限を得るには一番簡単な方法なのですが、コンソールから cmd.exe を起動すると別のコンソールが開いてしまうのでこれは使えません。 ShellExecute function (Windows) https://msdn.microsoft.com/en-us/library/windows/desktop/bb762153(v=vs.85).aspx ShellExecute が駄目となると CreateProcess でプロセスを作るしかありません (他に WinExec という化石のような API もありますが)。が、以下のブログに記載があるように、CreateProcess で権限昇格が必要なプロセスを起動することはできません。 Dealing with Administrator and standard user’s context | Windows SDK Support Team Blog https://blogs.msdn.microsoft.com/winsdk/2010/05/31/dealing-with-administrator-and-standard-users-context/ CreateProcess() and CreateProcessWithLogonW do not have new flags to launch the child process as elevated. Internally, CreateProcess() checks whether the target application requires elevation by looking for a manifest, determining if it is an installer, or if it has an app compat shim. If CreateProcess() determines the target application requires elevation, it simply fails with ERROR_ELEVATION_REQUIRED(740). It will not contact the AIS to perform the elevation prompt or run the app. 最後の AIS というのは Application Information Service (AppInfo サービス) という UAC を司る憎いやつです。いや、実際はお世話になっているのですが。 Understanding and Configuring User Account Control in Windows Vista https://technet.microsoft.com/en-us/library/cc709628(v=ws.10).aspx となると残された道は 1 つしかなく、sudo.exe を昇格させて実行し、そこから CreateProcess で子プロセスを作る方法です。権限は自動的に継承されるので、子プロセスも昇格されたままになるはずです。 さらに要件 1. を満たすためには、もう一捻り必要です。sudo.exe をコンソール アプリケーション、すなわち /SUBSYSTEM:CONSOLE オプションを使ってリンクした場合、そのコンソール プログラムを昇格していないコンソール上から起動すると、UAC プロンプトの後にやはり新たなコンソールが起動して、プログラム終了時にコンソールが破棄される動作になるので、標準出力が見えません。したがって、sudo.exe は /SUBSYSTEM:WINDOWS を使ってリンクしなければなりません。この場合、単純にプログラムから printf などで標準出力に文字を出力しても、データは破棄されるだけでどこにも表示されません。そこで、プログラムの標準出力を親プロセスのコンソールに関連付けて、小プロセスからの標準出力を受け取って親プロセスの標準出力にリダイレクトするようにします。うう面倒くさい・・。 小プロセスの標準入出力をパイプとして受け取る部分は、以下のサンプルのコードを流用できます。 Creating a Child Process with Redirected Input and Output (Windows) https://msdn.microsoft.com/en-us/library/windows/desktop/ms682499(v=vs.85).aspx 親プロセスのコンソールを自分の標準出力に関連付ける部分は、AttachConsole API に ATTACH_PARENT_PROCESS を渡すことで簡単に実現できます。 AttachConsole function (Windows) https://msdn.microsoft.com/en-us/library/windows/desktop/ms681952(v=vs.85).aspx sudo.exe 起動時に UAC プロンプトを表示させるには、マニフェスト XML で requestedExecutionLevel を highestAvailable に設定するだけです。Mt.exe を使うと、exe にマニフェストを埋め込むことが出来るので、Makefile の中でリンカーの後に Mt.exe を実行するように記述します。 Mt.exe (Windows) https://msdn.microsoft.com/en-us/library/aa375649(v=vs.85).aspx これらを組み合わせれば、基本の動きはほぼ達成できますが、最初の MSDN のサンプルには若干問題があります。サンプルをそのままコピーして標準出力に文字を出力するだけの簡単なプロセス (ipconfig.exe など) を実行すると、文字は出力されるのですが、ipconfig.exe が終了しても sudo.exe が終わりません。ReadFromPipe における ReadFile の呼び出しで、子プロセスが終了しているにも関わらず制御が返ってこないためです。原因は、STARTUPINFO 構造体に渡した子プロセス用のパイプへのハンドル、すなわち g_hChildStd_OUT_Wr と g_hChildStd_IN_Rd を閉じていないためと考えられます。ちゃんと確かめていないのですが、STARTUPINFO 構造体に渡したハンドルは、複製されてから小プロセスに渡されるので、子プロセス側で標準入出力のハンドルを破棄しても、複製元の g_hChildStd_OUT_Wr と g_hChildStd_IN_Rd を自分で保持している限りパイプが有効で、ReadFile はそれを待ち続けているのだと思います。じゃあ単純に CreateProcess の後でをさっさとハンドルをクローズしてしまえばよいかというと、大方のシナリオでは動くとは思いますが、微妙だと思います。子プロセスによっては、すぐに標準出力にデータを出力しない動作をするものがあってもおかしくありません。しかし、sudo.exe 側はパイプにデータが残っているかどうかだけで出力を続けるかどうかを判断しているので、子プロセスが動作中にも関わらずループを抜けてしまうかもしれません。 現時点のバージョンの sudo.exe では、単純に CreateProcess のあと子プロセスが終了するまで WaitForSingleObject で待って、その後でじっくりとパイプの中を読み出すようにしました。この実装も微妙で、子プロセスの出力量が膨大であったときにパイプが溢れる可能性がありますし、そもそも子プロセスが終わるまで待っているのは美しくありません。そのうち、子プロセスの状態を監視するようなスレッドを作って対応します。 WaitForSingleObject は使うものの、INIFINITE を渡して永遠に待ち続けるのはさすがに嫌だったので、タイムアウト値を設けることにしました。初め、このタイムアウト値は環境変数経由で設定しようと思っていました。そうすれば、CreateProcess の第二引数には WinMain の引数の pCmdLine をそのまま渡すだけで済むので、楽ができるのです。しかし、ここでも Windows のコマンド プロンプトの困った仕様が立ちはだかります。コマンドを実行している時だけに有効になる一時的な環境変数が使えないのです。正式に何というのかわかりませんが、つまり以下のようなことができません。make を実行するときによく使いますね。 $ cat test.sh echo $HOGE $ echo $HOGE $ HOGE=1 ./test.sh 1 $ echo $HOGE $ 一応、似たようなことはできます。それが以下のフォーラムで出てきているやり方で、cmd /C を使って子プロセスの中で set コマンドを実行してから目的のコマンドを実行する方法です。 Setting environment variable for just one command in Windows cmd.exe - Super User http://superuser.com/questions/223104/setting-environment-variable-for-just-one-command-in-windows-cmd-exe ちなみにこの中で紹介されている、/V オプションを使った環境変数の遅延評価は、子プロセスでは必要ですが孫プロセスでは不要です。マニアックですが、こんな感じです。 > cmd /C "set HOGE=1 && echo %HOGE% !HOGE! && cmd /c echo %HOGE% !HOGE!" %HOGE% !HOGE! 1 !HOGE! > cmd /V /C "set HOGE=1 && echo %HOGE% !HOGE! && cmd /c echo %HOGE% !HOGE!" %HOGE% 1 1 1 したがって、sudo.exe を使って cmd /c “set TIMEOUT=10 && sudo ipconfig” 的なことをやれば一応は環境変数からタイムアウト値を取れる、と思えますが結局駄目でした。UAC 昇格を要求するコマンドを起動した場合、環境変数が引き継がれません。これは Linux の sudo のデフォルト動作と同じですが、sudo に -E オプションを付加すると環境変数を引き継ぐことができます。 $ echo $HOGE $ HOGE=1 sudo ./test.sh $ HOGE=1 sudo -E ./test.sh 1 上記の理由で、環境変数を使うのは諦めて引数を取ることにしました。空白などの扱いを自分で決めたかったので pCmdLine をパースするステート マシンを 1 から書きました。それが CCommandOptions クラスですが、無駄に長いです。たぶんもっとまともな実装方法があると思います。アルゴリズムは苦手・・。 とりあえずこれで要件を満たすコマンドができました。出力例は ↓ の通りです。ここまで書いて言うのもなんですが、UAC ポップアップの表示が遅いので、もっさりした動作にしかなりません。なんだかんだスピードを求めるなら二窓体制が無難かも。 > sudo /t 10 powershell "get-vm | ?{$_.State -eq 'Running'} | select -ExpandProperty networkadapters | select vmname, macaddress, switchname, ipaddresses | fl *" [sudo] START Spawning a process (Timeout = 10 sec.) VMName : VM1 MacAddress : 00155DFE7A01 SwitchName : External IPAddresses : {10.124.252.117, fe80::8d2d:7dca:4498:82f9, 2001:4898:200:13:fc0e:945e:ffce:2bb5, 2001:4898:200:13:8d2d:7dca:4498:82f9} [sudo] END Press Enter to exit ...