日拱一卒,麻省理工教你debug技巧,从此debug不再掉头发

2022-09-21 11:08:42 浏览数 (1)

作者 | 梁唐

出品 | 公众号:Coder梁(ID:Coder_LT)

大家好,日拱一卒,我是梁唐。

今天我们继续麻省理工missing smester课程——消失的课程,那些不会在课堂上讲授的重要技能。

这节课的内容有两块,一块是debug相关的技巧,另外一个部分是对系统的性能分析。由于整节课内容非常多,篇幅很长,所以分成了两期,用两篇文章写完。今天我们先来看看debug技巧的部分,对于每个程序员来说,debug一定是不可避免的,所以花点时间了解一下debug技巧非常非常有必要,能够大大提升之后开发过程的效率,降低寻找bug的痛苦。

B站视频链接:https://www.bilibili.com/video/BV1x7411H7wa?p=7

和之前一样,这节课的note质量同样非常高。大家可以点击「阅读原文」跳转英文原文。

本文是基于本节课note以及老师上课演示的内容,还有我个人的一些理解做的翻译整理版本。日拱一卒,欢迎大家打卡一起学习。

前言

编程当中有一个铁律:代码不是像你设想的那样运行的,而是像你告诉它的那样。有时候填平设想和实际的鸿沟是非常艰难的。在这节课当中,我们将会涵盖一些有用的技术来处理bug以及资源管理。

Debugging

打印调试日志

最高效的debug工具就是缜密的思考配合恰当的输出语句——Brain Kernighan,Unix for Beginners.

第一个debug程序的方式就是在你觉得可能出问题的地方加入一些print语句,持续迭代直到你搜集了足够多的信息了解到底是什么导致了问题。

第二个方案是在你的程序当中使用日志,而不是临时添加print语句。相比于简单的print语句,日志拥有以下优势:

  • 可以将日志写入文件、socket 或者甚至是发送到远端服务器而不仅仅是标准输出;
  • 日志可以支持严重等级(例如 INFO, DEBUG, WARN, ERROR等),这使您可以根据需要过滤日志;
  • 对于新发现的问题,很可能您的日志中已经包含了可以帮助您定位问题的足够的信息。

下面这段python代码是一个使用log的例子:

代码语言:javascript复制
import logging
import sys

class CustomFormatter(logging.Formatter):
    """Logging Formatter to add colors and count warning / errors"""

    grey = "x1b[38;21m"
    yellow = "x1b[33;21m"
    red = "x1b[31;21m"
    bold_red = "x1b[31;1m"
    reset = "x1b[0m"
    format = "%(asctime)s - %(name)s - %(levelname)s - %(message)s (%(filename)s:%(lineno)d)"

    FORMATS = {
        logging.DEBUG: grey   format   reset,
        logging.INFO: grey   format   reset,
        logging.WARNING: yellow   format   reset,
        logging.ERROR: red   format   reset,
        logging.CRITICAL: bold_red   format   reset
    }

    def format(self, record):
        log_fmt = self.FORMATS.get(record.levelno)
        formatter = logging.Formatter(log_fmt)
        return formatter.format(record)

# create logger with 'spam_application'
logger = logging.getLogger("Sample")

# create console handler with a higher log level
ch = logging.StreamHandler()
ch.setLevel(logging.DEBUG)

if len(sys.argv)> 1:
    if sys.argv[1] == 'log':
        ch.setFormatter(logging.Formatter('%(asctime)s : %(levelname)s : %(name)s : %(message)s'))
    elif sys.argv[1] == 'color':
        ch.setFormatter(CustomFormatter())

if len(sys.argv) > 2:
    logger.setLevel(logging.__getattribute__(sys.argv[2]))
else:
    logger.setLevel(logging.DEBUG)

logger.addHandler(ch)

# logger.debug("debug message")
# logger.info("info message")
# logger.warning("warning message")
# logger.error("error message")
# logger.critical("critical message")

import random
import time
for _ in range(100):
    i = random.randint(0, 10)
    if i <= 4:
        logger.info("Value is {} - Everything is fine".format(i))
    elif i <= 6:
        logger.warning("Value is {} - System is getting hot".format(i))
    elif i <= 8:
        logger.error("Value is {} - Dangerous region".format(i))
    else:
        logger.critical("Maximum value reached")
    time.sleep(0.3)

一个我最喜欢的记录日志的技巧是对日志进行上色。你可能已经发现了你的终端使用一些高亮颜色让内容变得更加易读。但它是怎么实现的呢?像是grep这样的程序使用了ANSI escape codes:https://en.wikipedia.org/wiki/ANSI_escape_code,它是一系列特殊的字符,可以让你的shell改变输出结果的颜色。

比如运行echo -e "e[38;2;255;0;0mThis is rede[0m"会在终端打印出红色的字符串This is red

