読者です 読者をやめる 読者になる 読者になる

情弱ログ

参考にならないので当てにしないでください

カーネルからリンクダウン時のイベントを飛ばす処理を探す

あるインタフェースのリンク状態を確認するために、netlinkソケットを使ってカーネルからメッセージを受信するサンプルがnetlink(7)に載っています。

struct sockaddr_nl sa;

memset(&sa, 0, sizeof(sa));
sa.nl_family = AF_NETLINK;
sa.nl_groups = RTMGRP_LINK | RTMGRP_IPV4_IFADDR;

fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE);
bind(fd, (struct sockaddr *) &sa, sizeof(sa));

このRTMGRP_LINKなパケットがどこから飛んでくるのかを調べたのでメモ。
まず、ag(the silver searcher)でLinuxカーネル(linux-4.4.26-gentoo)をgrepすると以下の結果が得られました。

[vicco@localhost] ~
% ag RTMGRP_LINK /usr/src/linux 
/usr/src/linux/include/uapi/linux/rtnetlink.h
572:#define RTMGRP_LINK		1

/usr/src/linux/Documentation/networking/operstates.txt
25:operation RTM_GETLINK. It is also possible to subscribe to RTMGRP_LINK
136:are multicasted on the netlink group RTMGRP_LINK.
140:-subscribe to RTMGRP_LINK

