分类 技术志 下的文章

你好,TeX

由于某种原因,上周末开始学习使用TeX进行文档排版。哦,当然不是直接使用Donald Knuth他老人家设计的最原始的TeX命令。经过这么多年的发展,TeX领域早已出现了各种各样基于TeX开发的层次更高、易用性更好、更加让作者关注内容的好工具。在Ubuntu下,我选择了"TeX Live"。

周末的时间比较零碎(有了果果后,除了晚上外白天很难拿出一长段的时间钻研些东西了),TeX Live安装和体验的过程也是一波三折。TeX Live支持多种安装方式,起初我选择了网络安装,但TeX Live的网络安装程序需要下载2000多个小文件,我的1M宽带实在是太慢,遂放弃。替代的是下载TeX Live的iso文件,这个iso居然有1.9G,着实让我吃惊不小。虽然只有一个.iso文件,但下载过程耗时估计与网络安装也没差多少。不知等待了多久,TeX Live 2010 iso终于下载完毕。安装步骤参考了网上的一些资料,大致是:
1、安装perl-tk,TeX Live的图形化安装需要这个包的支持
sudo apt-get install perl-tk
2、挂载iso,执行安装程序
sudo mount -o loop texlive2010.iso /cdrom
sudo /cdrom/install-tl -gui

