缘起
换Arch后,我把首选字体改成了这样:
<match target="pattern">
<test qual="any" name="family">
<string>monospace</string>
</test>
<edit name="family" mode="prepend" binding="strong">
<string>DejaVu Sans Mono</string>
<string>文泉驿等宽正黑</string>
</edit>
</match>
<match target="pattern">
<test qual="any" name="family">
<string>serif</string>
</test>
<edit name="family" mode="prepend" binding="strong">
<string>DejaVu Serif</string>
<string>文泉驿正黑</string>
</edit>
</match>
<match target="pattern">
<test qual="any" name="family">
<string>sans-serif</string>
</test>
<edit name="family" mode="prepend" binding="strong">
<string>DejaVu Sans</string>
<string>文泉驿正黑</string>
</edit>
</match>
也就是说,DejaVu 的字体优先于文泉驿的,因为我觉得 DejaVu 的英文字体比文泉驿的好看。这一般没什么问题,除了某些PDF文件,不知道怎么搞的,不嵌入字体也就算了,字体名也奇奇怪怪弄得 evince 不认识。因为太奇怪,并且 evince 显示得更奇怪,我也不好像 KaiTi_GB2312→KaiTi 这样做字体替换了(其实应该用别名的,但当时不会)。想到一个很简单的办法是,让中文字体拥有更高的优先级。实践证明这样做有效。但是,我不希望为了几个乱码的PDF而更改我的全局字体配置。于是,基于 LD_PRELOAD 的解决方案出来了。
LD_PRELOAD 是什么?
LD_PRELOAD 是 Linux 动态链接器认识的一个环境变量(不知道其他系统是否支持)。我看的资料是这个Fun with LD_PRELOAD。原理很简单,使用自己的函数覆盖掉其它动态链接库的。因为 libc 对系统调用都有一个 wrapper,所以正常的程序都会“被骗”的。proxychains 这个让其它程序使用 socks/https 代理的程序的原理就是这个。
我的 hack
我想对指定程序(evince)重定向~/.fonts.conf
的读取,所以准备覆盖open
这个系统调用。按照从那个PDF的链接中找到的源码的方式,写出了最初的代码。结果用cat
测试就失败了——原来还有个 manpage 没说的open64
。继续测试,又发现对vi
写文件时不起作用。于是加上了第三个要覆盖的函数creat
。
程序写好后,觉得光重定向一个文件的读写不好玩,稍稍扩展一下,支持配置就强大了。本来想学那个PDF提到的netjail一样用环境变量的。但又觉得不好设计,而且 C 语言真的写起来太麻烦了。于是想到了用 Lua 来配置。后来还想到用 Python 的,但很可惜,段错误了。
#include<stdarg.h>
#include<dlfcn.h>
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<limits.h>
#include<unistd.h>
#include<lua.h>
#include<lualib.h>
#include<lauxlib.h>
static int lib_initialized = 0;
static int (*orig_open)(const char*, int, mode_t) = 0;
static int (*orig_open64)(const char*, int, mode_t) = 0;
static int (*orig_creat)(const char*, mode_t) = 0;
static lua_State *L = NULL;
void lib_init();
void die(char *fmt, ...) {
va_list args;
va_start(args, fmt);
vfprintf(stderr, fmt, args);
va_end(args);
fprintf(stderr, "\n");
fflush(stderr);
exit(-1);
}
static char* redirect(const char* file){
int ret;
const char *new;
lua_getglobal(L, "redirect");
lua_pushstring(L, file);
ret = lua_pcall(L, 1, 1, 0);
if(ret){
fprintf(stderr, "取得重定向文件路径时出错了: %s\n", lua_tostring(L, -1));
lua_pop(L, 1); /* 错误信息 */
return (char*)file;
}else{
new = lua_tostring(L, -1);
lua_pop(L, 1); /* 返回值 */
}
return (char*)new;
}
int open(const char* file, int flags, mode_t mode) {
lib_init();
file = redirect(file);
return orig_open(file, flags, mode);
}
int open64(const char* file, int flags, mode_t mode) {
lib_init();
file = redirect(file);
return orig_open64(file, flags, mode);
}
int creat(const char* file, mode_t mode) {
lib_init();
file = redirect(file);
return orig_creat(file, mode);
}
void lib_init() {
void *libhdl;
char *dlerr;
if (lib_initialized) return;
if (!(libhdl=dlopen("libc.so.6", RTLD_LAZY)))
die("Failed to patch library calls: %s", dlerror());
orig_open = dlsym(libhdl, "open");
if ((dlerr=dlerror()) != NULL)
die("Failed to patch open() library call: %s", dlerr);
orig_open64 = dlsym(libhdl, "open64");
if ((dlerr=dlerror()) != NULL)
die("Failed to patch open64() library call: %s", dlerr);
orig_creat = dlsym(libhdl, "creat");
if ((dlerr=dlerror()) != NULL)
die("Failed to patch creat() library call: %s", dlerr);
int ret;
L = luaL_newstate();
luaL_openlibs(L);
char config[PATH_MAX];
strcpy(config, getenv("HOME"));
strcat(config, "/.openredir.lua");
ret = luaL_dofile(L, config);
if(ret){
die("Error run ~/.openredir.lua");
}
lua_getglobal(L, "redirect");
if(!lua_isfunction(L,-1)){
die("Error run 'redirect' function in openredir.lua");
}
lua_pop(L, 1);
lib_initialized = 1;
}
Makefile 如下:
CC=gcc
CFLAGS=-g -Wall -I/usr/include/lua5.1
LDFLAGS=-ldl -llua
.PHONY: all clean
all: openredir.so
openredir.so: openredir.o
gcc -shared $< -o $@ $(LDFLAGS)
clean:
-rm *.o
下面这个是配置文件,要放到~/.openredir.lua
才行。
redirect = function(path)
-- TODO use absolute and normalized path
io.stderr:write('打开文件 ' .. path .. '\n')
if path == '/' then
return '/etc/issue.tty1'
elseif path == '/home/lilydjwg/.fonts.conf' then
return '/home/lilydjwg/tmpfs/fonts.conf'
else
return path
end
end
示例:
>>> LD_PRELOAD=./openredir.so cat /
打开文件 /
Arch Linux \r (\n) (\l)
不过使用时在LD_PRELOAD
中最好使用绝对路径,因为某些程序会chdir()
到其它地方去的。
最后吐槽下,Lua 里把路径转成 normalize 过的绝对路径都没有现成的函数,囧。。。
2014年11月23日更新:openredir 已经放在 GitHub 上了。