(*´・ω・`).o0(使えねぇ…)

とりあえず関係しそうなコードは${KERNEL}/net/core辺りにあること、リンクダウン時はドライバで何かしらの処理がありそうだということをカーネルに精通した先輩から聞いているので、その辺りから探してみます。
まず、${KERNEL}/drivers/net/ethernet/intel/e1000e/から"down"でgrepをかけて、引っかかったnetdev.cのe1000e_down()とかいう臭そうなところから見ていきます。EmacsならM-x grep-findすればエディタ内でgrepできて便利です(ステマ)。

/**
 * e1000e_down - quiesce the device and optionally reset the hardware
 * @adapter: board private structure
 * @reset: boolean flag to reset the hardware or not
 */
void e1000e_down(struct e1000_adapter *adapter, bool reset)
{
...
	/* signal that we're down so the interrupt handler does not
	 * reschedule our watchdog timer
	 */
	set_bit(__E1000_DOWN, &adapter->state);

	netif_carrier_off(netdev);

	/* disable receives in the hardware */
	rctl = er32(RCTL);
	if (!(adapter->flags2 & FLAG2_NO_DISABLE_RX))
		ew32(RCTL, rctl & ~E1000_RCTL_EN);
	/* flush and sleep below */

	netif_stop_queue(netdev);
...

netif_carrier_off()がそれっぽい。ググったらLinux Cross Referenceとかいうサイトから${KERNEL}/net/sched/sch_generic.cにあることが分かったので、見てみると以下の様な感じになっていました。(ところでカーネル開発者の人ってタグジャンプを使ってるんでしょうか。make TAGSでタグファイルを生成できるので少なからず使ってる人はいるんでしょうが、タグファイルを開くだけで1分以上待たされますし、メモリを滅茶苦茶食う上にもっさりとジャンプするので、諦めてググってからファイルを開いてます。ただのスペ貧では)

/**
 *	netif_carrier_off - clear carrier
 *	@dev: network device
 *
 * Device has detected loss of carrier.
 */
void netif_carrier_off(struct net_device *dev)
{
	if (!test_and_set_bit(__LINK_STATE_NOCARRIER, &dev->state)) {
		if (dev->reg_state == NETREG_UNINITIALIZED)
			return;
		atomic_inc(&dev->carrier_changes);
		linkwatch_fire_event(dev);
	}
}
EXPORT_SYMBOL(netif_carrier_off);

linkwatch_fire_event()、臭いっすね。${KERNEL}/net/core/link_watch.cにあるらしいので見てみます。

void linkwatch_fire_event(struct net_device *dev)
{
	bool urgent = linkwatch_urgent_event(dev);

	if (!test_and_set_bit(__LINK_STATE_LINKWATCH_PENDING, &dev->state)) {
		linkwatch_add_event(dev);
	} else if (!urgent)
		return;

	linkwatch_schedule_work(urgent);
}
EXPORT_SYMBOL(linkwatch_fire_event);

linkwatch_add_eventを見てみます。同じファイル内にあるので、Emacsならsemanticを入れておけばC-c , jでその関数にジャンプできて便利です(ステマ)。

static void linkwatch_add_event(struct net_device *dev)
{
	unsigned long flags;

	spin_lock_irqsave(&lweventlist_lock, flags);
	if (list_empty(&dev->link_watch_list)) {
		list_add_tail(&dev->link_watch_list, &lweventlist);
		dev_hold(dev);
	}
	spin_unlock_irqrestore(&lweventlist_lock, flags);
}

lweventlistというリストに突っ込んでいることが分かりました。list_add_tailとかはググったら以下のサイトに詳しく書いてありました。カーネル初心者なので基本的なところが日本語で書いてあると安心します。
list_head構造体 - Linuxの備忘録とか・・・(目次へ)

とりあえずenqueueしてそうだな、ということが分かったので、どこかでキューを処理している関数があるはず。linkwatch_fire_event()に戻って、今度はlinkwatch_schedule_work()を見てみます。

static void linkwatch_schedule_work(int urgent)
{
...
	/* Minimise down-time: drop delay for up event. */
	if (urgent) {
		if (test_and_set_bit(LW_URGENT, &linkwatch_flags))
			return;
		delay = 0;
	}
...
	/*
	 * If urgent, schedule immediate execution; otherwise, don't
	 * override the existing timer.
	 */
	if (test_bit(LW_URGENT, &linkwatch_flags))
		mod_delayed_work(system_wq, &linkwatch_work, 0);
	else
		schedule_delayed_work(&linkwatch_work, delay);
...
}

引数のurgentがtrueならlinkwatch_flagsにアトミックにLW_URGENTビットを立てて、フラグを元に下のif文で分岐しているらしい。なんで下のif文でif(urgent)って書いたら駄目なの?とかはよく分かりません。多分同時に呼ばれると困るんだと勝手に思い込んでます。
遅延処理はググったら出てきたこのサイトが詳しかったです。ちなみにこれは学部の授業の資料っぽいです。あなおそろしや。
割り込みの後半部、Softirq、Tasklet、Work Queue
上の方でstatic DECLARE_DELAYED_WORK(linkwatch_work, linkwatch_event);という定義があったので、linkwatch_eventを探すとmutexロックして__linkwatch_run_queue()といういかにもキューを処理していそうな関数を呼び出しているので見てみます。

static void __linkwatch_run_queue(int urgent_only)
{
...
	LIST_HEAD(wrk);
...
	list_splice_init(&lweventlist, &wrk);

	while (!list_empty(&wrk)) {

		dev = list_first_entry(&wrk, struct net_device, link_watch_list);
		list_del_init(&dev->link_watch_list);

		if (urgent_only && !linkwatch_urgent_event(dev)) {
			list_add_tail(&dev->link_watch_list, &lweventlist);
			continue;
		}
		spin_unlock_irq(&lweventlist_lock);
		linkwatch_do_dev(dev);
		spin_lock_irq(&lweventlist_lock);
	}
...

wrkリストという新しいリストにlweventlistを引っ付けて、wrkリストに対し処理しているようです。
list_splice_init
lweventlistを直接処理しないのはurgent_onlyな処理のためなんですかね。とりあえず中を見てみるとlinkwatch_do_dev()というのが重要そうなので見てみます。

static void linkwatch_do_dev(struct net_device *dev)
{
	/*
	 * Make sure the above read is complete since it can be
	 * rewritten as soon as we clear the bit below.
	 */
	smp_mb__before_atomic();

	/* We are about to handle this device,
	 * so new events can be accepted
	 */
	clear_bit(__LINK_STATE_LINKWATCH_PENDING, &dev->state);

	rfc2863_policy(dev);
	if (dev->flags & IFF_UP) {
		if (netif_carrier_ok(dev))
			dev_activate(dev);
		else
			dev_deactivate(dev);

		netdev_state_change(dev);
	}
	dev_put(dev);
}

netdev_state_change()を見ていきます。${KERNEL}/net/core/dev.cにあります。

/**
 *	netdev_state_change - device changes state
 *	@dev: device to cause notification
 *
 *	Called to indicate a device has changed state. This function calls
 *	the notifier chains for netdev_chain and sends a NEWLINK message
 *	to the routing socket.
 */
void netdev_state_change(struct net_device *dev)
{
	if (dev->flags & IFF_UP) {
		struct netdev_notifier_change_info change_info;

		change_info.flags_changed = 0;
		call_netdevice_notifiers_info(NETDEV_CHANGE, dev,
					      &change_info.info);
		rtmsg_ifinfo(RTM_NEWLINK, dev, 0, GFP_KERNEL);
	}
}
EXPORT_SYMBOL(netdev_state_change);

IFF_UPはインタフェースが動作中であることを示すらしいです。この時点ではまだtrueなのかな。rtmsg_ifinfo()を追うために${KERNEL}/net/core/rtnetlink.cを見ていきます。

void rtmsg_ifinfo(int type, struct net_device *dev, unsigned int change,
		  gfp_t flags)
{
	struct sk_buff *skb;

	if (dev->reg_state != NETREG_REGISTERED)
		return;

	skb = rtmsg_ifinfo_build_skb(type, dev, change, flags);
	if (skb)
		rtmsg_ifinfo_send(skb, dev, flags);
}
EXPORT_SYMBOL(rtmsg_ifinfo);

同ファイルからrtmsg_ifinfo_send()を見てみます。

void rtmsg_ifinfo_send(struct sk_buff *skb, struct net_device *dev, gfp_t flags)
{
	struct net *net = dev_net(dev);

	rtnl_notify(skb, net, 0, RTNLGRP_LINK, NULL, flags);
}

RTNLGRP_LINKは${KERNEL}/include/uapi/linux/rtnetlink.hで以下のように定義されています。

/* RTnetlink multicast groups */
enum rtnetlink_groups {
	RTNLGRP_NONE,
#define RTNLGRP_NONE		RTNLGRP_NONE
	RTNLGRP_LINK,
#define RTNLGRP_LINK		RTNLGRP_LINK
	RTNLGRP_NOTIFY,
#define RTNLGRP_NOTIFY		RTNLGRP_NOTIFY
...
	__RTNLGRP_MAX	__RTNLGRP_MAX
};
#define RTNLGRP_MAX	(__RTNLGRP_MAX - 1)

あれ…RTMGRP_LINKは…?という疑問なんですが、同じくrtnetlink.hには#define RTMGRP_LINK 1という定義があり、RTNLGRP_LINKも1であることから、別名がついてるだけなのかな?というあやふやな感じです。別名がついているにしては、その下のRTNLGRP_NEIGHは3なのに対し、RTMGRP_NEIGHは4だったりするので正直分かってません。何なんだろう…

以上、煮え切らない感じで終わってしまいましたが、カーネルからnetlinkでRTM_NEWLINKをユーザランドに飛ばすまでの処理を見てきました。この後、ユーザランドでは以下のサイトのように、netlinkのメッセージのタイプがRTM_NEWLINKかつ、インタフェースのフラグのIFF_UPビットがtrueならup、falseならdownといった処理でインタフェースの状態を確認できます。(IFF_UPフラグ、どこでfalseにしたんだろう…)
notifications - How to get notified about network interface changes with Netlist and RTMGRP_LINK signal? - Stack Overflow

1年生に送るデバッガ(gdb)の使い方(中級編)

前回の記事でコアダンプからセグメンテーション違反の原因を探す方法を書きました。
今回はデバッガからプログラムを実行することで、実行中のプログラムを一時停止したり変数に何が入っているかを確認してみましょう。
突然ですが、以下のプログラムには重大なバグがあります。原因が分かるでしょうか?
1年生向けと題打っておきながらまだ習ってないことがたくさん出てきますが、今年度中にやると思うのでおいおい見ていってください。

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

struct list_t {
	int value;
	struct list_t* next;
};

/*
 * 指定されたアドレスからlengthバイトまでを16進数でダンプする
 */
void debug(void *addr, int length) {
	int i;
	for ( i = 0; i < length; i++ ) {
		printf("%02x ", *(unsigned char *)(addr + i));
	}
	printf("\n");
}

/*
 * i番目のノードのvalueにiが入ったlength個のノードからなるリストを作る
 */
struct list_t* make_list(int length) {
	int i;
	struct list_t *head, *node, *tmp;

	head = node = malloc(sizeof(*node));
	node->value = 0;

	for ( i = 1; i < length; i++ ) {
		tmp = malloc(sizeof(*tmp));
		tmp->value = i;
		node->next = tmp;
		node = node->next;
	}
	return head;
}

/*
 * 後にmallocした領域を意図的に汚すための処理
 */
void pollute_mem(void) {
	struct list_t *head, *tmp;

	head = make_list(16);

	for ( tmp = head; tmp != NULL; tmp = tmp->next ) {
		free(tmp);
	}
}

int main(void) {
	struct list_t *head, *tmp;

	pollute_mem();

	// 16個のリストを作る
	head = make_list(16);

	for ( tmp = head; tmp != NULL; tmp = tmp->next ) {
		// ノードのアドレスとその中身をダンプする
		printf("------%p\n", tmp);
		debug(tmp, sizeof(struct list_t));

		printf("%d\n", tmp->value);
		// 無限ループだと早すぎるので1秒待つ
		sleep(1);
	}
	return 0;
}

コンパイルして実行してみましょう。ちなみに環境によって動作は異なります。僕の環境ではgcc 4.9.3でコンパイルし、x86_64なマシンで動くLinux 4.4.6-gentooカーネル上で実行しています。

$ ./a.out
------0x6021f0
00 00 00 00 00 00 00 00 d0 21 60 00 00 00 00 00
0
------0x6021d0
01 00 00 00 00 00 00 00 b0 21 60 00 00 00 00 00
1
------0x6021b0
02 00 00 00 00 00 00 00 90 21 60 00 00 00 00 00
2
------0x602190
03 00 00 00 00 00 00 00 70 21 60 00 00 00 00 00
3
------0x602170
04 00 00 00 00 00 00 00 50 21 60 00 00 00 00 00
4
------0x602150
05 00 00 00 00 00 00 00 30 21 60 00 00 00 00 00
5
------0x602130
06 00 00 00 00 00 00 00 10 21 60 00 00 00 00 00
6
------0x602110
07 00 00 00 00 00 00 00 f0 20 60 00 00 00 00 00
7
------0x6020f0
08 00 00 00 00 00 00 00 d0 20 60 00 00 00 00 00
8
------0x6020d0
09 00 00 00 00 00 00 00 b0 20 60 00 00 00 00 00
9
------0x6020b0
0a 00 00 00 00 00 00 00 90 20 60 00 00 00 00 00
10
------0x602090
0b 00 00 00 00 00 00 00 70 20 60 00 00 00 00 00
11
------0x602070
0c 00 00 00 00 00 00 00 50 20 60 00 00 00 00 00
12
------0x602050
0d 00 00 00 00 00 00 00 30 20 60 00 00 00 00 00
13
------0x602030
0e 00 00 00 00 00 00 00 10 20 60 00 00 00 00 00
14
------0x602010
0f 00 00 00 00 00 00 00 30 20 60 00 00 00 00 00
15
------0x602030
0e 00 00 00 00 00 00 00 10 20 60 00 00 00 00 00
14
------0x602010
0f 00 00 00 00 00 00 00 30 20 60 00 00 00 00 00
15
以下延々とループ

といった結果になったでしょうか?人によってはセグメンテーション違反となり、また別の人は特にエラーもなく15まで数字が表示されて正常終了したかもしれません。
このバグの原因をデバッガを使って探していきましょう。以下のコマンドを実行し、gdb上でプログラムを実行する準備をしてください。

$ gcc -g seglist.c
$ gdb ./a.out
~略~
Reading symbols from a.out...done.
(gdb)

では、gdbの使い方について説明します。gdbは実行中のプロセスをある段階で一時停止し、その時の変数やレジスタなどを表示することができます。このため、まず一時停止する場所を指定する必要があります。今回はまずmain関数の入り口で止めてみましょう。場所の指定はbreakコマンドを使います。

(gdb) break main
Breakpoint 1 at 0x400788: file seglist.c, line 56.

一時停止する場所を指定できました。ちなみに、この場所はブレークポイントと呼ばれています。また、ブレークポイントは行番号や相対位置で指定することも可能ですし、ある条件を満たした時だけ(例えばある変数が10の時などに)停止させることも可能です。
ブレークポイントを指定したので、プログラムを実行してみましょう。runコマンドでプログラムを実行できます。

(gdb) run
Starting program: /home/vicco/Scripts/a.out

Breakpoint 1, main () at seglist.c:56
56		pollute_mem();

main関数の最初の変数定義等は飛ばされます。これはデバッガの内部ではC言語ではなく機械語で解釈されているためです(多分)。
止めておいて何ですが、この時点ではまだ臭い部分は見当たらないので次に進めてみましょう。ステップ実行という機能を使います。

(gdb) step
pollute_mem () at seglist.c:46
46		head = make_list(16);

main関数からpollute_mem関数に入り、最初の処理でまた停止しました。どんどん進めて行きましょう。

(gdb) s
make_list (length=16) at seglist.c:28
28		head = node = malloc(sizeof(*node));
(gdb) s
29		node->value = 0;
(gdb) s
31		for ( i = 1; i < length; i++ ) {
(gdb) s
32			tmp = malloc(sizeof(*tmp));
(gdb) s
33			tmp->value = i;
(gdb) s
34			node->next = tmp;
(gdb) s
35			node = node->next;
(gdb) s
31		for ( i = 1; i < length; i++ ) {

言い忘れていましたが、ステップ実行はsと省略できます。他にもバックトレースはbt、ブレークポイントはbと略せます。さて、pollute_mem関数からmake_list関数に入ったのはいいのですが、forループに入ってしまいました。長ったらしいのでここは飛ばしたいと思います。untilコマンドでループを飛ばすことができます。

(gdb) until
37		return head;

ループを抜けました。どんどん進めて行きましょう。

(gdb) s
38	}
(gdb) s
pollute_mem () at seglist.c:48
48		for ( tmp = head; tmp != NULL; tmp = tmp->next ) {
(gdb) s
49			free(tmp);
(gdb) s
48		for ( tmp = head; tmp != NULL; tmp = tmp->next ) {
(gdb) until
51	}
(gdb) s
main () at seglist.c:59
59		head = make_list(16);

同じ関数を見るのは面倒くさいですね。飛ばしましょう。next(n)コマンドで関数に入らず次の処理に進めます。

(gdb) n
61		for ( tmp = head; tmp != NULL; tmp = tmp->next ) {

ガンガン飛ばしまくってますが、このあたりで変数の状態を見てみましょう。print(p)コマンドで変数の中身を見ることができます。

(gdb) p head
$1 = (struct list_t *) 0x6021f0
(gdb) p *head
$2 = {value = 0, next = 0x6021d0}

printコマンドの結果から、headは0x6021f0というアドレスを保持しており、head->value=0、head->next=0x6021d0ということが分かります。このように構造体も簡単に見ることができます。便利ですね。
では、少し進めてみましょう。

(gdb) s
63			printf("------%p\n", tmp);
(gdb) s
------0x6021f0
64			debug(tmp, sizeof(struct list_t));
(gdb) n
00 00 00 00 00 00 00 00 d0 21 60 00 00 00 00 00
66			printf("%d\n", tmp->value);
(gdb) s
0
68			sleep(1);
(gdb) s
61		for ( tmp = head; tmp != NULL; tmp = tmp->next ) {

ループが一周したのでリストの次のノードの中身を見てみましょう。

(gdb) p tmp
$1 = (struct list_t *) 0x6021f0
(gdb) p *tmp
$2 = {value = 0, next = 0x6021d0}
(gdb) s
63			printf("------%p\n", tmp);
(gdb) p tmp
$3 = (struct list_t *) 0x6021d0
(gdb) p *tmp
$4 = {value = 1, next = 0x6021b0}

この処理をループの回数だけやるのも面倒くさいですよね。変数が書き換わった時だけ実行を一時停止してみましょう。watchコマンドを使うと変数(メモリ)を監視することができます。また、監視対象をウォッチポイントと呼びます。

(gdb) watch tmp
Hardware watchpoint 2: tmp
(gdb) c
Continuing.
------0x6021d0
01 00 00 00 00 00 00 00 b0 21 60 00 00 00 00 00
1
Hardware watchpoint 2: tmp

Old value = (struct list_t *) 0x6021d0
New value = (struct list_t *) 0x6021b0
0x00000000004007fe in main () at seglist.c:61
61		for ( tmp = head; tmp != NULL; tmp = tmp->next ) {
(gdb) p *tmp
$5 = {value = 2, next = 0x602190}

途中でcontinue(c)コマンドを使って一気に実行しています。continueコマンドはブレークポイントやプログラムの終了まで実行を進めるコマンドです。watchコマンドでtmpを監視しているので、次のノードにtmpが差し替わったところで実行が一時停止しました。
先程のエラーの内容を思い出すとループの14回目から15回目あたりで異常が発生していました。そこまで進めましょう。continueコマンドを連打しました。

~略~
(gdb) c
Continuing.
~略~
(gdb) p *tmp
$6 = {value = 15, next = 0x602030}
(gdb) c
Continuing.
~略~
(gdb) p *tmp
$7 = {value = 14, next = 0x602010}
(gdb) c
Continuing.
~略~
(gdb) p *tmp
$8 = {value = 15, next = 0x602030}

14、15と続き、次はNULLであるはずが14になっていますね。つまりリストが循環してしまっています。main関数内にそんな処理は書いていないので、どうやらmake_list関数に問題がありそうです。一旦全てのブレークポイントを削除し、make_list関数にブレークポイントを設定してやり直しましょう。ブレークポイントの削除はdeleteコマンドで行なえます。deleteコマンドに引数を与えない場合、全てのブレークポイントが削除されます。また、プロセスの再起動は起動時と同じくrunコマンドで行なえます。

(gdb) delete
Delete all breakpoints? (y or n) y
(gdb) break make_list
Breakpoint 3 at 0x4006d5: file seglist.c, line 28.
(gdb) info break
Num     Type           Disp Enb Address            What
3       breakpoint     keep y   0x00000000004006d5 in make_list at seglist.c:28
(gdb) r
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /home/vicco/Scripts/a.out

Breakpoint 3, make_list (length=16) at seglist.c:28
28		head = node = malloc(sizeof(*node));

では、nodeにウォッチポイントを設定して実行を続けましょう。

(gdb) c
Continuing.
~略~
(gdb) p *node
$10 = {value = 1, next = 0x0}
~略~
(gdb) c
Continuing.
(gdb) p *node
$15 = {value = 15, next = 0x0}
(gdb) c
Continuing.

Watchpoint 4 deleted because the program has left the block in
which its expression is valid.
0x0000000000400751 in pollute_mem () at seglist.c:46
46		head = make_list(16);

特に問題はなさそうですが、make_list関数の処理が終わってしまいました。しかし、make_list関数はまた呼び出されるはずなので、とりあえず実行を進めてみましょう。

(gdb) c
Continuing.
Breakpoint 3, make_list (length=16) at seglist.c:28
28		head = node = malloc(sizeof(*node));

とりあえず中身を見てみましょうか。

(gdb) p *node
$16 = {value = -263877816, next = 0xf07d8348f0458948}

  _, ._
(;゚ Д゚) …?!
何だかえらいことになってますね。ウォッチポイントを設定してどんどん見ていきましょう。

(gdb) watch node
Hardware watchpoint 5: node
(gdb) c
Continuing.
~略~
(gdb) p *node
$2 = {value = 6300096, next = 0x0}
(gdb) c
Continuing.
(gdb) p *node
$3 = {value = 1, next = 0x6021f0}

先ほどとの違いが分かりますか?最初のmake_listの処理ではどのノードのnextも初期値は0x0だったのに対し、何故か今回は変な値が既に格納されています。
茶番なので種明かしをすると、このプログラムはmallocした領域をゼロクリア(zero fill)していません。最近のOSではセキュリティの観点からヒープ領域をゼロクリアして返すため、一回目はうまくいきます。しかし、同じプロセスが何度もmallocした場合、過去にfreeしたメモリ領域をゼロクリアせずに再利用するためこのようなバグが発生します。今回は偶然15個目のノードのnext領域に14個目のノードのアドレスが格納されていたため、無限ループになっていました。比較的バグの箇所が分かりづらく、また環境によっては成功するため自分の環境では動くのに先生のPCだと動かない…といった事態を招く厄介なバグでした。
このバグはリストの終端処理を行っていないため発生しています。このため、確保時にnextをNULLで初期化する、mallocした領域は全てゼロクリアする、callocを使いノードをゼロクリアした状態で確保する、for文中で終端となるノードを適当な変数で保持し、for文が終わった後に終端ノードのnextにNULLを書き込むといった対策でこのバグを解消できます。
長くなりましたが、これくらいできればゴリゴリデバッグできるのではないでしょうか。自分は上級編を書けるほどgdbを使いこなしていないので、ネタが溜まったら次回の記事を書く予定です。

1年生に送るデバッガ(gdb)の使い方(入門編)

長い夏休みが終わって、1年生はプログラミングⅡを履修すると思います。
プログラミングⅡではポインタ、ファイル入出力といった新しい概念を学んでいくことになります。
しかし、これらの発展的な内容は便利であると共にエンバグしやすい部分でもあります。

例えば「segmentation faultって出てプログラムが止まった…」「2回目以上呼び出した時の関数の挙動が怪しい…」といったバグを埋め込んだ時、ソースコードとにらめっこするかTAを呼んでないでしょうか。
それ、デバッガを使えば解決できるんですよ。

以下のソースコードを見ていきましょう。

#include <stdio.h>

int main(void) {
	int i;
	int *ptr = NULL;

	for ( i = 0; i < 128; i++ ) {
		printf("%d", ptr[i]);
	}
	return 0;
}

見るからにおかしな部分がありますが、コンパイルして実行してみましょう。

$ ./a.out
Segmentation fault: 11

はい。セグメンテーション違反を起こしました。このエラーはプログラムがアクセスしてはいけないメモリにアクセスしようとした際にOSが強制的に実行を停止させるエラーです。
このエラーが出た場合、コンパイルエラーとは異なりどこに原因があるのか分からないので、困り果てる人が多いと思います(流石にこのプログラムであれば分かると思いますが…)。
そこで、以下のようなコマンドを実行します。

$ gcc -g segfault.c
$ ulimit -c unlimited
$ ./a.out
Segmentation fault (core dumped)

同じくセグメンテーション違反を起こしましたが、core dumpedというメッセージが増えました。lsコマンドを実行すると同じディレクトリにcore.xxxxxというファイルがあるのが確認できると思います(macでは/coresというディレクトリにあります)。これがコアダンプです。コアダンプとはあるプロセスの実行時のメモリの状態をファイルに書き出したものです。このコアを使ってデバッガ(gdb)からエラーの原因を探してみましょう。以下のコマンドを実行してください。

$ gdb a.out core.xxxxx
~略~
Core was generated by `./a.out'.
Program terminated with signal SIGSEGV, Segmentation fault.
#0  0x0000000000400583 in main () at segfault.c:8
15			printf("%d", ptr[i]);

といった出力が得られました。ざっくり見るとsegfault.cの8行目のmain関数でセグメンテーション違反が起きたことが分かります。余談ですが、セグメンテーション違反を起こした場合はSISSEGVというシグナルを受け取るんだなぁと覚えておくといつか役に立つかもしれません。このプログラムではptr[i]という(NULL+i)番地にアクセスしようとしたため、セグメンテーション違反を起こして終了しています(そういえば説明無しでptrを配列として使ってますが、配列の糖衣構文とか授業でやりますよね?)。
ちなみに、ある関数の中でセグメンテーション違反を起こした場合、バックトレースによってどの関数から呼び出されたのかを追っていくことができます。以下のプログラムを例にあげます。

#include <stdio.h>

void func1(void) {
        int i;
        int *ptr = NULL;

        for ( i = 0; i < 128; i++ ) {
                printf("%d", ptr[i]);
        }
}

void func2(void) {
        func1();
}

int main(void) {
        func2();
        return 0;
}

これに先ほどと同じ処理を行い、今度はgdbでbacktraceというコマンドを実行します。ちなみにcoreファイルは毎回xxxxxが別の番号になっていると思うので、その都度最新のものを選んでください(ちなみにこの番号は実行時のプロセスのpidです)。

$ gcc -g segfault.c
$ ./a.out
$ gdb a.out core.xxxxx
~略~
Program terminated with signal SIGSEGV, Segmentation fault.
#0  0x0000000000400583 in func1 () at segfault.c:8
8			printf("%d", ptr[i]);
(gdb) backtrace
#0  0x0000000000400583 in func1 () at segfault.c:8
#1  0x00000000004005ab in func2 () at segfault.c:13
#2  0x00000000004005b6 in main () at segfault.c:17
(gdb)

このように、どの関数からどの関数を呼び出し、最終的にどこでエラーが起きたかが分かります。便利ですね。
これで簡単にですがセグメンテーション違反が起きた場合に原因がどこにあるかを探せるようになりました。TAや先生を呼ばなくて済むので捗りますね。
次回はある時点でプログラムを停止して実行中の変数を表示したりする予定です。

x86_64環境でシステムコールを発行する

「1日待ってください。本物のシステムコールをお見せしますよ。」
と言ってしまったので、C言語からインラインアセンブラを使ってシステムコールを実行することになった時のメモです。

「write()やread()は通常の関数ではなくシステムコールである」という説明があり、そんなわけないだろと思って以下のプログラムをコンパイルしてみました。

#include <unistd.h>

int main(void){
	write(1,"hello\n",6);
	return 0;
}

アセンブルすると以下のようになります。

0000000000400506 <main>:
  400506:	55                   	push   %rbp
  400507:	48 89 e5             	mov    %rsp,%rbp
  40050a:	ba 06 00 00 00       	mov    $0x6,%edx
  40050f:	be b4 05 40 00       	mov    $0x4005b4,%esi
  400514:	bf 01 00 00 00       	mov    $0x1,%edi
  400519:	e8 c2 fe ff ff       	callq  4003e0 <write@plt>
  40051e:	b8 00 00 00 00       	mov    $0x0,%eax
  400523:	5d                   	pop    %rbp
  400524:	c3                   	retq
  400525:	66 2e 0f 1f 84 00 00 	nopw   %cs:0x0(%rax,%rax,1)
  40052c:	00 00 00
  40052f:	90                   	nop

x86_64の呼び出し規約はこちら
x86/x86_64関数呼び出しチートシートを書いた | d.sunnyone.org

第一引数を%ediに、第二引数を%esiに、第三引数を%edxに突っ込んでwrite@pltという関数を呼び出しています。
どう見てもソフトウェア割り込みなどは行っていません。
ちなみにwrite@pltは.textセクションではなく.pltセクションというところに設置されており、以下の様なプログラムになっていました。

00000000004003e0 <write@plt>:
  4003e0:	ff 25 0a 05 20 00    	jmpq   *0x20050a(%rip)        # 6008f0 <_GLOBAL_OFFSET_TABLE_+0x18>
  4003e6:	68 00 00 00 00       	pushq  $0x0
  4003eb:	e9 e0 ff ff ff       	jmpq   4003d0 <_init+0x28>

ELFフォーマットなどはよく知らないのですが、.pltセクションは共有オブジェクトの関数などを呼び出すために使うらしいです。よく分かっていません。

これでは本当にシステムコールを呼んだとは言えません。writeというライブラリ関数を呼び出したに過ぎません。
そこで、冒頭の「1日待ってください。本物のシステムコールをお見せしますよ。」に戻ってきます。

本物のシステムコールを実行するために、インラインアセンブラを利用します。
去年参加した熱血シェルコードではint 0x80を用いてシステムコールを発行しました。
今回は環境をx86_64に限定して、syscallを使ってみたいと思います。

コードは以下のようになります。

#include <sys/types.h>

ssize_t my_write(int fd, const void *buf, size_t count){
    ssize_t ret;
    // __NR_write = 1
    asm volatile("movq $1, %%rax\n\t"
                 "syscall\n\t"
                 : "=a" (ret) // a=rax
                 : "D" (fd), "S" (buf), "d" (count) // D=rdi, S=rsi, d=rdx
                 : "memory", "cc", "rcx", "r11"
        );
    return ret;
}

int main(void){
	my_write(1,"hello\n",6);
	return 0;
}

アセンブルします。

00000000004004b6 <my_write>:
  4004b6:	55                   	push   %rbp
  4004b7:	48 89 e5             	mov    %rsp,%rbp
  4004ba:	89 7d ec             	mov    %edi,-0x14(%rbp)
  4004bd:	48 89 75 e0          	mov    %rsi,-0x20(%rbp)
  4004c1:	48 89 55 d8          	mov    %rdx,-0x28(%rbp)
  4004c5:	8b 45 ec             	mov    -0x14(%rbp),%eax
  4004c8:	48 8b 75 e0          	mov    -0x20(%rbp),%rsi
  4004cc:	48 8b 55 d8          	mov    -0x28(%rbp),%rdx
  4004d0:	89 c7                	mov    %eax,%edi
  4004d2:	48 c7 c0 01 00 00 00 	mov    $0x1,%rax
  4004d9:	0f 05                	syscall
  4004db:	48 89 45 f8          	mov    %rax,-0x8(%rbp)
  4004df:	48 8b 45 f8          	mov    -0x8(%rbp),%rax
  4004e3:	5d                   	pop    %rbp
  4004e4:	c3                   	retq

00000000004004e5 <main>:
  4004e5:	55                   	push   %rbp
  4004e6:	48 89 e5             	mov    %rsp,%rbp
  4004e9:	ba 06 00 00 00       	mov    $0x6,%edx
  4004ee:	be 94 05 40 00       	mov    $0x400594,%esi
  4004f3:	bf 01 00 00 00       	mov    $0x1,%edi
  4004f8:	e8 b9 ff ff ff       	callq  4004b6 <my_write>
  4004fd:	b8 00 00 00 00       	mov    $0x0,%eax
  400502:	5d                   	pop    %rbp
  400503:	c3                   	retq
  400504:	66 2e 0f 1f 84 00 00 	nopw   %cs:0x0(%rax,%rax,1)
  40050b:	00 00 00
  40050e:	66 90                	xchg   %ax,%ax

syscallを用いてシステムコールを発行していることが確認できます。
当然実行するとhelloという文字が出力されます。
車輪の再発明にも程が有りますが、本物のシステムコールをお見せすることができました。

ちなみに出力レジスタの"=a"を誤って"=A"と書いてしまい、%edxも破壊されてえらいことになったりしました。気をつけましょう。

参考にしたサイト
大体書き終わった後に見つけたのでしょぼくれてました。破壊されるレジスタも含めて書いてあるのはここくらいでしょうか。
stackoverflow.com
qiita.com

Emacsで保存時に句読点を全角コンマと全角ピリオドに置換する設定

様式か、風習か、日本語で論文を書くときは「、」や「。」は「,」と「.」に置き換えなくてはいけない。
句読点の設定はだいたいどのIMEにもあるメジャーな設定なのでそこまで困ることではないが、グローバルに変更されると他の文章も全て全角コンマ・ピリオドになるためなかなか使い勝手が悪い。
どうせなのでTeX modeの時は保存時に句読点を全角コンマと全角ピリオドに置換してくれるEmacs Lispを書いた。

(defun replace-dot-comma ()
  "s/。/./g; s/、/,/g;する"
  (interactive)
  (let ((curpos (point)))
    (goto-char (point-min))
    (while (search-forward "。" nil t) (replace-match "."))
    
    (goto-char (point-min))
    (while (search-forward "、" nil t) (replace-match ","))
    (goto-char curpos)
    ))

(add-hook 'tex-mode-hook
          '(lambda ()
             (add-hook 'before-save-hook 'replace-dot-comma nil 'make-it-local)
             ))

これで執筆中は何も考えることなく「、」と「。」を使うことができ、保存時には勝手に「,」と「.」に置換してくれる。

今時Emacs上でTeXで論文書くような人はこんなのよりもっと便利なものを知ってそう

ブート時に途中から画面が真っ暗で表示されなくなる時のtips

ブートローダの選択画面やブートの途中までは画面に映るのに、ある途端から画面が真っ暗になったり、バックグラウンドで処理は行われているのに画面が停止した際の解決法。
nvidiaプロプライエタリドライバを使用していることが前提。

原因はカーネルのオプションの選択ミスで、DRMというカーネルオプションを無効にしてドライバを再コンパイルすると直った。
詳しくはこちら NVidia/nvidia-drivers - Gentoo Wiki

DRMというのはDirect Rendering Managerといい、モダンなビデオカードを扱うためのインタフェースとのこと。
これがLinux kernelのものとドライバのもので競合を起こしていたのが原因の様子。

原因というか、.configファイルを消してしまったのが一番痛い。

glibcの野良ビルド方法

ちゃんとINSTALL読もうね案件

glibcは思考停止したまま./configure&&makeするとconfigureでコケる。
エラー内容としては

configure: error: you must configure in a separate build directory

といった出力がされるはず。これは書いてあるとおり、別個に用意したビルド用のディレクトリでconfigureしろという内容。
つまり、tarballを解凍したglibc-2.22というディレクトリ上で./configureするのではなく、新たにglibc-build等のディレクトリを用意し、glibc-buildから../glibc-2.22/configureをするといった具合。
今の例であれば

tar zxvf glibc-2.22.tar.gz
mkdir glibc-build
cd glibc-build
../glibc-2.22/configure

だろうか。

これによってconfigureが通るので、あとはビルド…と思うが、configureの最後で文句を言われる。

*** On GNU/Linux systems the GNU C Library should not be installed into
*** /usr/local since this might make your system totally unusable.
*** We strongly advise to use a different prefix. For details read the FAQ.
*** If you really mean to do this, run configure again using the extra
*** parameter `--disable-sanity-checks'.

configure時にprefixを指定しない場合デフォルトで--prefix=/usr/localが指定されるが、このディレクトリに野良ビルドしたライブラリをインストールするとシステムがぶっ壊れるからおすすめしないよ。といった内容。
無視する場合は--disable-sanity-checksオプションを付けてconfigureすると良い。そうでない人は--prefix=/home/hoge/usr/localなんかを指定すると良い。
configureが終わるとMakefileglibc-build上にできるので、後はmakeするだけ。コンパイルエラーが無ければそのままビルドが通るはず。

ちなみに全てINSTALLに書いてあるのでちゃんと読もうね。