在LINUX.ORG.RU
上阅读了“
Perl解释器代码已正式移植到GitHub ”的新闻之后,我决定看一下现在已经在GitHub上的Perl 5存储库。
令人惊奇的是,他们震惊与敬畏,不仅绝对保留了整个项目32年的历史,而且还保留了错误报告(陷入问题),补丁(陷入PR),发行版和分支。 文件旁边的题词“
32年前 ”引起不由自主的微笑。
在这个沉闷的星期五晚上,当雨和雪在街道上令人毛骨悚然地下着毛毛雨,而所有的街道都陷在秋天的泥泞中时,还有什么可做的呢? 是的,红眼睛! 因此,出于实验和兴趣的考虑,我决定在具有最新版本
GCC 9.2.0的现代x86_64机器上编译并组装古老的Perl。 这样的旧代码可以通过时间的考验吗?
在现代Arch Linux发行版中,展示了X窗口系统的首批窗口管理器twm 。为了完全真实可靠,我部署了一个带有裸X和窗口管理器
twm的虚拟机,该虚拟机也于1987年发布。 谁知道,也许
拉里·沃尔 (
Larry Wall )恰好使用
twm编写了他的Perl,可以说
是那个时代的
尖端技术 。 使用的发行版是Arch Linux。 只是因为他的存储库中有一些有用的东西,后来才派上用场。 所以走吧!
内容:
1.准备环境2.配置源代码3. Yacc语法文件错误4.“ C”上的代码编译错误5.纠正一些错误分段错误6.总结1.准备环境
首先,我们在虚拟机上已部署的操作系统上安装了汇编和编辑源代码所需的所有绅士工具集和编译器:
gcc ,
make ,
vim ,
git ,
gdb等。其中一些已经安装,而另一些则在meta包中提供。
base-devel ,如果未安装,则必须安装。 一旦环境准备好采取行动,我们将获得32年历史的Perl源代码的副本!
$ git clone https://github.com/Perl/perl5/ --depth=1 -b perl-1.0
借助Git的功能,我们无需拖一堆文件即可到达项目的第一个版本:
* commit 8d063cd8450e59ea1c611a2f4f5a21059a2804f1 (grafted, HEAD, tag: perl-1.0) Commit: Larry Wall <lwall@jpl-devvax.jpl.nasa.gov> CommitDate: Fri Dec 18 00:00:00 1987 +0000 a "replacement" for awk and sed
我们仅下载少量数据,因此,带有Perl第一个版本的源代码的存储库仅占用150 KB。
在那黑暗而密集的时间里,没有诸如
自动工具之类的基本
工具 (真是太
幸运 了! ),但是,存储库根目录中有一个
Configure脚本。 怎么了 但事实是,拉里·沃尔(Larry Wall)是此类脚本的发明者,这些脚本允许为当时最混乱的UNIX计算机生成Makefile。 正如Wikipedia上有关
同名脚本的
文章所述,Larry Wall在编写Perl的三年后就向
Configure文件提供了一些软件,例如新闻阅读器
rn 。 随后,Perl也不例外,并且使用已经在许多计算机上运行的脚本来构建它。 后来,其他开发人员(例如Trolltech的程序员)也采纳了这个想法。 他们使用类似的脚本来配置其Qt框架的构建,许多人将其与
autotools的 configure混淆了。 正是来自不同开发人员的此类脚本的动物园,它们为创建用于简化和自动生成的工具提供了动力。
<<跳到内容2.配置源代码
“老派”的
配置脚本,从其
Shebang '中已经很明显了,该脚本有一个空格:
$ cat Configure | head -5
根据评论,事实证明脚本中存在无法发表评论的shell! 空间状况看起来很不正常,但是一旦成为常态,请在
此处查看链接以获取更多信息。 最重要的是,对于现代的shell解释器而言,是否存在空间没有什么区别。
足够的歌词,让我们开始吧! 我们启动脚本并看到一个有趣的假设,事实并非完全如此:
$ ./Configure (I see you are using the Korn shell. Some ksh's blow up on Configure, especially on exotic machines. If yours does, try the Bourne shell instead.) Beginning of configuration questions for perl kit. Checking echo to see how to suppress newlines... ...using -n. Type carriage return to continue. Your cursor should be here-->
令人惊讶的是,脚本是交互式的,并且包含大量的各种背景信息。 用户交互模型建立在对话框上,分析脚本更改其参数所依据的答案,并根据脚本随后生成Makefile。 我个人有兴趣检查所有shell命令是否都到位?
Locating common programs... expr is in /bin/expr. sed is in /bin/sed. echo is in /bin/echo. cat is in /bin/cat. rm is in /bin/rm. mv is in /bin/mv. cp is in /bin/cp. tr is in /bin/tr. mkdir is in /bin/mkdir. sort is in /bin/sort. uniq is in /bin/uniq. grep is in /bin/grep. Don't worry if any of the following aren't found... test is in /bin/test. egrep is in /bin/egrep. I don't see Mcc out there, offhand.
显然,在此之前还远非如此。 我想知道
Mcc实用程序负责什么,找不到哪个? 有趣的是,该脚本具有当时最好的黑客传统,充满了幽默。 现在,您几乎看不到:
Is your "test" built into sh? [n] (OK to guess) OK Checking compatibility between /bin/echo and builtin echo (if any)... They are compatible. In fact, they may be identical. Your C library is in /lib/libc.a. You're normal. Extracting names from /lib/libc.a for later perusal...done Hmm... Looks kind of like a USG system, but we'll see... Congratulations. You aren't running Eunice. It's not Xenix... Nor is it Venix... Checking your sh to see if it knows about # comments... Your sh handles # comments correctly. Okay, let's see if #! works on this system... It does. Checking out how to guarantee sh startup... Let's see if '#!/bin/sh' works... Yup, it does.
我用默认值或脚本为我提供的内容回答了大多数问题。 对编译器和链接器的标志请求特别高兴和惊讶:
Any additional cc flags? [none] Any additional ld flags? [none]
您可以在其中编写一些有趣的内容,例如
-m32来构建32位可执行文件或库,这是链接期间所需的。 最后一个脚本问题:
Now you need to generate make dependencies by running "make depend". You might prefer to run it in background: "make depend > makedepend.out &" It can take a while, so you might not want to run it right now. Run make depend now? [n] y
我回答肯定。 通过
其 Wikipedia
页面判断,古老的
makedepend实用程序是在
Athena项目生命的一开始就创建的,以方便使用Makefiles。 该项目为我们提供了X窗口系统,Kerberos,Zephyr,并影响了当今许多其他熟悉的事物。 所有这些都很棒,但是这个实用程序在现代Linux环境中从何而来? 长期以来,没人和任何地方都在使用它。 但是,如果您仔细查看存储库的根目录,就会发现Larry Wall编写了其替代脚本版本,我们仔细解压缩并执行了配置脚本。
Makedepend完成时出现一些奇怪的错误:
./makedepend: command substitution: line 82: unexpected EOF while looking for matching `'' ./makedepend: command substitution: line 83: syntax error: unexpected end of file ./makedepend: command substitution: line 82: unexpected EOF while looking for matching `'' ./makedepend: command substitution: line 83: syntax error: unexpected end of file
也许是由引起问题的他们才对生成的Makefile稍加咀嚼:
$ make make: *** No rule to make target '<built-in>', needed by 'arg.o'. Stop.
我绝对不想进入
makedepend实用程序复杂的贝壳面的丛林,我决定仔细看一下Makefile,其中出现了一种奇怪的模式:
arg.o: arg.c arg.o: arg.h arg.o: array.h arg.o: <built-in> arg.o: cmd.h arg.o: <command-line> arg.o: config.h arg.o: EXTERN.h ... array.o: arg.h array.o: array.c array.o: array.h array.o: <built-in> array.o: cmd.h array.o: <command-line> array.o: config.h array.o: EXTERN.h ...
显然,某些实用程序错误地将其参数插入到了尾声中。 拿起
ax实用程序
sed,我决定稍微解决一下这个问题:
$ sed -i '/built-in/d' Makefile $ sed -i '/command-line/d' Makefile
出乎意料的是,这个技巧成功了,Makefiles可以正常工作了!
<<跳到内容3. Yacc语法文件错误
如果具有32年历史的代码可以毫无问题地进行汇编,那将是令人难以置信的。 不幸的是,奇迹没有发生。 在研究源代码树时,我遇到了一个
perl.y文件,该文件是
yacc实用程序的语法说明,该实用程序在现代发行版中早已被
bison取代。 位于
/ usr / bin / yacc路径中的脚本仅在与
yacc兼容的模式下调用
bison 。 只是这种兼容性还不完全,并且在处理此文件时会涌入大量错误,我不知道该如何更正,也不是真的想要,因为我最近学到了另一种解决方案。
一两年前,KDE的开发者Helio Chissini de Castro做了类似的工作,并将KDE 1,2和Qt 1,2适应了现代环境和编译器。 我对他的工作很感兴趣,下载了项目的源代码,但是在组装过程中,由于
yacc和
bison的不兼容而遇到了类似的
陷阱 ,而
yacc和
bison却被用来构建
moc元编译器的古老版本。 随后,我设法以
byacc实用程序(Berkeley Yacc)替代
bison的形式找到了解决此问题的方法,该实用程序证明与
yacc的旧语法兼容,并且在许多Linux发行版中都可用。
在构建系统中用
byacc轻松替换
yacc可以帮助我,尽管时间不长,因为稍后在
byacc的新版本中,
它们仍然破坏了与
yacc的兼容性,中断了与
yydebug实体相关的调试。 因此,我不得不稍微
更正该实用程序
的语法 。
因此,以前的经验曾预测过纠正
perl.y文件中的构造错误的策略:安装
byacc实用程序,在所有Makefile
中将 yacc更改为
byacc ,然后从任何地方剪切
yydebug 。 这些操作解决了该文件的所有问题,错误消失了,编译继续进行。
<<跳到内容4.“ C”上的代码编译错误
Perl的古老代码充满了恐惧,例如K&R类型的函数定义早已过时且被遗忘的表示法:
format(orec,fcmd) register struct outrec *orec; register FCMD *fcmd; { ... } STR * hfetch(tb,key) register HASH *tb; char *key; { ... } fatal(pat,a1,a2,a3,a4) char *pat; { fprintf(stderr,pat,a1,a2,a3,a4); exit(1); }
例如,在同样很古老的
Microsoft Word 1.1a代码中也发现了类似的功能。 编程语言“ C”的第一个标准称为“ C89”,将仅在两年内出现。 现代的编译器能够使用这样的代码,但是某些IDE并不容易解析这样的定义并将其突出显示为语法错误,例如,
Qt Creator在将其中的代码解析到
libclang库之前犯了罪。
GCC 9.2.0编译器发出了大量警告,并承诺编译Perl第一版的古老代码。 警告中的内容如此之大,以至于要找出错误,我们不得不向上滚动几页。 令我惊讶的是,大多数编译错误都是典型的错误,主要与预定义定义有关,预定义定义在程序集中起标志的作用。
twm窗口管理器和xterm终端仿真器中的现代GCC 9.2.0编译器和GDB 8.3.1调试器的工作。在STDSTDIO之下
,拉里·沃尔
( Larry Wall)试用了一些古老的非标准编程语言库“ C”,并且在DEBUGGING下使用了臭名昭著的
yydebug调试信息,如上所述。 默认情况下,这些标志是启用的。 通过在
perl.h文件中关闭它们并添加一些被遗忘的定义,我能够显着减少错误的数量。
另一种错误类型是重写标准库和POSIX层的当前标准化功能。 该项目具有自己的
malloc() ,
setenv()以及其他产生冲突的实体。
几个地方定义了没有声明的静态函数。 随着时间的流逝,编译器开始对这个问题采取更严格的方法,
并将警告变成错误 。 最后,还有几个被遗忘的标题,如果没有它们,您将去哪里。
令我惊讶的是,这个具有32年历史的代码的补丁是如此之小,以至于可以在此处完整引用:
diff --git a/malloc.cb/malloc.c index 17c3b27..a1dfe9c 100644 --- a/malloc.c +++ b/malloc.c @@ -79,6 +79,9 @@ static u_int nmalloc[NBUCKETS]; #include <stdio.h> #endif +static findbucket(union overhead *freep, int srchlen); +static morecore(register bucket); + #ifdef debug #define ASSERT(p) if (!(p)) botch("p"); else static diff --git a/perl.hb/perl.h index 3ccff10..e98ded5 100644 --- a/perl.h +++ b/perl.h @@ -6,16 +6,16 @@ * */ -#define DEBUGGING -#define STDSTDIO /* eventually should be in config.h */ +//#define DEBUGGING +//#define STDSTDIO /* eventually should be in config.h */ #define VOIDUSED 1 #include "config.h" -#ifndef BCOPY -# define bcopy(s1,s2,l) memcpy(s2,s1,l); -# define bzero(s,l) memset(s,0,l); -#endif +//#ifndef BCOPY +//# define bcopy(s1,s2,l) memcpy(s2,s1,l); +//# define bzero(s,l) memset(s,0,l); +//#endif #include <stdio.h> #include <ctype.h> @@ -183,11 +183,11 @@ double atof(); long time(); struct tm *gmtime(), *localtime(); -#ifdef CHARSPRINTF - char *sprintf(); -#else - int sprintf(); -#endif +//#ifdef CHARSPRINTF +// char *sprintf(); +//#else +// int sprintf(); +//#endif #ifdef EUNICE #define UNLINK(f) while (unlink(f) >= 0) diff --git a/perl.yb/perl.y index 16f8a9a..1ab769f 100644 --- a/perl.y +++ b/perl.y @@ -7,6 +7,7 @@ */ %{ +#include <stdlib.h> #include "handy.h" #include "EXTERN.h" #include "search.h" diff --git a/perly.cb/perly.c index bc32318..fe945eb 100644 --- a/perly.c +++ b/perly.c @@ -246,12 +246,14 @@ yylex() static bool firstline = TRUE; retry: +#ifdef DEBUGGING #ifdef YYDEBUG if (yydebug) if (index(s,'\n')) fprintf(stderr,"Tokener at %s",s); else fprintf(stderr,"Tokener at %s\n",s); +#endif #endif switch (*s) { default: diff --git a/stab.cb/stab.c index b9ef533..9757cfe 100644 --- a/stab.c +++ b/stab.c @@ -7,6 +7,7 @@ */ #include <signal.h> +#include <errno.h> #include "handy.h" #include "EXTERN.h" #include "search.h" diff --git a/util.hb/util.h index 4f92eeb..95cb9bf 100644 --- a/util.h +++ b/util.h @@ -28,7 +28,7 @@ void prexit(); char *get_a_line(); char *savestr(); int makedir(); -void setenv(); +//void setenv(); int envix(); void notincl(); char *getval();
32年旧代码的绝佳结果! 通过将
-lcrypt指令添加到带有适当的
libcrypt库的Makefile中,可以解决
对'crypt'链接错误的
未定义引用 ,此后,我终于获得了所需的Perl解释器可执行文件:
$ file perl perl: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=fd952ceae424613568530b3a2ca88ebd6477e0ae, for GNU/Linux 3.2.0, not stripped
<<跳到内容5.纠正一些错误分段错误
经过几乎没有麻烦的编译后,运气转过了我头。 启动组装好的Perl解释器后,我立即得到一些奇怪的错误和一个Segmentation错误:
$ ./perl -e 'print "Hello World!\n";' Corrupt malloc ptr 0x2db36040 at 0x2db36000 Corrupt malloc ptr 0x2db36880 at 0x2db36800 Corrupt malloc ptr 0x2db36080 at 0x2db36040 Corrupt malloc ptr 0x2db37020 at 0x2db37000 Segmentation fault (core dumped)
了短语
Corrupt malloc的源文本后,结果发现,从1982年起,就不再使用系统
malloc()了某种自定义分配器。 有趣的是,
Berkeley是用源代码中的字符串文字之一编写的,而
Caltech则是在其旁边的注释中编写的。 这些大学之间的合作非常明显。 总的来说,我注释掉了这个黑客分配器并重建了源代码。 内存损坏错误消失了,但是分段错误仍然存在。 所以这不是重点,现在我们需要发现调试器。
在
gdb下运行程序
,我发现当从libc创建临时文件
mktemp()的函数被调用时,发生崩溃:
$ gdb --args ./perl -e 'print "Hello, World!\n";' (gdb) r Starting program: /home/exl/perl5/perl -e print\ \"Hello\ World\!\\n\"\; Program received signal SIGSEGV, Segmentation fault. 0x00007ffff7cd20c7 in __gen_tempname () from /usr/lib/libc.so.6 (gdb) bt #0 0x00007ffff7cd20c7 in __gen_tempname () from /usr/lib/libc.so.6 #1 0x00007ffff7d71577 in mktemp () from /usr/lib/libc.so.6 #2 0x000055555556bb08 in main ()
顺便说一下,链接器以前在此功能时发誓。 不是编译器,而是链接器,这让我感到惊讶:
/usr/bin/ld: perl.o: in function `main': perl.c:(.text+0x978c): warning: the use of `mktemp' is dangerous, better use `mkstemp' or `mkdtemp'
您可能也想到的第一个想法就是用我做的
mkstemp()替换
不安全的函数 mktemp() 。 链接器警告消失了,但是分段错误仍然保留在该位置,只是现在它在
mkstemp()函数中。
因此,现在您需要非常仔细地查看与此功能关联的代码段。 在那里,我发现了一个很奇怪的东西,该片段中突出显示了这一点:
char *e_tmpname = "/tmp/perl-eXXXXXX"; int main(void) { mktemp(e_tmpname); e_fp = f_open(e_tmpname, "w"); ... }
事实证明,
mktemp()试图更改掩码的文字,该掩码位于
.rodata节中,这显然注定要失败。 还是毕竟,在32年前,这是可以接受的,在代码中得到了满足,甚至以某种方式起作用了?
当然,用
char e_tmpname []替换
char * e_tmpname 可解决此细分错误,并且我能够获得整个晚上都被杀死的信息:
$ ./perl -e 'print "Hello World!\n";' $ Hello, World! $ ./perl -e '$a = 5; $b = 6.3; $c = $a+$b; print $c."\n";' $ 11.3000000000000007 $ ./perl -v $Header: perly.c,v 1.0 87/12/18 15:53:31 root Exp $ Patch level: 0
我们从命令行检查了执行情况,但是文件呢? 我从互联网上下载了Perl编程语言的第一个“ Hello World”:
然后,我尝试运行它,但是,遗憾的是,细分错误再次在等我。 这次在完全不同的地方:
$ gdb --args ./perl test.pl (gdb) r Starting program: /home/exl/perl5/perl test.pl Program received signal SIGSEGV, Segmentation fault. 0x00007ffff7d1da75 in __strcpy_sse2_unaligned () from /usr/lib/libc.so.6 (gdb) bt #0 0x00007ffff7d1da75 in __strcpy_sse2_unaligned () from /usr/lib/libc.so.6 #1 0x00005555555629ea in yyerror () #2 0x0000555555568dd6 in yyparse () #3 0x000055555556bd4f in main ()
在
yyerror()函数中发现了以下有趣的观点,我引用了原始代码段:
同样,情况与我上面写过的情况类似。
.rodata节中的数据被
再次修改 。 也许是由于复制粘贴而造成的错别字,而不是他们想写
tmpbuf而不是
tname ? 还是它背后确实有某种隐藏的含义? 无论如何,用
char tokename [] [32]替换
char * tokename [] 都会消除分段错误,Perl会告诉我们以下内容:
$ ./perl test.pl syntax error in file test.pl at line 7, next token "strict" Execution aborted due to compilation errors.
事实证明,他不喜欢各种
严格的新奇
用法 ,这就是他要告诉我们的! 如果删除或注释掉文件中的这些行,程序将启动:
$ ./perl test.pl Hello, World!
<<跳到内容6.总结
实际上,我实现了自己的目标,并且使1987年的古老代码不仅可以编译,而且可以在现代Linux环境中工作。 毫无疑问,仍然存在大量的各种分段错误,可能与64位体系结构上的指针大小有关。 在准备好调试器几个晚上之后,可以清理所有这些东西。 但这不是一项非常令人愉快且乏味的任务。 毕竟,最初,这个实验被计划为一个无聊的夜晚的娱乐活动,而不是作为一项成熟的工作,而这项工作将结束。 采取的行动有什么实际的好处? 也许有一天,一些数字考古学家会遇到这篇文章,这对他很有用。 但是在我看来,即使在现实世界中,即使是从此类研究中获得的经验也不太有价值。
如果有人感兴趣,我会发布一组两个补丁。 第一个修复了编译错误,第二个修复了一些Segmentation错误。
PS我急于让
破坏性单线游戏玩家感到不快,这在这里不起作用。 也许Perl的版本太旧了,无法进行此类娱乐。
PPS一切都很好,周末愉快。 感谢
kawaii_neko的一个
小问题 。
更新2019年10月28日: LINUX.ORG.RU论坛的用户使用昵称
utf8nowhere 在他对本文的
评论中提供了非常有趣的链接,该信息不仅澄清了可变字符串文字的情况,甚至还考虑了上述使用问题。
mktemp()函数! 让我引用这些资料,这些资料描述了非标准化的K&R C和GNU C之间的各种不兼容性:
GCC的不兼容
GNU C和K&R(非ISO)版本的C之间存在一些值得注意的不兼容性。
GCC通常使字符串常量为只读。 如果使用了几个外观相同的字符串常量,则GCC仅存储该字符串的一个副本。
结果是您不能使用字符串常量参数调用mktemp 。 函数mktemp总是更改其参数指向的字符串。
另一个结果是,在将字符串常量作为格式控制字符串或输入传递时, sscanf在某些系统上不起作用。这是因为sscanf错误地尝试写入字符串常量。同样是fscanf和scanf。
这些问题的最佳解决方案是将程序更改为使用带初始化字符串的char -array变量而不是字符串常量来达到这些目的。但是,如果这不可能,则可以使用-fwritable-strings标志,该标志指示GCC以与大多数C编译器相同的方式处理字符串常量。
资料来源:使用GNU编译器集合(GCC 3.3)官方手册。
-fwritable-strings编译器标志在GCC 3.4中已弃用,并在GCC 4.0中被永久删除。ANSI C rationale | String literals
String literals are specified to be unmodifiable. This specification allows implementations to share copies of strings with identical text, to place string literals in read-only memory, and perform certain optimizations. However, string literals do not have the type array of const char, in order to avoid the problems of pointer type checking, particularly with library functions, since assigning a pointer to const char to a plain pointer to char is not valid. Those members of the Committee who insisted that string literals should be modifiable were content to have this practice designated a common extension (see F.5.5).
Existing code which modifies string literals can be made strictly conforming by replacing the string literal with an initialized static character array. For instance,
char *p, *make_temp(char *str); p = make_temp("tempXXX");
can be changed to:
char *p, *make_temp(char *str); { static char template[ ] = "tempXXX"; p = make_temp( template ); }
: Rationale for American National Standard for Information Systems, Programming Language C .
用户VarfolomeyKote4ka提供有趣gryaznenky黑客,允许你绕过错误分段错误,当您尝试区间变化数据.RODATA通过将其转换成部分.rwdata。不久前,程序员Guye1296在Internet上刊登了一篇非常有趣的文章,“从.rodata到.rwdata-内存映射和LD脚本简介”,其中介绍了如何实现此技巧。为了促进获得期望的结果,本文的作者为标准链接程序ld - rwdata.ld准备了相当多的脚本。。只需下载此脚本,将其放在Perl源目录的根目录中,按如下所示纠正LDFLAGS标志:LDFLAGS = -T rwdata.ld,然后重建项目。结果,我们得到以下结果: $ make clean && make -j1 $ mv perl perl_rodata $ curl -LOJ https://raw.githubusercontent.com/guye1296/ld_script_elf_blog_post/master/rwdata.ld $ sed -i 's/LDFLAGS =/LDFLAGS = -T rwdata.ld/' Makefile $ make clean && make -j1 $ mv perl perl_rwdata $ objdump -s -j .rodata perl_rodata | grep tmp -2 19da0 21233f5e 7e3d2d25 30313233 34353637 !
事实证明,由于存在这种黑客攻击,几乎可以忽略第二个补丁中的所有更改!当然,尽管使代码成为不违反标准的视图仍然是可取的。<<跳到内容