1 ポイント 投稿者 GN⁺ 3 시간 전 | 1件のコメント | WhatsAppで共有
  • Linux 7.2 でカーネル内部の strncpy API の使用箇所がなくなり、以前から廃止予定だった文字列コピーインターフェースが最終的に削除された
  • strncpy() は指定したバイト数だけコピーするが、NUL 終端の挙動が直感的ではなく、カーネルでは長年にわたりバグの原因となっていた
  • 宛先バッファを不要に 0 で埋める性質は 性能問題 まで引き起こし、これを取り除くのに約 6 年と 362 件のコミットが必要だった
  • 金曜日のマージでは、API 本体だけでなく最後に残っていた per-CPU のアーキテクチャ別実装 もあわせて削除された
  • カーネルコードでは今後、用途に応じて strscpy(), strscpy_pad(), strtomem_pad(), memcpy_and_pad(), memcpy() といった代替関数を選ぶ必要がある

Linux 7.2 で消えた strncpy

  • Linux 7.2 は、カーネルで以前から 廃止予定 とされていた strncpy API を最終的に削除した
  • 6 年にわたる整理作業の末、カーネル内部で strncpy インターフェースを使うコードは完全になくなった
  • 今回の変更は単なる関数の置き換えではなく、古い文字列コピー慣行をカーネル全体から取り除く作業に近い

削除までにかかった作業規模

  • strncpy の削除には約 362 件のコミット が必要だった
  • 作業は、カーネル内部の strncpy 使用コードを段階的になくしていく形で進められた
  • Linux 7.2 でこの整理作業は完了に達した

strncpy がカーネルで問題だった理由

  • strncpy は Linux カーネル内で長年 継続的なバグ要因 とみなされていた
  • 特に問題だったのは次の 2 つの挙動
    • NUL 終端 の意味と動作が直感的ではなく、利用者がミスしやすい
    • 宛先バッファを重複して 0 埋めするため、不要な性能コストが発生する

実際の削除マージ

  • 金曜日に行われたマージstrncpy API が削除された
  • 同じマージで、最後に残っていた per-CPU のアーキテクチャ別 strncpy 実装 もあわせて消えた

カーネルコードで使う代替 API

  • strncpy の代わりに、コピー先と終端条件に応じた関数を選ぶ必要がある
    • strscpy(): NUL 終端される宛先に使用
    • strscpy_pad(): NUL 終端される宛先で 0 パディングが必要な場合に使用
    • strtomem_pad(): NUL 終端されない固定幅フィールドに使用
    • memcpy_and_pad(): 明示的なパディングを伴う制限付きコピーに使用
    • memcpy(): 長さが分かっているメモリコピーに使用

