Perl 初学者指南 - 第6部分

编辑注:这个系列正在进行更新。您可能对以下新版本感兴趣,可在

目录

本系列第1部分
本系列第2部分
本系列第3部分
本系列第4部分
本系列第5部分

第一次做对
评论
警告
Taint
Taint 无法捕获的内容
use strict
  •严格的变量
  •严格的子程序
  •想要一个子程序,却得到一个字符串
  •唯一的例外
这是不是过度了?
试试看!

第一次做对

Perl 是一个有用的工具,许多人用它来编写一些优秀的软件。但像所有编程语言一样,Perl 也可以用来创建 糟糕 的软件。糟糕的软件包含错误,有安全漏洞,难以修复或扩展。

幸运的是,Perl 提供了多种方式来提高你编写的程序的质量。在本系列的最后一部分,我们将探讨其中的一些。

评论

在本系列的第一部分,我们介绍了低微的 #,它表示注释。注释是防御糟糕软件的第一道防线,因为它们有助于回答人们在查看源代码时总是有的两个问题:这个程序做什么,它是如何做到的?注释应该始终是任何你编写的软件的一部分。没有注释的复杂代码并不一定是 自动 的糟糕,但最好还是留一些圣水以防万一。

好的注释简短但富有教育意义。它们告诉你从阅读代码中无法了解的事情。例如,这里有段可能需要一两个注释的晦涩代码

        for $i (@q) {
            my ($j) = fix($i);
            transmit($j);
        }

糟糕的注释可能看起来像这样

        for $i (@q) { # @q is list from last sub
            my ($j) = fix($i);  # Gotta fix $j...
            transmit($j);  # ...and then it goes over the wire
        }

请注意,你从这些注释中学不到任何东西。 my ($j) = fix($i); # Gotta fix $j... 没有意义,就像包含有定义 widget (n.): A widget 的字典一样。 什么是 @q为什么 你要修复它的值?这可能从程序的更大上下文中变得清楚,但你不想在程序中到处寻找一行代码的作用!

这里有更清晰一些的东西。请注意,我们实际上有更少的注释,但它们更有教育意义

       # Now that we've got prices from database, let's send them to the buyer
       for $i (@q) {
           my ($j) = fix($i);  # Add local taxes, perform currency exchange
           transmit($j);
       }

现在很明显 @q 从哪里来,以及 fix() 做了什么。

警告

注释很好,但编写良好 Perl 的最重要的工具是“警告”标志,即 -w 命令行开关。你可以通过将 -w 放置在程序的 第一行来开启警告,如下所示

         #!/usr/local/bin/perl -w

或者,如果你是从命令行运行程序,你可以在那里使用 -w,例如 perl -w myprogram.pl

启用警告会导致Perl在许多事情上发出抱怨,而这些事情往往是程序中bug的来源。Perl通常对可能存在的问题采取宽松的态度;即使你不清楚自己在做什么,它也假定你知道自己在做什么。

以下是一个Perl可以愉快运行而不眨眼的程序示例,尽管它几乎每行都有错误!(你能找出多少?)

       #!/usr/local/bin/perl

       $filename = "./logfile.txt";
       open (LOG, $fn);
       print LOG "Test\n";
       close LOGFILE;

现在,在第一行添加-w开关,然后再次运行。你应该看到类似以下内容:

名称“main::filename”只使用一次:在./a6-warn.pl的第3行可能有拼写错误。名称“main::LOGFILE”只使用一次:在./a6-warn.pl的第6行可能有拼写错误。名称“main::fn”只使用一次:在./a6-warn.pl的第4行可能有拼写错误。在./a6-warn.pl的第4行使用了未初始化的值。在./a6-warn.pl的第5行向关闭的文件句柄main::LOG打印。

