缘起
换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 上了。