Programming

Jump Label

从gcc 4.5开始,gcc 内嵌汇编开始支持一个叫jump label的东西。说白了,其实就是在gcc 内嵌汇编中支持外面C语言的goto label。不能访问外面C语言的goto label一直以来都是gcc内嵌汇编的一大缺陷。来自Red Hat 的 Richard Henderson向gcc社区提交了这个想法

最初的动机是因为内核要tracer需要这个东西,因为现在的tracer都是静态实现的,类似于:

[c]
if (unlikely(tracer_is_enabled))
trace();
[/c]

这样显然增加了一个额外的 if 开销,每次都要判断是否需要调用trace()。

而如果jump label 实现的话,那么我们就可以用汇编把需要调用trace()的那部分代码放到一个goto label之下,把这个goto label存放到一个单独的 section 里。而当启用或禁用某个tracer时,我们就可以修改里面的代码!换句话说,如果tracer没有启用,它里面放的就是nop指令,而如果tracer开启,那么我们就把一个jmp指令复制到那个位置,让它跳转到那个label从而去调用trace()!

下面就是用jump label来实现的这个功能的最重要的部分:
[c]

define JUMP_LABEL_INITIAL_NOP “.byte 0xe9 nt .long 0nt”

define JUMP_LABEL(key, label)

   do {                                                    
           asm goto("1:"                                   
                   JUMP_LABEL_INITIAL_NOP                  
                    ".pushsection __jump_table,  "a" nt"
                    _ASM_PTR "1b, %l[" #label "], %c0 nt" 
                    ".popsection nt"                      
                    : :  "i" (key) :  : label);            
    } while (0)

[/c]

所谓.pushsection和.popsection就是把当前的section也保存起来,然后建立一个新的section,名字就是在后面指定。可见,所有的label都是放在了__jump_table这个section里,格式固定如下:

[instruction address] [jump target] [tracepoint key]

先前代码的地址也是要保存的,因为jmp需要一个offset。更详细的介绍可以参考Jump Label的内核文档

要完全理解它的原理,你还需要读一下整个patch set,尤其是arch_jump_label_transform()的实现。代码不难理解,而且读起来很有意思。

两个有意思的bug

因为工作的关系,平时都是在和bugzilla打交道。在红帽如此,在之前的Critical-Links也是,这让我感觉开源的公司似乎都是用bugzilla这个东西管理项目bug的。

我见过很多bug,各种各样的,但是像下面这两个还是头一次见,很有趣。

一个是gnome的一个“严重的”bug,gnome 偷走了一位美女的男朋友:

https://bugzilla.gnome.org/show_bug.cgi?id=626593

看来她男朋友是个gnome hacker,受他的影响,这位美女才知道去gnome的bugzilla去报告这个bug。果然geek到家了!

另一个是mozilla上的一个bug,说是要给实习生买几张小床。看来在mozilla实习很累啊,赶上华为了!

https://bugzilla.mozilla.org/show_bug.cgi?id=579522

引号带来的一个问题

可能你也发现了,不少/etc 下面的配置文件其实就是 bash 脚本,用到它们的bash程序都是直接source进来的。这种情况下没什么问题。可以如果你不是source进行来的配置文件,因为格式的原因,而是一行一行read进来的,那就可能有问题了。我们的配置文件格式如下:

OPTION VALUE

直接read一般不会有问题,可有一种情况很特殊,那就是当某个option接受字符串值的时候。字符串值要是不允许带引号吧,可值中有空格就不好办了,虽然read可以处理,但配置文件读起来很奇怪。值要是带引号吧,嘿嘿,read又不好办了,因为引号本身不是值的一部分。这时候你可能会想用sed把引号去掉,仔细一想这个方法行不通,因为引号的情况完全比你想象得要复杂,不信你试试。我的解决方法是用eval:OPTION=eval echo $VALUE,让bash自己处理这该死的引号去好了!

几个有用的 malloc 环境变量