以下是对这些错误的解释

  1. 名称“main::filename”只使用一次:在./a6-warn.pl的第3行可能有拼写错误。名称“main::fn”只使用一次:在./a6-warn.pl的第4行可能有拼写错误。Perl注意到$filename$fn都只使用了一次,并猜测你可能拼错了或误用了其中一个。这通常是因为代码中的错误,比如使用了$filenmae而不是$filename,或者在整个程序中使用$filename,除了在一个地方使用了$fn(如这个程序所示)。

  2. 名称“main::LOGFILE”只使用一次:在./a6-warn.pl的第6行可能有拼写错误。正如我们弄错了$filename的拼写一样,我们也搞混了文件句柄的名字:我们在写入日志条目时使用LOG作为文件句柄,但试图关闭LOGFILE

  3. 在./a6-warn.pl的第4行使用了未初始化的值。这是Perl较为晦涩的抱怨之一,但修复起来并不困难。这意味着你在给变量赋值之前就尝试使用它,这几乎总是一个错误。当我们第一次在我们的程序中提到$fn时,它还没有被赋值。你可以在第一次使用变量之前总是为它设置一个默认值,以避免此类警告。

  4. 在./a6-warn.pl的第5行向关闭的文件句柄main::LOG打印。我们没有成功打开LOG,因为$fn为空。当Perl看到我们尝试向LOG文件句柄打印内容时,它通常会忽略它,并假设我们知道自己在做什么。但是当启用-w时,Perl会警告我们它怀疑有问题。

那么,我们如何修复这些警告?显然的第一步是修复脚本中的这些问题。(顺便说一下,我故意违反了总是检查open()是否成功的规则!让我们也修复它。)这使得它变成:

        #!/usr/local/bin/perl -w

        $filename = "./logfile.txt";
        open (LOG, $filename) or die "Couldn't open $filename: $!";
        print LOG "Test\n";
        close LOG;

现在,我们运行我们的修正后的程序,并从它得到以下结果

文件句柄main::LOG在./a6-warn2.pl的第5行仅打开用于输入。

这个错误是从哪里来的?看看我们的open()。由于我们没有在文件名前加>或>>,Perl以读取模式打开文件,但在下一行我们尝试用print向它写入。Perl通常会让这过去,但当启用警告时,它会提醒你可能有问题。将第4行改为以下内容,一切都会好起来的

       open (LOG, ">>$filename") or die "Couldn't open $filename: $!";

<-w> 标志是你的好朋友。始终保持开启状态。你还可能想阅读 <perldiag> 手册页,其中包含 Perl 遇到问题时会输出的所有各种消息(包括警告)。每条消息都附有对该消息含义的详细描述以及如何修复它的方法。

污染

使用 -w 将有助于使你的 Perl 程序正确,但它不会帮助你使它们变得 安全。可以编写一个不会发出任何警告,但却是完全不安全的程序!

例如,假设你正在编写一个需要将用户的评论写入用户指定的文件的 CGI 程序。你可能使用以下类似的方法

       #!/usr/local/bin/perl -w

       use CGI ':standard';

       $file = param('file');
       $comment = param('comment');

       unless ($file) { $file = 'file.txt'; }
       unless ($comment) { $comment = 'No comment'; }

       open (OUTPUT, ">>/etc/webstuff/storage/" . $file) or die "$!";
       print OUTPUT $comment . "\n";
       close OUTPUT;

       print header, start_html;
       print "<P>Thanks!</P>\n";       
       print end_html;

如果你已经阅读了 CGI 编程章节,警钟已经足够响亮,以至于可以使人聋掉。这个程序信任用户只能指定一个“正确”的文件名,而你比任何人都清楚不应该信任用户。但在这个程序中没有任何东西会引起 -w 的注意;就警告而言,这个程序是完全正确的。

幸运的是,有一种方法可以在这些类型的问题成为问题之前阻止它们。Perl 提供了一种称为 污染 的机制,它将任何用户可能控制的变量标记为不安全。这包括用户输入、文件输入和环境变量。你自己在程序中设置的任何东西都被认为是安全的

     $taint = <STDIN>;   # This came from user input, so it's tainted
     $taint2 = $ARGV[1]; # The @ARGV array is considered tainted too.
     $notaint = "Hi";    # But this is in your program... it's untainted

你可以通过使用 -T 标志来启用污染检查,你可以将其与 -w 结合使用,如下所示

      #!/usr/local/bin/perl -Tw

-T 将阻止 Perl 运行大多数可能是不安全的代码。如果你尝试使用污染变量进行各种危险的操作,如打开文件进行写入或使用 system()exec() 函数来运行外部命令,Perl 将立即停止并抱怨。

