この記事について
この記事は echo
コマンドのソースコードを書き換えて動かすハンズオンです。
書き換える過程で触れる技術の理解を深めることを目的としています。
こんな感じの処理に書き換えます
映画『マトリックス』(原題: The Matrix) の冒頭でトリニティがネオにコンタクトを取るシーンが好きなのでオマージュしてみました。
こんな人におすすめ
- GNU/Linux が好き
- ディストリビューション標準のパッケージの知見を深めたい
- GNU Coreutils ともっと仲良くなりたい
- C のプログラムに触ってみたい
- Makefile でプログラムのコンパイルしてみたい
作業環境
- MacOS on Macbook M1
- Ubuntu 24.04 on Docker
Docker 上で完結するので Windows 環境の場合はコンテナの準備手順を適当に読み替えてください。
コンテナ準備
コンテナイメージを引っ張ってきます。
特に何でもいいですが今回はメジャーなところで Ubuntu の最新LTS(※執筆時点)である 24.04 を使います。
$ docker pull ubuntu:24.04
24.04: Pulling from library/ubuntu
59a5d47f84c3: Download complete
Digest: sha256:353675e2a41babd526e2b837d7ec780c2a05bca0164f7ea5dbbd433d21d166fc
Status: Downloaded newer image for ubuntu:24.04
docker.io/library/ubuntu:24.04
What's next:
View a summary of image vulnerabilities and recommendations → docker scout quickview ubuntu:24.04
イメージ確認したらコンテナを起動してシェルにアタッチ。
$ docker images -f reference=ubuntu:24.04
REPOSITORY TAG IMAGE ID CREATED SIZE
ubuntu 24.04 353675e2a41b 8 days ago 139MB
$ docker run --name ubuntu -it ubuntu bash
root@3fabf328c64a:/#
ここから先はすべてコンテナのシェル上での作業です。
ディストリビューションに含まれている echo
の確認
echo
の動作確認。
root@3fabf328c64a:/# echo hogehoge
hogehoge
実行ファイルのパスを調べます。
aptリポジトリから対象パッケージを検索するためにファイルパスを使用します。
root@3fabf328c64a:/# which echo
/usr/bin/echo
root@3fabf328c64a:/# ls -l /usr/bin/echo
-rwxr-xr-x 1 root root 67792 Jun 22 16:21 /usr/bin/echo
シンボリックリンクも貼られていないので /usr/bin/echo が実態と見てOK。
apt-file
のインストール
実行ファイルが提供されている apt パッケージを特定するために apt-file
を使います。
ディストリビューションには最初から含まれないので apt でダウンロードしましょう。
まずはお約束。
最新のパッケージリストを取得してパッケージをすべて更新しておきます。
root@3fabf328c64a:/# apt update && apt upgrade -y
...(略)...
インストール。
root@3fabf328c64a:/# apt install -y apt-file
...(略)...
apt-file
自体もパッケージリストを参照するので最新を取得します。
(というかインストール直後だからパッケージリスト自体存在してないはず)
root@3fabf328c64a:/# apt-file update
...(略)...
それでは /usr/bin/echo が提供されるパッケージを調べていきます。
root@3fabf328c64a:/# apt-file -F search `which echo`
coreutils: /usr/bin/echo
分かりきってたことですが echo
コマンドが含まれているパッケージは coreutils
です。
ディストリビューションによっては付属のシェルにビルトインで実装されているケースも存在するので確認するに越したことはないです。
(昔 WSL2 で使っていた Ubuntu が確かこれでした)
Coreutils の詳細はこんな感じ。
見たところで今必要な情報は特にないです。(強いて言えばバージョン分かるくらい)
root@3fabf328c64a:/# apt show coreutils
Package: coreutils
Version: 9.4-3ubuntu6.1
Priority: required
Essential: yes
Section: utils
Origin: Ubuntu
Maintainer: Ubuntu Developers <ubuntu-devel-discuss@lists.ubuntu.com>
Original-Maintainer: Michael Stone <mstone@debian.org>
Bugs: https://bugs.launchpad.net/ubuntu/+filebug
Installed-Size: 9691 kB
Pre-Depends: libacl1 (>= 2.2.23), libattr1 (>= 1:2.4.48), libc6 (>= 2.38), libgmp10 (>= 2:6.3.0+dfsg), libselinux1 (>= 3.1~), libssl3t64 (>= 3.0.0)
Breaks: usrmerge (<< 39)
Homepage: http://gnu.org/software/coreutils
Task: minimal, server-minimal
Download-Size: 1364 kB
APT-Manual-Installed: yes
APT-Sources: http://ports.ubuntu.com/ubuntu-ports noble-updates/main arm64 Packages
Description: GNU core utilities
N: There is 1 additional record. Please use the '-a' switch to see it
deb-src 有効化
/usr/bin/echo の提供パッケージが Coreutils であると特定できたので、これをダウンロードするために deb-src を
従来の作法に則って /etc/apt/sources.list を確認したところ、
# Ubuntu sources have moved to the /etc/apt/sources.list.d/ubuntu.sources
# file, which uses the deb822 format. Use deb822-formatted .sources files
# to manage package sources in the /etc/apt/sources.list.d/ directory.とのこと。
とのことでファイルが /etc/apt/sources.list.d/ubuntu.sources に移動し、書式は deb822 に変更されています。
ubuntu.sources のコメントには、
## Types: Append deb-src to enable the fetching of source package.
とあるので Types: deb
を Types: deb deb-src
に変更して deb-src を有効化します。
このハンズオンでは vim
を使ってますが、それ以外のテキストエディタでも大丈夫です。
vim
はディストリビューションに含まれてないと思うので、使う場合は入れておいてください。
root@3fabf328c64a:/# apt install -y vim
root@3fabf328c64a:/# vi /etc/apt/sources.list.d/ubuntu.sources
- Types: deb
+ Types: deb deb-src
URIs: http://ports.ubuntu.com/ubuntu-ports/
Suites: noble noble-updates noble-backports
Components: main universe restricted multiverse
Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg
## Ubuntu security updates. Aside from URIs and Suites,
## this should mirror your choices in the previous section.
- Types: deb
+ Types: deb deb-src
URIs: http://ports.ubuntu.com/ubuntu-ports/
Suites: noble-security
Components: main universe restricted multiverse
Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg
root@3fabf328c64a:~/coreutils-9.4#
deb822 の書式は apt マニュアルの『THE DEB AND DEB-SRC TYPES: GENERAL FORMAT』 に記載があります。
The format for two one-line-style entries using the deb and deb-src types is:
deb [ option1=value1 option2=value2 ] uri suite [component1] [component2] [...] deb-src [ option1=value1 option2=value2 ] uri suite [component1] [component2] [...]
Alternatively the equivalent entry in deb822 style looks like this:
Types: deb deb-src URIs: uri Suites: suite Components: [component1] [component2] [...] option1: value1 option2: value2
deb-src を有効化したのでパッケージリストを更新。
root@3fabf328c64a:/# apt update
...(略)...
Coreutils のソースコードをリポジトリからダウンロード
ルートディレクトリに落とすのは忍びないのでホームディレクトリに移動してダウンロードします。
恐らく apt source
実行時に以下のエラーで怒られると思います。
E: Unpack command 'dpkg-source --no-check -x coreutils_9.4-3ubuntu6.1.dsc' failed.
N: Check if the 'dpkg-dev' package is installed.
怒られたら Notice に従って dpkg-dev
をインストールしましょう。
root@3fabf328c64a:/# apt install -y dpkg-dev
root@3fabf328c64a:/# cd ~
root@3fabf328c64a:~# apt source coreutils
Reading package lists... Done
Need to get 6023 kB of source archives.
Get:1 http://ports.ubuntu.com/ubuntu-ports noble-updates/main coreutils 9.4-3ubuntu6.1 (dsc) [2030 B]
Get:2 http://ports.ubuntu.com/ubuntu-ports noble-updates/main coreutils 9.4-3ubuntu6.1 (tar) [5979 kB]
Get:3 http://ports.ubuntu.com/ubuntu-ports noble-updates/main coreutils 9.4-3ubuntu6.1 (diff) [41.3 kB]
Fetched 6023 kB in 2s (2774 kB/s)
dpkg-source: info: extracting coreutils in coreutils-9.4
dpkg-source: info: unpacking coreutils_9.4.orig.tar.xz
dpkg-source: info: unpacking coreutils_9.4-3ubuntu6.1.debian.tar.xz
dpkg-source: info: using patch list from debian/patches/series
dpkg-source: info: applying 72_id_checkngroups.patch
dpkg-source: info: applying cp-n.diff
dpkg-source: info: applying 80_fedora_sysinfo.patch
dpkg-source: info: applying 99_float_endian_detection.patch
dpkg-source: info: applying treat-devtmpfs-and-squashfs-as-dummy-filesystems.patch
dpkg-source: info: applying tail-fix-tailing-sysfs-files-where-PAGE_SIZE-BUFSIZ.patch
dpkg-source: info: applying CVE-2024-0684.patch
dpkg-source: info: applying suppress-permission-denied-errors-on-nfs.patch
W: Download is performed unsandboxed as root as file 'coreutils_9.4-3ubuntu6.1.dsc' couldn't be accessed by user '_apt'. - pkgAcquire::Run (13: Permission denied)
ls
でダウンロード結果を確認。
なおダウンロード時に Permission denied
の警告が出ていましたが目的のものはダウンロードできているので今回は無視します。
root@3fabf328c64a:~# ls
coreutils-9.4 coreutils_9.4-3ubuntu6.1.debian.tar.xz coreutils_9.4-3ubuntu6.1.dsc coreutils_9.4.orig.tar.xz
先ほど確認した apt show coreutils
に Version: 9.4-3ubuntu6.1
とあったように、ディレクトリ名は coreutils-9.4 で作成されています。
なお GNU Coreutils のホームページを見たら執筆時点の最新版は 9.7 でした
Coreutils のソースを確認
早速中身を見ていきましょう。
なおここから先は GNU Coreutils プロジェクトの世界のお話です。(GNU プロジェクトの Coreutils サブプロジェクトって言った方が正しいかも)
root@3fabf328c64a:~# cd coreutils-9.4/
目的の echo
のソースは src/echo.c です。
root@3fabf328c64a:~/coreutils-9.4# wc -l src/echo.c
277 src/echo.c
Version 9.4 時点で 277 行。
個人的な感想:
echo
ってロジック自体がシンプルなのでソースコード自体のボリュームもコンパクトで、コマンドのオプション引数の実装の仕方とかを参考にする上で見やすくて教材としてすごく良いプログラムだと思ってます。
余談ですが src 配下の .c
extension のファイルを眺めて見ると Coreutils 全体のボリューム感とか知れて面白いですよ。
↓例えばこんな感じとか。
root@3fabf328c64a:~/coreutils-9.4# wc -l src/*.c | sort -nr
echo
コマンド改造 Step 1: 引数の出力ごとに0.1秒待つ
vim
で編集します。
root@3fabf328c64a:~/coreutils-9.4# vi src/echo.c
300行に満たないとは言え全文載せると長くなるのと、数行程度の書き換えなので抜粋します。
#include <config.h>
#include <stdio.h>
#include <sys/types.h>
#include "system.h"
#include "assure.h"
+ #include <unistd.h>
just_echo:
if (do_v9 || posixly_correct)
{
//
// 省略
//
}
else
{
while (argc > 0)
{
+ usleep(0.1 * 1000 * 1000);
fputs (argv[0], stdout);
+ fflush(stdout);
argc--;
argv++;
if (argc > 0)
putchar (' ');
}
}
if (display_return)
putchar ('\n');
return EXIT_SUCCESS;
インストール
INSTALL を参照して手順を確認します。
root@3fabf328c64a:~/coreutils-9.4# head -n 13 INSTALL
Installation Instructions
*************************
Basic Installation
==================
The following shell commands:
test -f configure || ./bootstrap
./configure
make
make install
test
コマンド実行。
root@3fabf328c64a:~/coreutils-9.4# test -f configure || ./bootstrap
次は configure
を実行。
ですがエラーが発生しました。
root@3fabf328c64a:~/coreutils-9.4# ./configure
...(略)...
checking whether mknod can create fifo without root privileges... configure: error: in '/root/coreutils-9.4':
configure: error: you should not run configure as root (set FORCE_UNSAFE_CONFIGURE=1 in environment to bypass this check)
See 'config.log' for more details
エラーメッセージに従って環境変数を設定。
root@3fabf328c64a:~/coreutils-9.4# export FORCE_UNSAFE_CONFIGURE=1
文脈と環境変数名からして本来的にはroot権限じゃなくてユーザ権限で実行することを想定してるはず。
ただし今回はコンテナ環境なのでこのあたりのケアを気にせず進めます。
というわけで、もう一回 configure
を実行。
root@3fabf328c64a:~/coreutils-9.4# ./configure
...(略)...
今度はエラー発生せずに終了しました。
make
を実行。
そしてまたエラー。
root@3fabf328c64a:~/coreutils-9.4# make
cd . && /bin/bash /root/coreutils-9.4/build-aux/missing automake-1.16 --gnu Makefile
/root/coreutils-9.4/build-aux/missing: line 81: automake-1.16: command not found
WARNING: 'automake-1.16' is missing on your system.
You should only need it if you modified 'Makefile.am' or
'configure.ac' or m4 files included by 'configure.ac'.
The 'automake' program is part of the GNU Automake package:
<https://www.gnu.org/software/automake>
It also requires GNU Autoconf, GNU m4 and Perl in order to run:
<https://www.gnu.org/software/autoconf>
<https://www.gnu.org/software/m4/>
<https://www.perl.org/>
make: *** [Makefile:8842: Makefile.in] Error 127
お次は automake-1.16
が無いとのこと。
なので入れます。
root@3fabf328c64a:~/coreutils-9.4# apt install -y automake
...(略)...
入れました。
make
リトライ。
root@3fabf328c64a:~/coreutils-9.4# make
...(略)...
今度はエラー発生せずに終了しました。
最後に make install
実行。
root@3fabf328c64a:~/coreutils-9.4# make install
インストール終了しました。
make[3]
の結果を見るに、インストール先は /usr/local/bin/ 配下です。
make[3]: Entering directory '/root/coreutils-9.4'
/usr/bin/mkdir -p '/usr/local/bin'
src/ginstall -c src/ginstall '/usr/local/bin/./install'
src/ginstall -c src/chroot src/hostid src/timeout src/nice src/who src/users src/pinky src/stty src/df src/stdbuf src/[ src/b2sum src/base64 src/base32 src/basenc src/basename src/cat src/chcon src/chgrp src/chmod src/chown src/cksum src/comm src/cp src/csplit src/cut src/date src/dd src/dir src/dircolors src/dirname src/du src/echo src/env src/expand src/expr src/factor src/false src/fmt src/fold src/groups src/head src/id src/join src/kill src/link src/ln src/logname src/ls src/md5sum src/mkdir src/mkfifo src/mknod src/mktemp src/mv src/nl src/nproc src/nohup src/numfmt src/od src/paste src/pathchk src/pr src/printenv src/printf src/ptx src/pwd src/readlink src/realpath src/rm src/rmdir src/runcon src/seq src/sha1sum src/sha224sum src/sha256sum src/sha384sum src/sha512sum src/shred src/shuf src/sleep src/sort src/split src/stat src/sum src/sync src/tac src/tail src/tee src/test src/touch src/tr src/true src/truncate src/tsort src/tty src/uname src/unexpand src/uniq src/unlink src/uptime src/vdir src/wc src/whoami src/yes '/usr/local/bin'
/usr/bin/mkdir -p '/usr/local/libexec/coreutils'
src/ginstall -c src/libstdbuf.so '/usr/local/libexec/coreutils'
実行してみる
引数単位でちゃんとスリープしてくれています。
echo
コマンド改造 Step 2: 1文字出力するごとに0.1秒待つ
root@3fabf328c64a:~/coreutils-9.4# vi src/echo.c
just_echo:
の else
配下のみ抜粋
just_echo:
if (do_v9 || posixly_correct)
{
//
// 省略
//
}
else
{
while (argc > 0)
{
- usleep(0.1 * 1000 * 1000);
- fputs (argv[0], stdout);
- fflush(stdout);
+ char const *s = argv[0];
+ unsigned char c;
+ while ((c = *s++))
+ {
+ usleep(0.1 * 1000 * 1000);
+ putchar (c);
+ fflush(stdout);
+ }
argc--;
argv++;
if (argc > 0)
putchar (' ');
}
}
サクッとキャッシュ削除 & 再コンパイル
root@3fabf328c64a:~/coreutils-9.4# make clean && make distclean && test -f configure || ./bootstrap && ./configure && make && make install
実行してみる
完成です。