或许除了LDDEBUG等少数几个环境变量,你对glibc的其它环境变量并不熟悉,比如 MALLOC_PERTURB

MALLOCPERTURB 很有用,它的作用是指定用来填充 malloc(3) 所分配的内存的内容,单位是字节。我们知道,malloc(3) 并不会对其所分配的内容进行初始化,所以如果直接使用这部分内存会出错。而 MALLOCPERTURB 就是用来解决这个问题的,你可以用它来填充这些内存,然后一旦使用了未初始化的内存,这部分内存就很容易被识别出来。所以它在某种程度上可以帮助我们检测未初始化的 malloc(3) 内存。

需要注意的是,用来初始化 malloc(3) 内存的值是 MALLOCPERTURB 的二进制取反(也就是~MALLOCPERTURB),但是 0 会取消掉这个功能。通常我们可以这样来设置:

export MALLOCPERTURB=$(($RANDOM % 255 + 1))

另一个是MALLOCCHECK,当设置了它时 glibc 就会使用另外一个 malloc(3) 的实现来检查一些简单的内存错误,比如多次 free(3) 同一个地址,off-by-one 错误等。它有三个值:0表示忽视检测到的错误,1表示打印错误到标准错误输出,2表示检测到错误就马上中止程序。更详细的描述可以参考手册

还有几个和 malloc 内存分配策略相关的环境变量是:

MALLOCMMAP_MAX
MALLOCMMAP_THRESHOLD
MALLOCTOP_PAD
MALLOCTRIM_THRESHOLD

它们分别等价于 mallopt() 的对应参数,可以参考 mallopt() 的手册。更详细的介绍见这里

glibc定义的系统调用

最近有人问道,glibc 中对我们常见的那些系统调用的定义在哪里?比如write(2)recv(2)

这个问题我以前在看glibc的代码时注意到了。我们通常可以直接找到的所谓定义,比如下面这个:

[c]
ssize_t
libc_write (int fd, const void *buf, size_t nbytes)
{
if (nbytes == 0)
return 0;
if (fd < 0)
{
set_errno (EBADF);
return -1;
}
if (buf == NULL)
{
__set_errno (EINVAL);
return -1;
}

set_errno (ENOSYS);
return -1;
}
libc_hidden_def (
libc_write)
stub_warning (write)

weak_alias (libc_write, write)
libc_hidden_weak (write)
weak_alias (
libc_write, write)
[/c]

其实很明显,这并不是真正的定义,至少并不是你想找的那个。这个是什么呢?这个其实是write(2)的一个alias,而且还是weak alias,换句话说也就是,如果一个平台上没有定义自己的write(2),那么就用一个。而且从上面的代码也可以看得出来,这个函数仅仅是处理了一下errno,别的什么都不做。

那真正的定义究竟在哪里?说实话,我当初找到费了一番周折,找到它们并不容易,因为它们是编译时生成的!!可以从下面三个文件中看出来:

sysdeps/unix/make-syscalls.sh
sysdeps/unix/syscalls.list(sysdeps/unix/inet/syscalls.list)
sysdeps/unix/syscall-template.S

syscall-template.S顾名思义是个定义的模板,每个生成的系统调用都要参考这个模板,但是怎么用模板来“刻画”每一个系统调用呢?于是就有了syscalls.list,而make-syscalls.sh就是用模板和那个列表来构建生成系统调用定义的makefile,该makefile最终生成最后的定义。有兴趣的朋友应该仔细看看这几个文件。

现在再想想,这么做其实是有道理的,在Linux下,系统调用的真正定义有很多相似的地方,确实可以通过“模板”来生成对应的汇编,但是否真值得花时间去构建那么抽象的一个模板和框架?我说不清楚,本着“懒惰”的原则确实应该如此,不过看看模板本身似乎原因不仅仅是“懒惰”。

从这里我们也可以看出glibc的代码难读啊,比起Linux内核来,不仅仅是风格的问题,还有就是使用了太多的tricks,导致的结果也很显而易见,参与glibc开发的和参与linux内核开发的人明显不是一个数量级的。