通过运行正则表达式并进行匹配子表达式的操作,你可以 取消污染 一个变量,并使用子表达式的结果。Perl 将认为 $1$2 等是安全的,适用于你的程序。

例如,我们的文件写入 CGI 程序可能期望“正常”的文件名只包含由 \w 元字符匹配的字母数字字符(这将防止恶意用户传递像 ~/.bashrc../test 这样的文件名)。我们会使用如下过滤器

       $file = param('file');
       if ($file) {
           $file =~ /^(\w+)$/;
           $file = $1;
       }

       unless ($file) { $file = "file.txt"; }

现在,$file 可以保证是取消污染的。如果用户传递给我们一个文件名,我们不会使用它,直到我们确定它只匹配 \w+。如果没有提供文件名,那么我们在程序中指定一个默认值。至于 $comment,我们实际上从未做过任何可能引起 Perl 污染检查担忧的操作,因此不需要检查即可通过 -T

污染检查无法捕获的内容

小心!即使你已经启用了污染检查,你仍然可以编写一个不安全的程序。记住,污染只在尝试通过打开文件或运行程序来修改系统时才会被查看。从文件中读取不会触发污染!一种非常常见的安全漏洞利用代码与这个小程序看起来没有太大区别

        #!/usr/local/bin/perl -Tw

        use CGI ':standard';

        $file = param('filename');
        unless ($file) { $file = 'file.txt'; }

        open (FILE, "</etc/webstuff/storage/" . $file) or die "$!";

        print header();
        while ($line = <FILE>) {
            print $line;
        }

        close FILE;

只需想象当“文件名”参数包含 ../../../../../../etc/passwd 时是多么的令人愉快。(如果你没有看到问题:在 Unix 系统中,/etc/passwd 文件包含系统上所有用户名的列表,还可能包含他们加密的密码列表。这对于想要进入机器进行更多破坏的黑客来说是非常有用的信息。)由于你只是读取文件,Perl 的污染检查不会启动。同样,print 也不会触发污染检查,所以当你将任何用户输入写入文件时,你必须编写自己的值检查代码!

污染是安全中的一个良好开端,但并非终点。

use strict

警告和污染是防止程序做坏事的两大利器。如果你想走得更远,Perl 提供了 use strict。这两个简单的词可以放在任何程序的开始处

        #!/usr/local/bin/perl -wT

        use strict;

use strict 这样的命令称为 声明。声明是给 Perl 解释器运行的指令,当它运行你的程序时执行一些特殊操作。use strict 做了两件事,使得编写坏的软件更加困难:它让你声明所有变量(“严格的变量”),并使 Perl 在你使用子程序时更难以误解你的意图(“严格的子程序”)。

如果你只想在你的程序中使用一种或两种严格类型,你可以在 use strict 声明中列出它们,或者你可以使用特殊的 no strict 声明来关闭之前启用的任何或所有严格类型。

        use strict 'vars';   # We want to require variables to be declared
        no strict 'vars';    # We'll go back to normal variable rules now

        use strict 'subs';   # We want Perl to distrust barewords (see below).

        no strict;           # Turn it off. Turn it all off. Go away, strict.

(实际上还有一种严格的类型——严格的引用——它防止你使用符号引用。由于我们尚未真正处理引用,我们将集中讨论其他两种严格类型。)

严格的变量

Perl 通常对变量持信任态度。它允许你从无到有地创建它们,这是我们迄今为止在程序中所做的。使你的程序更加正确的一种方法是用 严格的变量,这意味着你必须在使用变量之前始终 声明 它们。你可以通过使用 my 关键字来声明变量,无论是给它们赋值还是在你第一次提到它们之前

        my ($i, $j, @locations);
        my $filename = "./logfile.txt";
        $i = 5;

这种使用 my 的方法不会干扰你在其他地方使用它,比如在子程序中,并且记住子程序中的 my 变量将替换掉程序其他部分的那个变量

        my ($i, $j, @locations);
        # ... stuff skipped ...
        sub fix {
            my ($q, $i) = @_;  # This doesn't interfere with the program $i!
        }

如果你最终使用了一个未声明的变量,你的程序运行前会出现错误

        use strict;
        $i = 5;
        print "The value is $i.\n";