Tex Live包确很庞大,完全安装后要2.5个多G空间,无奈本子磁盘太小,需要对其做些裁减,在安装窗口中,点击"Language Collections"按钮,在打开的对话框中取消除中文和英文以外的所有语言包,另外相关文档也只要中英文的,其他语种的一概取消安装。这样才能节省几百M的空间。TeX Live的默认安装目录是"/usr/local/texlive",我的根目录所剩可用空间已经不多了,不想将TeX Live安装到默认目录下,遂找了另外一个FAT32分区,建了一个texlive目录,修改了Tex Live的安装路径,点击“安装”。让人失望的是这次安装失败了,安装日志提示在操作某目标文件似乎没有某种权限,很是疑惑,命令前面已经sudo了,问题无法解决。将安装目录改回默认目录,再次尝试安装,这次倒是很顺利,安装后我的根目录下只剩下3G的空间了:(。

哦,似乎还忘记了一点:
在安装前务必将"创建指向系统目录的符号链接"这个选项置为"是/Yes",这样安装程序会自动为你设置好各种TeX
Live的环境变量,省去之后的很多麻烦。

3、测试安装结果是否正确
TeX Live的官方指南(通过texdoc
texlive-zh-cn打开)中给出了测试安装是否成功的步骤,这里就不赘述了。测试通过后,你将得到一份用Tex排版后的文档:sample2e.pdf,你可以同时打开sample2e.tex文件与sample2e.pdf做一下直观对比,了解一下各种语法宏的作用。

如果你只是做纯英文文档的排版,那你大可到此为止,无须继续向下看了,因为后面要说的是如何让TeX Live支持中文。

TeX Live支持中文排版的方式有多种,初学起来很容易被一堆概念和工具包名搞得晕头转向(直到目前,我也只是了解些皮毛)。这里固定选择一种方式:使用xelatex命令+xeCJK宏包组合。xelatex命令用来编译用LaTeX格式写成的tex文件,并支持Unicode编码以及直接访问系统字体。TeX Live 2010包中包含xelatex和xeCJK宏包,这样无须我们单独下载安装了。

开始测试TeX的中文支持,下面是一个包含中文字符的tex源文件:
%HelloTeX.tex
\documentclass{article}
\usepackage{xeCJK}
\setmainfont{SimSun}
\begin{document}
你好,TeX!
\end{document}

使用xelatex命令编译该tex文件 – xelatex HelloTeX.tex,执行结果如下:
……
Output written on HelloTeX.pdf (1 page).
Transcript written on HelloTeX.log.

xelatex将tex直接转换为pdf格式输出,并将转换过程的log输出到HelloTeX.log中了。用文档查看器打开HelloTeX.pdf,发现中文字符显示为乱码,看来这次转换并未成功。打开HelloTeX.log尝试分析一下转换日志,发现有这样的错误信息:
Invalid UTF-8 byte or sequence at line 5 replaced by U+FFFD.
Invalid UTF-8 byte or sequence at line 5 replaced by U+FFFD.
Missing character: There is no ? in font SimSun/ICU!
Missing character: There is no ? in font SimSun/ICU!

xelatex居然在HelloTeX.tex中发现了非法UTF-8字节!突然恍然大悟,我的VIM配置文件中将文件内码设置为GBK了,这样HelloTeX.tex的数据存储内码是GBK,而不是UTF-8,而xelatex似乎默认采用UTF-8分析.tex文件,不出错误才怪。

临时修改.vimrc,重新编辑HelloTeX.tex(或用iconv将HelloTeX.tex从GBK编码转换为UTF-8编码),重新执行xelatex HelloTeX.tex,这回成功了,"你好,TeX!"被正确输出到pdf文件中了。

xelatex应该也支持解析GBK内码文件才对,翻了翻网上资料,果不其然,通过增加一行\XeTeXinputencoding
"GBK"即可告知xelatex这个tex文件用的是GBK编码:
%HelloTeX.tex
\documentclass{article}
\usepackage{xeCJK}
\XeTeXinputencoding "GBK"
\setmainfont{SimSun}
\begin{document}
你好,TeX!
\end{document}

在体验TeX Live期间还遇到了xelatex找不到系统字体(如SimSun)的情况,后发现我的系统的确没有安装这些字体,Ubuntu 10.04安装字体似乎很方便,将simsun.ttc从Windows系统的"系统盘\Windows\Fonts"目录中copy出一份放置到你的~/.fonts下面即可,然后你通过"fc-list :lang=zh-cn"命令查看系统已安装的字体列表,字体Copy前列表中没有SimSun,Copy结束后我们就发现SimSun的踪影了:"宋体,SimSun:style=Regular"。

想用好TeX Live,不看Manual是不行的,如果你和我一样采用xelatex和xeCJK的组合,那么XeTex(texdoc xetex)和xeCJK(texdoc xeCJK)的Manual是必许要通读的。

从mock malloc说起

上午对一段代码进行单元测试,由于需要用到mock,所以选择使用cmockery
作为Unit Testing框架(lcut还未提供mock功能)。测试代码里需要mock malloc以模拟分配内存失败的异常情况。

编写一个用例后,Build,提示出错:multiple definition of `malloc'。经检查发现Makefile中定义mock malloc的那个目标文件(.o文件)居然被link了两次,类似于下面的这种错误情形:
$ gcc testmain.c malloc.o malloc.o
malloc.o: In function `malloc':
malloc.c:(.text+0×0): multiple definition of `malloc'
malloc.o:malloc.c:(.text+0×0): first defined here
collect2: ld returned 1 exit status

去掉一个显式链接的malloc.o文件后Build顺利通过,运行该单元测试,程序dump core,对此很是疑惑!使用gdb查看core文件,很快发现了问题所在:因为cmockery本身也使用了malloc,但在链接过程中,cmockery库中的malloc符号被绑定到了malloc.c中的那个malloc实现上了,而我们mock的那个malloc在测试用例中又被设置返回NULL,这样非法地址访问就不足为奇了。

对以上两个问题的理解或多或少都需要一些链接方面的知识,这里你可能会问到以下两个问题:
1、C运行库(libc.a)是要被作为默认库隐式提供给ld程序做链接的,那么用自己实现的malloc替代C标准库中的malloc,链接器在链接时为什么没有检查出重定义?
2、cmockery库中的malloc是如何绑定到我们自己实现的那个malloc上的呢?为什么不绑定到C运行库中的那个malloc?

从问题内容我们也似乎可隐约推论出一点:那就是链接器对目标文件(.o)和归档文件(.a)的对待似乎是不同的。没错,的确是这样的。

可执行程序是由一系列.o文件“合并”而成。以静态链接为例,.o文件集合中除了包含我们显式(.c->.o)提供的.o文件外,还有从归档文件(.a)中提取出来的.o文件。这类.o文件是“按需”从.a中提取出来的,这也符合.a文件最初设计的初衷(减少可执行文件的size + 减少可执行文件load到内存后的内存占用)。

我们用一个的例子来解释.o文件“按需”从.a中提取的过程,也顺便回答上面的两个问题。
我们有三个源文件testmain.c、print.c和libprint.c,三个文件都很简单:
/* testmain.c */
extern void print();

int main() {
    print();
    return 0;
}

/* print.c */
#include
void print() {
    printf("print in object files\n");
}

/* libprint.c */
#include
void print() {
    printf("print in archive files\n");
}
我们将libprint.c构建为一个.a文件(gcc -c libprint.c; ar rcs libprint.a libprint.o),用于模拟库中的符号。print.c中的print则是我们自定义函数,试图用来替换库中同名函数。

执行gcc testmain.c print.c -L ./ -lprint,编译顺利通过。执行a.out,输出“print in object files”。显然testmain.c中的print调用被绑定到print.o中的print函数了。分析这个编译链接过程,我们就能回答上面的两个问题了。

我们知道gcc只是一组gnu compile tools的外部名称,gcc像个指挥官,协调一系列tools去完成任务。其中链接是最后一环,ld的输入是.o文件和.a文件。以这个例子来说,最后一步执行的是ld testmain.o print.o -L ./ -lprint …..,其中…..代表的是默认传入的C运行库。链接器从左向右扫描命令行参数中的.o和.a,目的是确定最终.o集合以及为每个.o中的外部符号(引用了但是未在本.o文件中定义)确定具体定义的位置。

链接器依从左到右顺序首先扫描testmain.o,将testmain.o加入到"最终.o文件集合"(初始该集合为空),并发现testmain.o中引用了符号print,但却未定义,将该符号放到"undefined集合"中(初始"undefined集合"为空),另外testmain中还有一个符号main,与print不同,该符号为已定义的符号,同样链接器将之放到"defined集合"中(初始"defined集合"为空)。

继续从左向右扫描,轮到print.o这个目标文件了。该文件中有一个已定义的符号print和一个引用但未定义的外部符号printf,链接器的处理过程是:发现print是当前"undefined集合"中的元素,将print从"undefined集合"中取出,放入"defined集合"中; printf因无法确定定义,放入"undefined集合",print.o放入"最终.o文件集合"。

继续向右扫描,遇到libprint.a。上面说过链接器对待.a与.o不同,.a中的符号是按需提取,这里的“按需”指的就是"undefined集合"中的符号。当前"undefined集合"中只有一个元素:printf,链接器尝试在libprint.a中查找printf的定义,未果。则链接器略过libprint.a,继续向右扫描。

最后剩下的就是libc.a了,也就是默认传递的C运行库。libc.a中包含了成百上千个.o文件。但目前只剩下printf一个符号没有得到定义了,我们只需要libc.a中包含printf符号定义的那个.o文件,也就是print.o,链接器找到print.o后将print.o放入"最终.o文件集合",将printf符号从"undefined集合"挪到"defined集合"中,此致"undefined集合"变为空集合了。也就说明这次链接是成功的。

相信上面的两个问题通过这段过程描述已经可以被解释了。

如果我们将构建语句写为:gcc testmain.c -L./ -lprint print.c会发生什么呢?我们看看执行结果:
/tmp/ccSNKvLP.o: In function `print':
print.c:(.text+0×0): multiple definition of `print'
.//libprint.a(libprint.o):libprint.c:(.text+0×0): first defined here
collect2: ld returned 1 exit status

出现重定义错误!不过有了之前的基础,这里的重定义也很好理解了。gcc testmain.c -L./ -lprint print.c执行到最后一步是ld testmain.o -L./ -lprint print.o ….; 链接器扫描完libprint.a后,print的符号已经从libprint.a中的libprint.o目标文件中被"按需"提取出来放入"defined集合"中了。接下来链接器扫描print.o居然又发现了一个名为print的全局定义的符号,与"defined集合"中冲突,ld自然就会报错。

我们再来做点修改,构造一个稍微复杂些的例子:
/* testmain.c */
extern void do_print();

int main() {
    do_print();
    return 0;
}

/* print.c */
#include
void print() {
    printf("print in object files\n");
}

/* libprint.c */
#include
void print();
void do_print() {
    print();
}

void print() {
    printf("print in archive files\n");
}
在testmain.c中我们换作调用do_print了,do_print在libprint.a中有定义。执行gcc testmain.c print.c -L ./ -lprint,结果出错:
.//libprint.a(libprint.o): In function `print':
libprint.c:(.text+0xd): multiple definition of `print'
/tmp/ccoWjHZS.o:print.c:(.text+0×0): first defined here
collect2: ld returned 1 exit status

这回怎么又变成“重定义”了呢?我们来分析一下:
*扫描testmain.o,"undefined集合"中有了符号do_print;
*扫描print.o,"undefined集合"未变,"defined集合"中增加了print
*碰到libprint.a,按照"按需"提取的原则,我们找到了do_print定义,"undefined集合"中的do_print被移到"defined集合",libprint.a中的libprint.o被放置到"最终.o文件集合"中;与前面例子不同的是libprint.o中有两个符号do_print和print,作为"最终.o文件集合"中的一分子,libprint.o的地位与testmain.o和print.o是一致的,链接器需要扫描其全部内容,而不仅仅只是提取do_print,这样链接器又发现一个print的定义,与"defined集合"中的print符号重复,链接器报错!

如果要进一步了解链接器相关内容的话,推荐阅读一下下面几本书籍:
1、《链接器与加载器
2、《深入理解计算机系统
3、国人总结性质的大作《程序员的自我修养–链接、装载与库

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言精进之路1 Go语言精进之路2 Go语言编程指南
商务合作请联系bigwhite.cn AT aliyun.com

欢迎使用邮件订阅我的博客

输入邮箱订阅本站,只要有新文章发布,就会第一时间发送邮件通知你哦!

这里是 Tony Bai的个人Blog,欢迎访问、订阅和留言! 订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠 ,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:

如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:

以太币:

如果您喜欢通过微信浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:
本站Powered by Digital Ocean VPS。
选择Digital Ocean VPS主机,即可获得10美元现金充值,可 免费使用两个月哟! 著名主机提供商Linode 10$优惠码:linode10,在 这里注册即可免费获 得。阿里云推荐码: 1WFZ0V立享9折!


View Tony Bai's profile on LinkedIn
DigitalOcean Referral Badge

文章

评论

  • 正在加载...

分类

标签

归档



View My Stats