Kimian Self Reproduction

看《GEB》时看到了这个东西,感觉很有意思。《GEB》在第 16 章中提到了这个东西,中文版的把它翻译成了“凯姆式自复制”。其实它类似于Quine)(如果你还不了解这个东西,可以在我的主页上找到我写的Quine),也是打印其本身,不过它是无意义的,也就是说编译器/解释器会报错,而错误信息本身就是它的内容!

在网上搜了搜,发现就这么一个页面介绍Kimian,里面给出的例子除了Perl的那个别的在Linux上都不行。所以我又自己特意写了几个:

zsh版的Kimian:

% zsh: command not found: zsh:
zsh: command not found: zsh:

Python版的Kimian:

% cat kimian.py
File “kimian.py”, line 1
File “kimian.py”, line 1
^
IndentationError: unexpected indent
% python kimian.py
File “kimian.py”, line 1
File “kimian.py”, line 1
^
IndentationError: unexpected indent

另一个Perl版的:

% cat kimian.pl
kimian.pl syntax OK
% perl -X -c kimian.pl
kimian.pl syntax OK

C版的Kimian:

% cat kimian.c
kimian.c:1: error: expected ‘=’, ‘,’, ‘;’, ‘asm’ or ‘attribute‘ before ‘.’ token
% gcc -w -c kimian.c
kimian.c:1: error: expected ‘=’, ‘,’, ‘;’, ‘asm’ or ‘attribute’ before ‘.’ token

有兴趣的朋友可以自己尝试去写写。;)

Integer format conversion

我们知道 C99 中引入了uint32_t,uintmax_t 这样的标准整数类型,可有一个随之而来的问题,那就是它们在 printf()/scanf() 中对应的是 format conversion 该用什么呢?

其实这个问题由来已久,比如之前就有的标准类型:ptrdiff_t,printf()有专门的一个 “t” length modifier 来对应它。再比如非标准的 pid_t,我们是没有对应的 length modifier 可用的,这时该怎么办?其实这种问题的一般处理原则是,先把它们转化成可能的最大整数类型,比如这个 pid_t,POSIX 只要求它是一个 signed integer type,所以至少转化成 long 才比较安全,所以我们可以:

printf(“%ldn”, (long)pid);

所以我们最开始的那个问题也可以这么处理,但是很明显这种处理方式并不优雅,尤其是对于标准整数类型来说。一种解决方案是像 ptrdiff_t 那样引入新的 length modifier,但这恐怕需要增加不少,所以C99采取了另一种方式——添加标准的宏来代替直接使用字符串,比如针对 uint32_t,uintmax_t 我们可以使用 PRIu32 和 PRIuMAX,像这样:

printf(“%” PRIu32 “%” PRIuMAX “n”, uint32, uintmax);

这些宏可以在 C99 标准附录 B.7 找到,可以在 <inttypes.h> 中看到其定义(在 glibc 源代码中对应 sysdeps/generic/inttypes.h)。有兴趣的可以直接去看一下源代码,其实就是对已有的 format conversion 进行的宏封装。

那些宏也挺好记的:PRI 代表 printf,SCN 代表 scanf,PRI/SCN后面的第一个字母其实就是对应的常见的 conversion specifier,比如 uint32_t 肯定对应一个u。再后面要么对应长度比如uint32_t中的32,要么对应max什么的。

最后,对于 intmax_t 和 uintmax_t 这两个类型,printf()有对对应的 “j” length modifier 可以直接用,对于 size_t 和 ssize_t ,有对应的 “z”。

获取C语言数据类型大小

一个简单的脚本,可以在命令行下获取C语言中数据类型的大小。在邮件列表的讨论中看到的这个主意,我用 Perl 重写了一下。

代码很简单,见下。分享一下,希望对你有用。
[perl]

!/usr/bin/perl -w

use strict;
use File::Temp qw/tempfile tempdir/;