当你尝试运行这个程序时,你会看到一个类似这样的错误消息:全局符号 “$i” 需要显式包名在 a6-my.pl 行 3。 你可以通过在程序中声明 $i 来解决这个问题

        use strict;
        my $i = 5;   # Or "my ($i); $i = 5;", if you prefer...
        print "The value is $i.\n";

请注意,一些 严格的变量所做的事情将与 -w 标志重叠,但并非全部。同时使用这两个标志会使使用不正确的变量名变得非常困难,但并非不可能。例如,严格的变量 不会 捕获你意外使用错误变量的情况

         my ($i, $ii) = (1, 2);
         print 'The value of $ii is ', $i, "\n";

此代码有错误,但严格的变量和 -w 标志都不会捕获它。

严格的子程序

在这系列的课程中,我故意没有提及所有那些让你能够写出更 紧凑 的 Perl 的技巧。这是因为有一个简单的规则:可读性总是优先。紧凑性不仅会使代码难以阅读,有时还可能产生奇怪的反效果!Perl 在你的程序中查找子程序的方式就是一个例子。看看这两个三行程序

       $a = test_value;
       print "First program: ", $a, "\n";
       sub test_value { return "test passed"; }

       sub test_value { return "test passed"; }
       $a = test_value;
       print "Second program: ", $a, "\n";

同样的程序,只不过有一行小而无关紧要的行被移动了,对吧?在两种情况下我们都有一个 test_value() 子程序,我们想将其结果放入 $a 中。然而,当我们运行这两个程序时,我们得到两个不同的结果

       First program's result: test_value
       Second program's result: test passed

我们得到两个不同结果的原因有点复杂。

在第一个程序中,当我们到达 $a = test_value; 时,Perl 还不知道任何 test_value() 子程序,因为它还没有走到那里。这意味着 test_value 被解释为字符串 'test_value'。

在第二个程序中,test_value() 的定义在 $a = test_value; 行之前。由于 Perl 有一个可调用的 test_value() 子程序,这就是它认为 test_value 的含义。

test_value这样的孤立单词的术语,可能是子例程也可能是字符串,这取决于上下文,顺便说一句,是裸词。Perl对裸词的处理可能会让人困惑,并且可能导致两种不同类型的错误。

想要一个子例程,就得到一个字符串

第一种类型的错误就是我们第一次程序中遇到的,我会在下面重复

        $a = test_value;
        print "First program: ", $a, "\n";
        sub test_value { return "test passed"; }

记住,Perl不会向前查找test_value(),所以因为它已经看到了test_value(),它假设你想要一个字符串。严格的子例程会导致这个程序因错误而终止

        use strict;

        my $a = test_value;
        print "Third program: ", $a, "\n";
        sub test_value { "test passed"; }

(注意,插入my是为了确保严格的变量不会对$a提出抱怨。)

现在你会看到一个错误消息,比如“在启用“strict subs”的情况下不允许裸词“test_value”在./a6-strictsubs.pl行3。”这很容易修复,有两种方法可以做到

  1. 使用括号清楚地表明你在调用子例程。如果Perl看到$a = test_value();,它会假设即使它还没有看到test_value()的定义,它也会在程序结束之前。如果程序中没有test_value(),Perl将在运行时终止。这是最简单的事情,也是最易读的。

  2. 在你第一次使用它之前声明你的子例程,就像这样

        use strict;
    
        sub test_value;  # Declares that there's a test_value() coming later ...
        my $a = test_value;  # ...so Perl will know this line is okay.
        print "Fourth program: ", $a, "\n";
        sub test_value { return "test_passed"; }
    

声明你的子例程的好处是允许你维护$a = test_value;语法,如果你觉得它更易读,但它也有点晦涩。其他程序员可能不会明白为什么你的代码中有sub test_value;

当然,你总是可以把你的子例程定义移动到你想调用的行之前。这并不像其他两种方法那么好,因为你现在是在移动代码而不是使现有的代码更清晰。此外,它还可能引起其他问题,我们现在会讨论……

想要一个字符串,就得到一个子例程