img

前提是你的终端支持true color,如果不支持的话(比如macOS的terminal就不支持),你可以使用更加通用的16色,比如echo -e "e[31;1mThis is rede[0m"

接下来的脚本演示了如何在终端当中输出多种RGB颜色(同样这需要支持true color)

代码语言:javascript复制
#!/usr/bin/env bash
for R in $(seq 0 20 255); do
    for G in $(seq 0 20 255); do
        for B in $(seq 0 20 255); do
            printf "e[38;2;${R};${G};${B}m█e[0m";
        done
    done
done

第三方log

当你开始创建大规模的软件系统的时候,你可能会使用一些依赖,这些依赖可能会独立运行。网页服务器、数据库以及消息代理都是常见的第三方依赖。当和这些系统交互的时候,不可避免地会需要阅读它们的log,因为仅仅靠客户端的错误信息不足以定位问题。

庆幸的是,大多数程序都会把它们的日志记录在你的系统的某处。在UNIX系统当中,程序通常会把它们的日志写在/var/log当中。比如NGINX服务器将它的日志写在/var/log/nginx

目前系统开始使用系统日志,你所有的日志消息都会存在这里。大多数(不是全部)Linux系统使用systemd,这是一个系统守护进程,控制你系统当中的许多东西,比如一些服务的启动和运行。systemd将特殊格式的日志存放在/var/log/journal中,你可以使用journalctl命令来展示这些消息。类似的,在macOS上仍然有/var/log/system.log,但越来越多的工具开始使用系统日志,这些日志可以通过log show展示。对于大多数UNIX系统来说,你可以使用dmesg命令来访问内核日志。

你可以使用logger shell程序来记录系统日志,下面是一个使用logger记录日志系统日志,以及进行查询的例子。并且,大多数编程语言都支持向系统日志当中记录日志。

代码语言:javascript复制
logger "Hello Logs"
# On macOS
log show --last 1m | grep Hello
# On Linux
journalctl --since "1m ago" | grep Hello

就像是我们在数据处理那节课上看到的一样,日志通常非常冗长,并且需要进行一定程度的处理和过滤才能提取出有用的信息。如果你发现通过jounrnalog show进行过滤非常麻烦,你可以试试使用它们的flag,可以先对结果进行一波过滤。同样也有类似于lnav这样的工具,为日志提供了更好的浏览和导航。

Debuggers

当我们打印debug信息已经不足以找到问题的时候,你可以考虑使用debugger(调试器)。调试器是一些允许你可以交互式执行程序的工具,它允许你进行以下的操作:

  • 当到达某一行时将程序暂停;
  • 一次一条指令地逐步执行程序;
  • 程序崩溃后查看变量的值;
  • 满足特定条件时暂停程序;
  • 其他高级功能。

许多编程语言支持调试器,在Python当中,调试器是pdb

下面是对pdb支持的命令的一些简单介绍:

  • l(ist) - 显示当前行附近的11行或继续执行之前的显示;
  • s(tep) - 执行当前行,并在第一个可能的地方停止;
  • n(ext) - 继续执行直到当前函数的下一条语句或者 return 语句;
  • b(reak) - 设置断点(基于传入的参数);
  • p(rint) - 在当前上下文对表达式求值并打印结果。还有一个命令是pp ,它使用 pprint 打印;
  • r(eturn) - 继续执行直到当前函数返回;
  • q(uit) - 退出调试器。

让我们使用pdb来修复下面的Python代码(参考讲课视频)

代码语言:javascript复制
def bubble_sort(arr):
    n = len(arr)
    for i in range(n):
        for j in range(n):
            if arr[j] > arr[j 1]:
                arr[j] = arr[j 1]
                arr[j 1] = arr[j]
    return arr

print(bubble_sort([4, 2, 1, 8, 7, 6]))

注意,由于Python是解释型语言,我们可以使用pdbshell来执行命令。ipdbpdb的一个改进版本,使用IPython 并作为REPL开启了tab不全、语法高亮、更好的回溯以及更好的检查,同时还保持了和pdb模块相同的接口。

对于一些更底层的语言,你可以使用gdb(它的改进版pwndbg)和lldb。它们都对C-like语言调试做了优化,允许你探索任意进程以及获得当前机器的状态:寄存器、栈、程序计数器等等。

特定工具

甚至当你在一个黑盒二进制文件中debug的时候,都有特定的工具可以帮到你。程序执行某些特定操作的时候必须要通过操作系统内核,这需要用到system call。有一些命令可以让你追踪你程序执行的system call。在Linux当中叫做strace在macOS和BSD当中有dtrace

dtrace用起来可能比较别扭,因为它使用它自有的D语言,但也有封装好的叫做dtruss的工具,提供和strace相似的接口。