die “Wrong usage.n” unless @ARGV == 1;
my $type=$ARGV[0];
my $dir = tempdir(CLEANUP => 1);
my ($obj, $src) = tempfile(“$dir/XXXXX”, SUFFIX => ‘.c’, UNLINK => 0);
open my $fd, “>$src” or die “can’t create file: $!”;
$src =~ s/.c$//g;
my $exe_file = $src;

print $fd <<eof;

include

include

include

include

include

include

include

include

include

include

include

include

include

int
main (void)
{
$type x;
printf (“%d\n”, sizeof x);
exit (0);
}
EOF
my $result = qx(gcc -o $exe_file $exe_file.c && $exe_file);
print $result;
[/perl]

一个典型的 off-by-one bug

今天美国人民过节,所以我今天上班基本上没啥事做。闲得发慌时无意中看到有人在邮件列表中报告 hexdump 有一个 bug,重现方法很简单,你只要通过 -s 把 skip 设置成该文件的大小就可以了,你可以用你手头上的 hexdump 试试,你会发现你可以看到全部文件的内容。这一下子激起了我的兴趣。

我一开始的想法是,hexdump 是个老命令了,虽然不能说像 ls 那些命令经过“千锤百炼”吧,但说“基本上不可能有bug”还是可以的。于是我抱着这个心态去翻源代码,虽然我在此之前从来没看过 hexdump 的源代码,但我还是信心满怀地找到了下面这么一句:skip -= sbuf.st_size; ,恩,看来是知道这种情况了,只是把它“折回”处理了,所以我回答说可能是一个 feature 而不是 bug。

但那个人后来的回复一下子惊醒了我,把 skip 再多加上一就是另一种结果了,什么都没有。所以这确实是一个 bug!当时我绕着源代码转了半天也没头绪,而且被里面那么多的全局变量和 static 变量给绕晕了,思考方向走错了。后来等我回到家做完晚饭一想,我靠,多一个或少一就没有 bug,这不就是 off-by-one 的症状嘛!肯定是哪里少算或者多算了一个临界值!抱着这个想法去找就简单了,很快就可以定位到问题所在,于是补丁自然而然地出来了

以前我们学C语言时不经常被提醒一定要注意 off-by-one 嘛,可能都没遇到过多么好的例子,现在这个就是现实中活生生的,非常典型的一个例子。作为反面教材放这里了。

现在再来回头看,问题其实很简单了,但当时分析这个 bug 时怎么就没想到这个原因值得深思:首先是心态不对,要面对现实,哪怕现实再残酷再不符合你的逻辑也要勇于否定自己,不要尝试找理由去否定现实,而是要接受现实去分析原因;其次,没有对“现象”进行深入思考,没有掌握到“现象”中表现出来的隐含规律,而这才是解决这个问题的关键!所以啊,不管在哪里多思考都是有好处的!

我很早以前就有这么个想法啊:作为程序员做 debug 工作,其实在很大程度上和侦探做破案工作是一样的。都要根据“凶案现场”(bug 症状)留下的蛛丝马迹顺藤摸瓜地去找“杀人凶手”(bug 所在);同样都是靠严密的逻辑去推理,唯一不同的是,侦探的逻辑和线索是事情发展的过程和顺序,而程序员的逻辑和线索是源代码;侦探在破案毫无头绪时会想更多的办法去搜集证据和证词等等,而程序员在毫无头绪时可以通过调试器等获取更多 debug 信息,总之都是获取更多的信息量,因为信息更多意味着分析出来的东西会更多。

这让我想起多年前上高中时上课都偷看的《福尔摩斯探案集》来了,里面福尔摩斯老师的那几句话真是饱含哲理,牛逼轰轰,金光闪闪啊:

当你把决不可能的因素排除后,不管剩下的是什么——-不管多么难以置信——那就是真相。

没有掌握全部证据以前,先作出假设,这是决大的错误,那样会使判断产生偏差。

猜想是很不好的习惯,它有害于作逻辑推理。

内牛满面地把这几句话收藏起来作为 debug 时的座右铭…… T_T