我们已经看到use strict如何帮助防止你打算调用一个子例程,但得到一个字符串值时的错误。它还有助于防止相反的错误:想要一个字符串值,但调用了一个子例程。这是一类更危险的错误,因为它可能非常难以追踪,并且它通常出现在最意想不到的地方。看看这个长程序中的一段摘录

        #!/usr/local/bin/perl -Tw

        use strict;

        use SomeModule;
        use SomeOtherModule;
        use YetAnotherModule;

        # ... (and then there's hundreds of lines of code) ...

        # Now we get to line 400 of the program, which tests if we got an "OK"
        # before we act on a request from the user.
        if ($response_code eq OK) {
            act_on($request);
        } else {
            throw_away($request);
        }

这个程序在没有故障的情况下运行了很长时间,因为Perl看到了裸词OK,并认为它是一个字面字符串。然后,两年后,有人需要添加代码来使这个程序理解HTTP状态码。他们在第2行、第180行或第399行(具体在哪里并不重要,只要它在第400行之前)添加了这个代码

        sub OK { return 200; } # HTTP "request ok, response follows" code
        sub NOT_FOUND { return 404; } # "URL not found" code
        sub SERVER_ERROR { return 500; } # "Server can't handle request"

花点时间猜测我们的程序现在会发生什么。尽量在“灾难”这个词中找到一些线索。

多亏了这个小小的变化,我们的程序现在丢弃了所有送入它的请求。现在的if ($response eq OK)测试现在调用OK()子例程,它返回一个值为200的值。现在每次都会失败!如果程序员在这次混乱之后还有工作,他必须翻遍整个程序,才能找出if ($response eq OK)的行为何时改变,以及为什么。

顺便说一句,如果程序员真的很不幸,那个新的OK()子例程甚至根本不在他们的代码中,而是在新安装的SomeOtherModule.pm的新版本中定义的!

裸词之所以危险,是因为这种不可预测的行为。use strict(或use strict 'subs')使它们可预测,因为可能导致未来奇怪行为的裸词将在它们造成破坏之前使你的程序终止。

一个例外

有一个地方即使在启用严格子例程的情况下也可以使用裸词:当你正在分配散列键时。

        $hash{sample} = 6;   # Same as $hash{'sample'} = 6
        %other_hash = ( pie => 'apple' );

散列键中的裸词始终被解释为字符串,因此没有歧义。

这是否有点过度了?

在使用 Perl 提供的所有质量执行功能(或者如果你喜欢拟人化,称之为“正确性警察”)时,有时会感觉过度。如果你只是搭建一个简单、三行的工具,只用一次就不再使用,你可能不会关心它是否能在 use strict 下正确运行。当你是唯一运行程序的人时,你通常不会关心 -T 标志是否会显示你在尝试对用户输入进行不安全操作。

尽管如此,利用你拥有的每一个工具来编写优秀的软件是个好主意。以下是当你编写几乎任何东西时关注正确性的三个原因:

  1. 一次性程序不常见。 很少有值得编写的程序只运行一次。软件工具往往会积累并投入使用。你会发现,你使用一个程序越多,你越希望它能够做更多的事情。

  2. 其他人会阅读你的代码。 当程序员编写真正优秀的东西时,他们往往会保留它,并将其提供给有相同问题的朋友。更重要的是,大多数项目不是一个人的工作;有许多程序员需要一起工作,阅读、修复和扩展彼此的代码。除非你的未来计划是总是独自工作,而且没有朋友,否则你应该预计到将来其他人会阅读并修改你的代码。

  3. * 也会阅读自己的代码。* 不要认为你因为自己编写了代码就有特殊优势来理解它!你经常需要回到几个月甚至几年前的软件来修复或扩展它。在那段时间里,你会忘记那些你在咖啡因刺激的通宵达旦中想出的巧妙技巧,以及你注意到但认为稍后会修复的小问题。

这三个观点有一个共同点:你的程序 将会 被其他人重写和增强,他们会赞赏你为使他们的工作更轻松所做出的每一项努力。当你确保你的代码可读且正确时,它通常从更安全、更无错误的状态开始,并倾向于保持这种状态!

玩一玩!

在这个系列的进行过程中,我们只是触及了 Perl 能做什么的表面。不要把这些文章当作终极指南——它们只是入门!阅读 perlfunc 页面来了解 Perl 的所有内置函数,看看它们激发出了什么想法。我的个人资料页面会告诉你如何联系我,如果你有任何问题。

标签

反馈

这篇文章有什么问题吗?请通过在 GitHub 上打开问题或拉取请求来帮助我们。