堵住 lisp 宏的漏洞
本文的内容来自于 Practical Common Lisp
先来一个有漏洞的宏作为示例
;;判断一个数是否为质数 (defun primep (number) (when (> number 1) (loop for fac from 2 to (isqrt number) never (zerop (mod number fac))))) ;;返回一个质数的下一个质数(若当前这个数是质数则返回该数) (defun next-prime (number) (loop for n from number when (primep n) return n)) ;;这是一个只对变量在 start 至 end 区域内(包括边界)的质数执行 body 的宏 (defmacro do-primes ((var start end) &body body) `(do ((,var (next-prime ,start) (next-prime (1+ ,var)))) ((> ,var ,end)) ,@body))
这里的 &body
和 &rest
是等价的意思,但是在调用 do-primes
时,两者会有不同的缩进。
do-primes
的漏洞连锁
执行以下代码,来看看 do-primes
里的第一个漏洞:
(do-primes (p 0 (random 100)) (format t "~d " p))
什么漏洞?看起来好像很正常。
用宏展开的命令一探以上代码的究竟:
(macroexpand-1 '(do-primes (p 0 (random 100)) (format t "~d " p)))
(DO ((P (NEXT-PRIME 0) (NEXT-PRIME (1+ P)))) ((> P (RANDOM 100))) (FORMAT T "~d " P)) T
可以看到,在每次判定结束的时候,都重新生成了一个 100 以内的随机数。在宏 do-primes
里, (RANDOM 100)
并未在传进去的时候就计算,而是 按原样传进去了1,然后在 do-primes
宏展开后再考虑计算问题!
那么怎么修复这个漏洞呢?很简单,想办法在宏内让 (random 100)
只执行一次,好,按这个思路修改 do-primes
宏的定义代码:
(defmacro do-primes ((var start end) &body body) `(do ((ending-value ,end) (,var (next-prime ,start) (next-prime (1+ ,var)))) ((> ,var ending-value)) ,@body))
等会儿,这个修改引入了一个 不算太严重的 问题: end 比 start 先求值了 ,这在这段代码里可能不算什么问题,但是考虑到通用性,还是应该这样写:
(defmacro do-primes ((var start end) &body body) `(do ((,var (next-prime ,start) (next-prime (1+ ,var))) (ending-value ,end)) ((> ,var ending-value)) ,@body))
嗯,修复这个漏洞很简单,交换一下就行了,不过目前这个 do-primes
形式有一个更隐秘且更严重的新漏洞,尝试以下代码:
(do-primes (ending-value 0 10) (print ending-value))
宏展开看看:
(macroexpand-1 '(do-primes (ending-value 0 10) (print ending-value)))
(DO ((ENDING-VALUE (NEXT-PRIME 0) (NEXT-PRIME (1+ ENDING-VALUE))) (ENDING-VALUE 10)) ((> ENDING-VALUE 10)) (PRINT ENDING-VALUE)) T
见鬼, ending-value
和宏定义里的 ending-value
重合了! ending-value
会一直停留在 10 这个值上,导致死循环!
为了修复这个漏洞,我们需要在宏定义里使用永远不会在宏定义外面被用到的符号。但是考虑到外界的无限可能性,这种符号在哪里找呢?
函数 GENSYM
提供了答案。 GENSYM
每次被调用都会返回唯一的一个符号——不曾在之前的 lisp 代码中出现过——以后也不会。基于这个函数,重写 do-primes
!:
(defmacro do-primes ((var start end) &body body) (let ((ending-value-name (gensym))) `(do ((,var (next-prime ,start) (next-prime (1+ ,var))) (,ending-value-name ,end)) ((> ,var ,ending-value-name)) ,@body)))
由于 let
的部分是在 do-primes
定义的时候就执行的 ,所以 ending-value-name
变量只在本地有效,外部不会有重名。这里仅为给新人的提醒。
之后,对 var
start
做和 end
一样的处理,这些漏洞就补上了。
填补 lisp 漏洞的总结
使用上述三方法,就能消除一些大部分的 lisp 宏漏洞,减少宏抽象的细节泄露。
事实上, 任何一种抽象手段,都有泄露抽象细节的问题 ,即使是被视为 史上最强抽象手段(包括未来史) 的 lisp 宏也无法避免。关于这点可以参看 The Law of Leaky Abstractions 。
脚注:
实际上对所有宏而言都是如此。
Generated by Emacs 26.x(Org mode 9.x)
Copyright © 2014 - 皐月中二 - Powered by EGO
- Analysized by