1件のコメント

 
GN⁺ 3 시간 전
Hacker Newsのコメント
  • 昔は、世界最高水準のC開発者である Linuxカーネル開発者 たちが stringbuffer や stringview 型も作れないとからかわれていたが、当時は今のようにこの話題について合意があった時代ではなかったので、ある程度は理解できる。
    正しい方向性をすでに見ていたのは Dennis Ritchie で、1990年にC向けの fat pointer 型を提案していた。これがC99に入っていれば完璧な追加機能になっていたはずで、委員会が採用していれば世界はかなり違っていたかもしれない。
    2007年には Walter Bright の「C's greatest mistake」という記事が出て、2度目のチャンスがあった。これは本質的には Ritchie と同じ発想である slice/stringview をより明確に説明していたが、それでもC11には入らなかった。C23まで来てもまだ存在せず、その代わりに _Generic と VLA を得たので、盛大に祝おうという感じだ。

    • Walter Bright の2007年の記事はこちら: https://digitalmars.com/articles/C-biggest-mistake.html
      検索していたら同じ話題の Reddit 投稿も見つけたが、自転車小屋論争が面白かった: https://www.reddit.com/r/C_Programming/comments/90uq7c/cs_bi...
      Cの配列がポインタに崩壊する動作が、なぜそう設計されたのか気になる。Bのコードを最小限の変更でCにコンパイルできるようにするのが目的だったという説明があるが、Bでは配列宣言が実際にはポインタと配列を定義し、そのポインタが配列の最初の要素を指すよう初期化されていたらしい。
    • VLA はC11でオプション機能に格下げされ、それは良いことだと思う。
      今のもっと大きな問題は、C標準ライブラリがいまだにK&R時代に縛られており、C99で追加された構造体引数や戻り値のような言語機能すら標準ライブラリAPIに反映されていない点だ。標準ライブラリにポインタ/サイズの組である 範囲構造体 と、それを使う新しい文字列関数や更新版の文字列関数があるだけでも、かなり改善できるはずだ。
    • Ritchie の提案へのリンク: https://web.archive.org/web/20150611114358/https://www.bell-...
    • チームワークでいちばん腹立たしいパターンはこれだ。解決策A、B、Cがあり、それぞれ長所と短所があるので2週間議論した末、結局 何も選ばない
    • それは WG14の優先順位 がどこにあるかを示しているだけだ。
  • Linuxカーネル内の strncpy は、直感に反するセマンティクス、NUL終端処理、宛先を不要に0で埋めることによる性能問題のせいで、何年もの間「しつこいバグの温床」だったという。
    Cコードレビューを頼まれるたびに strncpy を探していたが、毎回そこでバグを見つけていた。

  • 40年間ずっと気に障ってきたものがある。NUL終端文字列、そして今では入出力におけるUTF-8以外の文字列まで含まれる。
    行末をLF、CR、CRLFで扱う慣習もそうだし、パイプやカンマでフィールドを区切る方式もそうだ。GS、FS、RSのような曖昧でないASCII文字を使っていれば、行末のエンコード/デコードは入出力の問題になり、HT/VT/CR/LF/FFは文字通り出力関連のコードとして残せたはずだ。

    • ASCIIのフィールド/レコード区切り文字でフレーミングされたデータを変換するプロジェクトをやったことがあるが、本当に簡単に処理できた。
      カンマ区切りデータで発生する厄介な エスケープ処理 の悩みが消えて、ずっと単純になった。
    • Unicodeには今ではもっと選択肢がある。EBCDIC由来のような NL Next line、Unicodeが作った LS Line separator、PS Paragraph separator がある。
      Unicode標準は、CR、LF、CRLFとこれらの文字だけでなく、垂直タブフォームフィード も行区切りとして扱うべきだとしている。
    • 標準入出力では UTF-8 は完璧にうまく動く。もちろん、国際テキストエンコーディングで90年代前半に取り残されている Windows でなければという話だ。
      LF、CR、CRLFのような行末はOSの慣習でもあり、プログラミング言語が正しい行末を「推測」しようとしないほうがよい。これは解決する以上に問題を増やし、繰り返しになるが、たいていはWindows特有の問題なので、Microsoft が Windows を今世紀へ連れてくるべきだ。
    • LF がいちばん理にかなっているが、テキストファイルならどちらでも構わない。問題は CSV がテキストではないことだ。
      最後に bash でCSVファイルを扱わなければならなかったときは、内部的に RS と FS に変換して処理した。
    • もうどこでも UTF-8 を使えばいいと思う。
  • strncpy の代わりに Linuxカーネルのコードでは、NUL終端の宛先には strscpy()、0パディングが必要なNUL終端の宛先には strscpy_pad()、NUL終端でない固定幅フィールドには strtomem_pad()、明示的パディング付きの境界コピーには memcpy_and_pad()、長さが分かっているメモリコピーには memcpy() を使えという。
    これは悪夢のようで、ここまで 複雑である必要があるのか わからない。

    • 理由は 性能 だ。これらの大半を処理する安全な万能関数は、内部の分岐のせいでどうしても遅くなるし、どの関数を選ぶかには開発者の意図が表れている。
      コードを読むとき、関数の選択だけで開発者の意図が明確に見えるほうがよいと思う。
    • strncpy を正しく使うのは、もともと常に複雑だった。
    • せめて名前だけでももう少し良くできなかったのだろうか。
  • こういう退屈な反復作業こそ、システムエンジニアリングの本当の仕事 が行われる場所だ。
    Linuxカーネルを全工程を通して実運用可能な状態に保ちながら、より信頼性の高いものにしていくこうした大規模インフラプロジェクトは、数か月ではなく数十年単位で動く。

    • なぜ数十年規模になるのかは理解できる。ユーザーと依存関係のロングテールが本当に長いからだ。
      ただ、その速度で長期的に意味のある進歩を生み出せるのかはよくわからない。不満というより、中核インフラのパラドックス に近い。
  • これはすごいし、謙虚な気持ちにもなる仕事だ。これほど多くの人が貢献したことに驚かされる
    「すばらしい新機能」は功績として認められやすいが、カーネルのような根本的な対象では、悪い機能を取り除くことのほうがむしろ重要なのかもしれない
    50年後、人々がソースコードの読み方を忘れ、Claude/Codexの残骸が静かに積み上がって地球のエネルギーの大半を燃やす時代が来たら、こうした仕事は「建国時代」の伝説のように語られる気がする

    • Vernor Vingeの Deepness in the Sky を思い出した。あれでは、ある人物がソフトウェア考古学で宇宙船を保守している
      Unix epoch が何かを知っている唯一の人物でもある
    • 50年後に誰もがソースコードを理解する方法を忘れている、ということはないと思う。物事がどう動くのかを知りたがる人間の欲求は、その時代にも残っているはずだ
    • AIが作った 寄せ集めコード は、それよりずっと前に手に負えなくなるだろうと思う
  • ヌル終端文字列 は、コンピューティング史上最大の失敗だと思う。Pascal式文字列のほうがずっと安全だった

    • Visual Basic、そして後のCOMが採用した BSTR のような中間的な落としどころもある
      依然として0で終わる文字配列を指すポインタだが、ポインタが指す先頭バイトの直前に長さフィールドがある。埋め込みNUL文字がないという前提ではC文字列とも互換性があり、BSTR型の関数は長さの値を利用できる
    • ある程度は同意するが、サイズフィールドの型をめぐって争いが起きていただろう。可変長でなければなおさらだし、可変長ならまた別の問題があったはずだ
      しばらくの間は16ビットですら大きすぎると感じられたかもしれないし、今では32ビットが小さすぎるように見えるかもしれない。「強い型付け」の言語であるはずのCは、実際に重要だった場所ではかなり緩い
    • ヌル終端文字列は、非常に多くの有用なソフトウェアの土台だった。これをコンピューティング最大の失敗と呼ぶのは少し大げさだ
      Pascal関連のコードは30年以上書いていないが、当時でも文字列システムは使いづらすぎると思っていた、というかすかな記憶がある
    • 255文字あればみんなに十分なはずじゃなかったのか?
    • 改行終端の行と同じくらいひどい
  • 文字列型 が1つないだけで生じる苦労と無駄な作業が多すぎる

    • 正確には、文字列型がないこと自体ではなく、Cに文字列型がないという事実を回避するために生じる苦労と無駄な作業だ
    • ここで強い型付けを導入するなら、どんなやり方があり得るだろう? strncpy 周辺のコードも、その型や関数を使うように大規模なリファクタリングが必要になるのではないかと思う
  • strncpy の使用箇所を書き換えるのに、何がそんなに難しくて 6年 もかかったのか気になる
    使用箇所がそれだけ広範だったのか、それとも同じファイルに手を入れる機会があるときだけ直していく長期作業だったのか、あるいは別の難しさがあったのか知りたい

  • Win32アプリで 空白パディング文字列 を使うコードを扱ったことがある。宛先文字列は空白でパディングされるが、最後のバイトにはそれでもヌル文字があった
    長さやコピーのような処理には、専用の文字列関数版を使わなければならなかった。なぜそうだったのかは分からないが、コードベースがあまりに古いので、Pascalの構造体の挙動に由来していたのかもしれない

    • SQLデータベースの char フィールド 由来の文字列だったのかもしれない。varchar ではなく char フィールドは空白でパディングされる
    • この挙動のルーツはPascalではなく COBOL だと思う
    • 文字列サイズが変わるたびの再割り当てを防ごうとしていたのかもしれないし、CPUキャッシュラインの整列のためだったのかもしれない