下面是使用strace或者dtruss来展示执行ls时,对stat system call的调用结果。如果想要深度了解strace,可以阅读这两篇文章:https://blogs.oracle.com/linux/post/strace-the-sysadmins-microscope,https://jvns.ca/strace-zine-unfolded.pdf

代码语言:javascript复制
# On Linux
sudo strace -e lstat ls -l > /dev/null
4
# On macOS
sudo dtruss -t lstat64_extended ls -l > /dev/null

对于 web 开发, Chrome/Firefox 的开发者工具非常方便,功能也很强大:

  • 源码 -查看任意站点的 HTML/CSS/JS 源码;
  • 实时地修改 HTML, CSS, JS 代码 - 修改网站的内容、样式和行为用于测试(从这一点您也能看出来,网页截图是不可靠的);
  • Javascript shell - 在 JS REPL中执行命令;
  • 网络 - 分析请求的时间线;
  • 存储 - 查看 Cookies 和本地应用存储。

静态分析

对于一些问题,你不需要执行代码就可以发现。比如说,仔细观察一段代码,你就能发现你的循环变量覆盖了一个已有的变量名或函数名。或者是有一个变量在读取之前没有被定义。这种情况下静态分析工具就可以派上用场了。静态分析工具将源代码作为输入,基于编程规则对它进行分析,找出其中的问题。

在下面这个Python代码片段当中存在一些错误。首先,我们的循环变量foo覆盖了先前定义的函数foo。我们同样把最后一行的变量bar写成了baz,所以程序完成sleep之后会崩溃。

代码语言:javascript复制
import time

def foo():
    return 42

for foo in range(5):
    print(foo)
bar = 1
bar *= 0.2
time.sleep(60)
print(baz)

静态分析工具可以定位这类问题。当我们运行pyflakes之后,我们可以得到一些关于bug的错误提醒。mypy是另外一个工具,可以帮助我们检查类型问题。这里mypy将会警告我们bar这个变量一开始是int类型,但后来被强制转换成了float。再次强调,这些问题都可以在不执行代码的情况下被发现。

代码语言:javascript复制
$ pyflakes foobar.py
foobar.py:6: redefinition of unused 'foo' from line 3
foobar.py:11: undefined name 'baz'

$ mypy foobar.py
foobar.py:6: error: Incompatible types in assignment (expression has type "int", variable has type "Callable[[], Any]")
foobar.py:9: error: Incompatible types in assignment (expression has type "float", variable has type "int")
foobar.py:11: error: Name 'baz' is not defined
Found 3 errors in 1 file (checked 1 source file)

在shell工具那节课当中,我们介绍了shellcheck,这是一个类似的工具,不过是用来检查shell脚本的。

大多数编辑器和IDE支持将这些工具的输出结果展示在界面里,对有警告和错误的地方进行高亮。这个过程通常被称为code linting。同样,其他类型的问题也可以同样被展示,比如代码风格检查和安全检查。

在vim当中,alesyntastic插件可以让你做到这些。对于Python来说,pylintpep8是两种用于进行代码风格检查的工具。bandit则可以用来进行安全检查。

对于其他语言来说,人们编译、整合了一系列拥有的静态检查的工具,比如awesome static analysis:https://github.com/analysis-tools-dev/static-analysis(你可以查看一下Writing章节),对于lint工具,也有awesome linters:https://github.com/caramelomartins/awesome-linters

一个完善的工具用来做风格检查通常被叫做code formatter,比如Python中的black,Go中的gofmt,Rust中的rustfmt或者是JavaScript、HTML、CSS中的prettier。这些工具会自动格式化你的代码,让你的代码和常用的风格保持统一。虽然你可能并不想让这些工具控制你的代码,但标准的代码格式可以帮助其他人更好的理解和阅读你的代码,同样也会更方便你去阅读其他人的代码。

Exercises

  1. 使用Linux中的journalctl或者是macOS中的log show来获取最近一天超级用户(root)登录以及所执行的命令。如果找不到任何记录,你可以手动执行一些无伤大雅的命令,比如ls
  2. 学习这份pdb指南:https://github.com/spiside/pdb-tutorial,并熟悉相关命令,你可以参考这份教程:https://realpython.com/python-debugging-pdb/
  3. 安装shellcheck并且尝试对下面的脚本进行检查,它当中有什么问题?修复它。在你的编辑器当中安装一个linter插件,这样的话它可以自动警告你
代码语言:javascript复制
#!/bin/sh
## Example: a typical script with several problems
for f in $(ls *.m3u)
do
  grep -qi hq.*mp3 $f 
    && echo -e 'Playlist $f contains a HQ file in mp3 format'
done

4.(进阶)请阅读这份可逆调试文档:https://undo.io/resources/reverse-debugging-whitepaper/,并使用rr或者RevPDB运行一个简单的例子

喜欢本文的话不要忘记三连~

0 人点赞