分类 软件开发 下的文章

Twitter 允许用户将博客帖子和文章分享给全世界。使用 Python 和 Tweepy 库使得创建一个 Twitter 机器人来接管你的所有的推特变得非常简单。这篇文章告诉你如何去构建这样一个机器人。希望你能将这些概念也同样应用到其他的在线服务的项目中去。

开始

tweepy 库可以让创建一个 Twitter 机器人的过程更加容易上手。它包含了 Twitter 的 API 调用和一个很简单的接口。

下面这些命令使用 pipenv 在一个虚拟环境中安装 tweepy。如果你没有安装 pipenv,可以看一看我们之前的文章如何在 Fedora 上安装 Pipenv

$ mkdir twitterbot
$ cd twitterbot
$ pipenv --three
$ pipenv install tweepy
$ pipenv shell

Tweepy —— 开始

要使用 Twitter API ,机器人需要通过 Twitter 的授权。为了解决这个问题, tweepy 使用了 OAuth 授权标准。你可以通过在 https://apps.twitter.com/ 创建一个新的应用来获取到凭证。

创建一个新的 Twitter 应用

当你填完了表格并点击了“ 创建你自己的 Twitter 应用 Create your Twitter application ”的按钮后,你可以获取到该应用的凭证。 Tweepy 需要 用户密钥 API Key 用户密码 API Secret ,这些都可以在 “ 密钥和访问令牌 Keys and Access Tokens ” 中找到。

向下滚动页面,使用“ 创建我的访问令牌 Create my access token ”按钮生成一个“ 访问令牌 Access Token ” 和一个“ 访问令牌密钥 Access Token Secret ”。

使用 Tweppy —— 输出你的时间线

现在你已经有了所需的凭证了,打开一个文件,并写下如下的 Python 代码。

import tweepy
auth = tweepy.OAuthHandler("your_consumer_key", "your_consumer_key_secret")
auth.set_access_token("your_access_token", "your_access_token_secret")
api = tweepy.API(auth)
public_tweets = api.home_timeline()
for tweet in public_tweets:
    print(tweet.text)

在确保你正在使用你的 Pipenv 虚拟环境后,执行你的程序。

$ python tweet.py

上述程序调用了 home_timeline 方法来获取到你时间线中的 20 条最近的推特。现在这个机器人能够使用 tweepy 来获取到 Twitter 的数据,接下来尝试修改代码来发送 tweet。

使用 Tweepy —— 发送一条推特

要发送一条推特 ,有一个容易上手的 API 方法 update_status 。它的用法很简单:

api.update_status("The awesome text you would like to tweet")

Tweepy 拓展为制作 Twitter 机器人准备了非常多不同有用的方法。要获取 API 的详细信息,请查看文档

一个杂志机器人

接下来我们来创建一个搜索 Fedora Magazine 的推特并转推这些的机器人。

为了避免多次转推相同的内容,这个机器人存放了最近一条转推的推特的 ID 。 两个助手函数 store_last_idget_last_id 将会帮助存储和保存这个 ID。

然后,机器人使用 tweepy 搜索 API 来查找 Fedora Magazine 的最近的推特并存储这个 ID。

import tweepy

def store_last_id(tweet_id):
    """ Stores a tweet id in text file """
    with open('lastid', 'w') as fp:
        fp.write(str(tweet_id))


def get_last_id():
    """ Retrieve the list of tweets that were
    already retweeted """

    with open('lastid') as fp:
        return fp.read()

if __name__ == '__main__':

    auth = tweepy.OAuthHandler("your_consumer_key", "your_consumer_key_secret")
    auth.set_access_token("your_access_token", "your_access_token_secret")

    api = tweepy.API(auth)

    try:
        last_id = get_last_id()
    except FileNotFoundError:
        print("No retweet yet")
        last_id = None

    for tweet in tweepy.Cursor(api.search, q="fedoramagazine.org", since_id=last_id).items():
        if tweet.user.name  == 'Fedora Project':
            store_last_id(tweet.id)
            #tweet.retweet()
            print(f'"{tweet.text}" was retweeted')

为了只转推 Fedora Magazine 的推特 ,机器人搜索内容包含 fedoramagazine.org 和由 「Fedora Project」 Twitter 账户发布的推特。

结论

在这篇文章中你看到了如何使用 tweepy 的 Python 库来创建一个自动阅读、发送和搜索推特的 Twitter 应用。现在,你能使用你自己的创造力来创造一个你自己的 Twitter 机器人。

这篇文章的演示源码可以在 Github 找到。


via: https://fedoramagazine.org/learn-build-twitter-bot-python/

作者:Clément Verna 选题:lujun9972 译者:Bestony 校对:校对者ID

本文由 LCTT 原创编译,Linux中国 荣誉推出

用 Perlbrew 在你系统上安装多个版本的 Perl。

有比在系统上安装了 Perl 更好的事情吗?那就是在系统中安装了多个版本的 Perl。使用 Perlbrew 你可以做到这一点。但是为什么呢,除了让你包围在 Perl 下之外,有什么好处吗?

简短的回答是,不同版本的 Perl 是......不同的。程序 A 可能依赖于较新版本中不推荐使用的行为,而程序 B 需要去年无法使用的新功能。如果你安装了多个版本的 Perl,则每个脚本都可以使用最适合它的版本。如果您是开发人员,这也会派上用场,你可以针对多个版本的 Perl 测试你的程序,这样无论你的用户运行什么,你都知道它能否工作。

安装 Perlbrew

另一个好处是 Perlbrew 会安装 Perl 到用户的家目录。这意味着每个用户都可以管理他们的 Perl 版本(以及相关的 CPAN 包),而无需与系统管理员联系。自助服务意味着为用户提供更快的安装,并为系统管理员提供更多时间来解决难题。

第一步是在你的系统上安装 Perlbrew。许多 Linux 发行版已经在包仓库中拥有它,因此你只需要 dnf install perlbrew(或者适用于你的发行版的命令)。你还可以使用 cpan App::perlbrew 从 CPAN 安装 App::perlbrew 模块。或者你可以在 install.perlbrew.pl 下载并运行安装脚本。

要开始使用 Perlbrew,请运行 perlbrew init

安装新的 Perl 版本

假设你想尝试最新的开发版本(撰写本文时为 5.27.11)。首先,你需要安装包:

perlbrew install 5.27.11

切换 Perl 版本

现在你已经安装了新版本,你可以将它用于该 shell:

perlbrew use 5.27.11

或者你可以将其设置为你帐户的默认 Perl 版本(假设你按照 perlbrew init 的输出设置了你的配置文件):

perlbrew switch 5.27.11

运行单个脚本

你也可以用特定版本的 Perl 运行单个命令:

perlberew exec 5.27.11 myscript.pl

或者,你可以针对所有已安装的版本运行命令。如果你想针对各种版本运行测试,这尤其方便。在这种情况下,请指定版本为 perl

plperlbrew exec perl myscriptpl

安装 CPAN 模块

如果你想安装 CPAN 模块,cpanm 包是一个易于使用的界面,可以很好地与 Perlbrew 一起使用。用下面命令安装它:

perlbrew install-cpanm

然后,你可以使用 cpanm 命令安装 CPAN 模块:

cpanm CGI::simple

但是等下,还有更多!

本文介绍了基本的 Perlbrew 用法。还有更多功能和选项可供选择。从查看 perlbrew help 的输出开始,或查看App::perlbrew 文档。你还喜欢 Perlbrew 的其他什么功能?让我们在评论中知道。


via: https://opensource.com/article/18/7/perlbrew

作者:Ben Cotton 选题:lujun9972 译者:geekpi 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

3 个可以使你的 Python 代码更优雅、可读、直观和易于维护的工具。

Python 提供了一组独特的工具和语言特性来使你的代码更加优雅、可读和直观。为正确的问题选择合适的工具,你的代码将更易于维护。在本文中,我们将研究其中的三个工具:魔术方法、迭代器和生成器,以及方法魔术。

魔术方法

魔术方法可以看作是 Python 的管道。它们被称为“底层”方法,用于某些内置的方法、符号和操作。你可能熟悉的常见魔术方法是 __init__(),当我们想要初始化一个类的新实例时,它会被调用。

你可能已经看过其他常见的魔术方法,如 __str____repr__。Python 中有一整套魔术方法,通过实现其中的一些方法,我们可以修改一个对象的行为,甚至使其行为类似于内置数据类型,例如数字、列表或字典。

让我们创建一个 Money 类来示例:

class Money:

    currency_rates = {
        '$': 1,
        '€': 0.88,
    }

    def __init__(self, symbol, amount):
        self.symbol = symbol
        self.amount = amount

    def __repr__(self):
        return '%s%.2f' % (self.symbol, self.amount)

    def convert(self, other):
        """ Convert other amount to our currency """
        new_amount = (
            other.amount / self.currency_rates[other.symbol]
            * self.currency_rates[self.symbol])

        return Money(self.symbol, new_amount)

该类定义为给定的货币符号和汇率定义了一个货币汇率,指定了一个初始化器(也称为构造函数),并实现 __repr__,因此当我们打印这个类时,我们会看到一个友好的表示,例如 $2.00 ,这是一个带有货币符号和金额的 Money('$', 2.00) 实例。最重要的是,它定义了一种方法,允许你使用不同的汇率在不同的货币之间进行转换。

打开 Python shell,假设我们已经定义了使用两种不同货币的食品的成本,如下所示:

>>> soda_cost = Money('$', 5.25)
>>> soda_cost
    $5.25

>>> pizza_cost = Money('€', 7.99)
>>> pizza_cost
    €7.99

我们可以使用魔术方法使得这个类的实例之间可以相互交互。假设我们希望能够将这个类的两个实例一起加在一起,即使它们是不同的货币。为了实现这一点,我们可以在 Money 类上实现 __add__ 这个魔术方法:

class Money:

    # ... previously defined methods ...

    def __add__(self, other):
        """ Add 2 Money instances using '+' """
        new_amount = self.amount + self.convert(other).amount
        return Money(self.symbol, new_amount)

现在我们可以以非常直观的方式使用这个类:

>>> soda_cost = Money('$', 5.25)
>>> pizza_cost = Money('€', 7.99)
>>> soda_cost + pizza_cost
    $14.33
>>> pizza_cost + soda_cost
    €12.61

当我们将两个实例加在一起时,我们得到以第一个定义的货币符号所表示的结果。所有的转换都是在底层无缝完成的。如果我们想的话,我们也可以为减法实现 __sub__,为乘法实现 __mul__ 等等。阅读模拟数字类型魔术方法指南来获得更多信息。

我们学习到 __add__ 映射到内置运算符 +。其他魔术方法可以映射到像 [] 这样的符号。例如,在字典中通过索引或键来获得一项,其实是使用了 __getitem__ 方法:

>>> d = {'one': 1, 'two': 2}
>>> d['two']
2
>>> d.__getitem__('two')
2

一些魔术方法甚至映射到内置函数,例如 __len__() 映射到 len()

class Alphabet:
    letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'

    def __len__(self):
        return len(self.letters)

>>> my_alphabet = Alphabet()
>>> len(my_alphabet)
    26

自定义迭代器

对于新的和经验丰富的 Python 开发者来说,自定义迭代器是一个非常强大的但令人迷惑的主题。

许多内置类型,例如列表、集合和字典,已经实现了允许它们在底层迭代的协议。这使我们可以轻松地遍历它们。

>>> for food in ['Pizza', 'Fries']:

         print(food + '. Yum!')

Pizza. Yum!
Fries. Yum!

我们如何迭代我们自己的自定义类?首先,让我们来澄清一些术语。

  • 要成为一个可迭代对象,一个类需要实现 __iter__()
  • __iter__() 方法需要返回一个迭代器
  • 要成为一个迭代器,一个类需要实现 __next__()(或在 Python 2中是 next()),当没有更多的项要迭代时,必须抛出一个 StopIteration 异常。

呼!这听起来很复杂,但是一旦你记住了这些基本概念,你就可以在任何时候进行迭代。

我们什么时候想使用自定义迭代器?让我们想象一个场景,我们有一个 Server 实例在不同的端口上运行不同的服务,如 httpssh。其中一些服务处于 active 状态,而其他服务则处于 inactive 状态。

class Server:

    services = [
        {'active': False, 'protocol': 'ftp', 'port': 21},
        {'active': True, 'protocol': 'ssh', 'port': 22},
        {'active': True, 'protocol': 'http', 'port': 80},
    ]

当我们遍历 Server 实例时,我们只想遍历那些处于 active 的服务。让我们创建一个 IterableServer 类:

class IterableServer:
    def __init__(self):
        self.current_pos = 0
    def __next__(self):
        pass  # TODO: 实现并记得抛出 StopIteration

首先,我们将当前位置初始化为 0。然后,我们定义一个 __next__() 方法来返回下一项。我们还将确保在没有更多项返回时抛出 StopIteration。到目前为止都很好!现在,让我们实现这个 __next__() 方法。

class IterableServer:
    def __init__(self):
        self.current_pos = 0.  # 我们初始化当前位置为 0
    def __iter__(self):  # 我们可以在这里返回 self,因为实现了 __next__
        return self
    def __next__(self):
        while self.current_pos < len(self.services):
            service = self.services[self.current_pos]
            self.current_pos += 1
            if service['active']:
                return service['protocol'], service['port']
        raise StopIteration
    next = __next__  # 可选的 Python2 兼容性

我们对列表中的服务进行遍历,而当前的位置小于服务的个数,但只有在服务处于活动状态时才返回。一旦我们遍历完服务,就会抛出一个 StopIteration 异常。

因为我们实现了 __next__() 方法,当它耗尽时,它会抛出 StopIteration。我们可以从 __iter__() 返回 self,因为 IterableServer 类遵循 iterable 协议。

现在我们可以遍历一个 IterableServer 实例,这将允许我们查看每个处于活动的服务,如下所示:

>>> for protocol, port in IterableServer():

        print('service %s is running on port %d' % (protocol, port))

service ssh is running on port 22

service http is running on port 21

太棒了,但我们可以做得更好!在这样类似的实例中,我们的迭代器不需要维护大量的状态,我们可以简化代码并使用 generator(生成器) 来代替。

class Server:
    services = [
        {'active': False, 'protocol': 'ftp', 'port': 21},
        {'active': True, 'protocol': 'ssh', 'port': 22},
        {'active': True, 'protocol': 'http', 'port': 21},
    ]
    def __iter__(self):
        for service in self.services:
            if service['active']:
                yield service['protocol'], service['port']

yield 关键字到底是什么?在定义生成器函数时使用 yield。这有点像 return,虽然 return 在返回值后退出函数,但 yield 会暂停执行直到下次调用它。这允许你的生成器的功能在它恢复之前保持状态。查看 yield 的文档以了解更多信息。使用生成器,我们不必通过记住我们的位置来手动维护状态。生成器只知道两件事:它现在需要做什么以及计算下一个项目需要做什么。一旦我们到达执行点,即 yield 不再被调用,我们就知道停止迭代。

这是因为一些内置的 Python 魔法。在 Python 关于 __iter__() 的文档中我们可以看到,如果 __iter__() 是作为一个生成器实现的,它将自动返回一个迭代器对象,该对象提供 __iter__()__next__() 方法。阅读这篇很棒的文章,深入了解迭代器,可迭代对象和生成器

方法魔法

由于其独特的方面,Python 提供了一些有趣的方法魔法作为语言的一部分。

其中一个例子是别名功能。因为函数只是对象,所以我们可以将它们赋值给多个变量。例如:

>>> def foo():
       return 'foo'
>>> foo()
'foo'
>>> bar = foo
>>> bar()
'foo'

我们稍后会看到它的作用。

Python 提供了一个方便的内置函数称为 getattr(),它接受 object, name, default 参数并在 object 上返回属性 name。这种编程方式允许我们访问实例变量和方法。例如:

>>> class Dog:
        sound = 'Bark'
        def speak(self):
            print(self.sound + '!', self.sound + '!')

>>> fido = Dog()

>>> fido.sound
'Bark'
>>> getattr(fido, 'sound')
'Bark'

>>> fido.speak
<bound method Dog.speak of <__main__.Dog object at 0x102db8828>>
>>> getattr(fido, 'speak')
<bound method Dog.speak of <__main__.Dog object at 0x102db8828>>


>>> fido.speak()
Bark! Bark!
>>> speak_method = getattr(fido, 'speak')
>>> speak_method()
Bark! Bark!

这是一个很酷的技巧,但是我们如何在实际中使用 getattr 呢?让我们看一个例子,我们编写一个小型命令行工具来动态处理命令。

class Operations:
    def say_hi(self, name):
        print('Hello,', name)
    def say_bye(self, name):
        print ('Goodbye,', name)
    def default(self, arg):
        print ('This operation is not supported.')

if __name__ == '__main__':
    operations = Operations()
    # 假设我们做了错误处理
    command, argument = input('> ').split()
    func_to_call = getattr(operations, command, operations.default)
    func_to_call(argument)

脚本的输出是:

$ python getattr.py
> say_hi Nina
Hello, Nina
> blah blah
This operation is not supported.

接下来,我们来看看 partial。例如,functool.partial(func, *args, **kwargs) 允许你返回一个新的 partial 对象,它的行为类似 func,参数是 argskwargs。如果传入更多的 args,它们会被附加到 args。如果传入更多的 kwargs,它们会扩展并覆盖 kwargs。让我们通过一个简短的例子来看看:

>>> from functools import partial
>>> basetwo = partial(int, base=2)
>>> basetwo
<functools.partial object at 0x1085a09f0>
>>> basetwo('10010')
18

# 这等同于
>>> int('10010', base=2)

让我们看看在我喜欢的一个名为 agithub 的库中的一些示例代码中,这个方法魔术是如何结合在一起的,这是一个(名字起得很 low 的) REST API 客户端,它具有透明的语法,允许你以最小的配置快速构建任何 REST API 原型(不仅仅是 GitHub)。我发现这个项目很有趣,因为它非常强大,但只有大约 400 行 Python 代码。你可以在大约 30 行配置代码中添加对任何 REST API 的支持。agithub 知道协议所需的一切(RESTHTTPTCP),但它不考虑上游 API。让我们深入到它的实现中。

以下是我们如何为 GitHub API 和任何其他相关连接属性定义端点 URL 的简化版本。在这里查看完整代码

class GitHub(API):
    def __init__(self, token=None, *args, **kwargs):
        props = ConnectionProperties(api_url = kwargs.pop('api_url', 'api.github.com'))
        self.setClient(Client(*args, **kwargs))
        self.setConnectionProperties(props)

然后,一旦配置了访问令牌,就可以开始使用 GitHub API

>>> gh = GitHub('token')
>>> status, data = gh.user.repos.get(visibility='public', sort='created')
>>> # ^ 映射到 GET /user/repos
>>> data
... ['tweeter', 'snipey', '...']

请注意,你要确保 URL 拼写正确,因为我们没有验证 URL。如果 URL 不存在或出现了其他任何错误,将返回 API 抛出的错误。那么,这一切是如何运作的呢?让我们找出答案。首先,我们将查看一个 API的简化示例:

class API:
    # ... other methods ...
    def __getattr__(self, key):
        return IncompleteRequest(self.client).__getattr__(key)
    __getitem__ = __getattr__

API 类上的每次调用都会调用 IncompleteRequest作为指定的 key

class IncompleteRequest:
    # ... other methods ...
    def __getattr__(self, key):
        if key in self.client.http_methods:
            htmlMethod = getattr(self.client, key)
            return partial(htmlMethod, url=self.url)
        else:
            self.url += '/' + str(key)
            return self
    __getitem__ = __getattr__

class Client:
    http_methods = ('get')  # 还有 post, put, patch 等等。
    def get(self, url, headers={}, **params):
        return self.request('GET', url, None, headers)

如果最后一次调用不是 HTTP 方法(如 getpost 等),则返回带有附加路径的 IncompleteRequest。否则,它从Client获取 HTTP 方法对应的正确函数,并返回 partial

如果我们给出一个不存在的路径会发生什么?

>>> status, data = this.path.doesnt.exist.get()
>>> status
... 404

因为 __getattr__ 别名为 __getitem__

>>> owner, repo = 'nnja', 'tweeter'
>>> status, data = gh.repos[owner][repo].pulls.get()
>>> # ^ Maps to GET /repos/nnja/tweeter/pulls
>>> data
.... # {....}

这真心是一些方法魔术!

了解更多

Python 提供了大量工具,使你的代码更优雅,更易于阅读和理解。挑战在于找到合适的工具来完成工作,但我希望本文为你的工具箱添加了一些新工具。而且,如果你想更进一步,你可以在我的博客 nnja.io 上阅读有关装饰器、上下文管理器、上下文生成器和命名元组的内容。随着你成为一名更好的 Python 开发人员,我鼓励你到那里阅读一些设计良好的项目的源代码。RequestsFlask 是两个很好的起步的代码库。


via: https://opensource.com/article/18/4/elegant-solutions-everyday-python-problems

作者:Nina Zakharenko 选题:lujun9972 译者:MjSeven 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

你也许用过调速器检查过你的代码,但你知道它们是如何做到的吗?

调试器是大多数(即使不是每个)开发人员在软件工程职业生涯中至少使用过一次的那些软件之一,但是你们中有多少人知道它们到底是如何工作的?我在悉尼 linux.conf.au 2018 的演讲中,将讨论从头开始编写调试器……使用 Rust

在本文中,术语 调试器 debugger 跟踪器 tracer 可以互换。 “ 被跟踪者 Tracee ”是指正在被跟踪器跟踪的进程。

ptrace 系统调用

大多数调试器严重依赖称为 ptrace(2) 的系统调用,其原型如下:

long ptrace(enum __ptrace_request request, pid_t pid, void *addr, void *data);

这是一个可以操纵进程几乎所有方面的系统调用;但是,在调试器可以连接到一个进程之前,“被跟踪者”必须以请求 PTRACE_TRACEME 调用 ptrace。这告诉 Linux,父进程通过 ptrace 连接到这个进程是合法的。但是……我们如何强制一个进程调用 ptrace?很简单!fork/execve 提供了在 fork 之后但在被跟踪者真正开始使用 execve 之前调用 ptrace 的简单方法。很方便地,fork 还会返回被跟踪者的 pid,这是后面使用 ptrace 所必需的。

现在被跟踪者可以被调试器追踪,重要的变化发生了:

  • 每当一个信号被传送到被跟踪者时,它就会停止,并且一个可以被 wait 系列的系统调用捕获的等待事件被传送给跟踪器。
  • 每个 execve 系统调用都会导致 SIGTRAP 被传递给被跟踪者。(与之前的项目相结合,这意味着被跟踪者在一个 execve 完全发生之前停止。)

这意味着,一旦我们发出 PTRACE_TRACEME 请求并调用 execve 系统调用来实际在被跟踪者(进程上下文)中启动程序时,被跟踪者将立即停止,因为 execve 会传递一个 SIGTRAP,并且会被跟踪器中的等待事件捕获。我们如何继续?正如人们所期望的那样,ptrace 有大量的请求可以用来告诉被跟踪者可以继续:

  • PTRACE_CONT:这是最简单的。 被跟踪者运行,直到它接收到一个信号,此时等待事件被传递给跟踪器。这是最常见的实现真实世界调试器的“继续直至断点”和“永远继续”选项的方式。断点将在下面介绍。
  • PTRACE_SYSCALL:与 PTRACE_CONT 非常相似,但在进入系统调用之前以及在系统调用返回到用户空间之前停止。它可以与其他请求(我们将在本文后面介绍)结合使用来监视和修改系统调用的参数或返回值。系统调用追踪程序 strace 很大程度上使用这个请求来获知进程发起了哪些系统调用。
  • PTRACE_SINGLESTEP:这个很好理解。如果您之前使用过调试器(你会知道),此请求会执行下一条指令,然后立即停止。

我们可以通过各种各样的请求停止进程,但我们如何获得被调试者的状态?进程的状态大多是通过其寄存器捕获的,所以当然 ptrace 有一个请求来获得(或修改)寄存器:

  • PTRACE_GETREGS:这个请求将给出被跟踪者刚刚被停止时的寄存器的状态。
  • PTRACE_SETREGS:如果跟踪器之前通过调用 PTRACE_GETREGS 得到了寄存器的值,它可以在参数结构中修改相应寄存器的值,并使用 PTRACE_SETREGS 将寄存器设为新值。
  • PTRACE_PEEKUSERPTRACE_POKEUSER:这些允许从被跟踪者的 USER 区读取信息,这里保存了寄存器和其他有用的信息。 这可以用来修改单一寄存器,而避免使用更重的 PTRACE_{GET,SET}REGS 请求。

在调试器仅仅修改寄存器是不够的。调试器有时需要读取一部分内存,甚至对其进行修改。GDB 可以使用 print 得到一个内存位置或变量的值。ptrace 通过下面的方法实现这个功能:

  • PTRACE_PEEKTEXTPTRACE_POKETEXT:这些允许读取和写入被跟踪者地址空间中的一个字。当然,使用这个功能时被跟踪者要被暂停。

真实世界的调试器也有类似断点和观察点的功能。 在接下来的部分中,我将深入体系结构对调试器支持的细节。为了清晰和简洁,本文将只考虑 x86。

体系结构的支持

ptrace 很酷,但它是如何工作? 在前面的部分中,我们已经看到 ptrace 跟信号有很大关系:SIGTRAP 可以在单步跟踪、execve 之前以及系统调用前后被传送。信号可以通过一些方式产生,但我们将研究两个具体的例子,以展示信号可以被调试器用来在给定的位置停止程序(有效地创建一个断点!):

  • 未定义的指令:当一个进程尝试执行一个未定义的指令,CPU 将产生一个异常。此异常通过 CPU 中断处理,内核中相应的中断处理程序被调用。这将导致一个 SIGILL 信号被发送给进程。 这依次导致进程被停止,跟踪器通过一个等待事件被通知,然后它可以决定后面做什么。在 x86 上,指令 ud2 被确保始终是未定义的。
  • 调试中断:前面的方法的问题是,ud2 指令需要占用两个字节的机器码。存在一条特殊的单字节指令能够触发一个中断,它是 int $3,机器码是 0xCC。 当该中断发出时,内核向进程发送一个 SIGTRAP,如前所述,跟踪器被通知。

这很好,但如何我们才能胁迫被跟踪者执行这些指令? 这很简单:利用 ptracePTRACE_POKETEXT 请求,它可以覆盖内存中的一个字。 调试器将使用 PTRACE_PEEKTEXT 读取该位置原来的值并替换为 0xCC ,然后在其内部状态中记录该处原来的值,以及它是一个断点的事实。 下次被跟踪者执行到该位置时,它将被通过 SIGTRAP 信号自动停止。 然后调试器的最终用户可以决定如何继续(例如,检查寄存器)。

好吧,我们已经讲过了断点,那观察点呢? 当一个特定的内存位置被读或写,调试器如何停止程序? 当然你不可能为了能够读或写内存而去把每一个指令都覆盖为 int $3。有一组调试寄存器为了更有效的满足这个目的而被设计出来:

  • DR0DR3:这些寄存器中的每个都包含一个地址(内存位置),调试器因为某种原因希望被跟踪者在那些地址那里停止。 其原因以掩码方式被设定在 DR7 寄存器中。
  • DR4DR5:这些分别是 DR6DR7 过时的别名。
  • DR6:调试状态。包含有关 DR0DR3 中的哪个寄存器导致调试异常被引发的信息。这被 Linux 用来计算与 SIGTRAP 信号一起传递给被跟踪者的信息。
  • DR7:调试控制。通过使用这些寄存器中的位,调试器可以控制如何解释 DR0DR3 中指定的地址。位掩码控制监视点的尺寸(监视1、2、4 或 8 个字节)以及是否在执行、读取、写入时引发异常,或在读取或写入时引发异常。

由于调试寄存器是进程的 USER 区域的一部分,调试器可以使用 PTRACE_POKEUSER 将值写入调试寄存器。调试寄存器只与特定进程相关,因此在进程抢占并重新获得 CPU 控制权之前,调试寄存器会被恢复。

冰山一角

我们已经浏览了一个调试器的“冰山”:我们已经介绍了 ptrace,了解了它的一些功能,然后我们看到了 ptrace 是如何实现的。 ptrace 的某些部分可以用软件实现,但其它部分必须用硬件来实现,否则实现代价会非常高甚至无法实现。

当然有很多我们没有涉及。例如“调试器如何知道变量在内存中的位置?”等问题由于空间和时间限制而尚未解答,但我希望你从本文中学到了一些东西;如果它激起你的兴趣,网上有足够的资源可以了解更多。

想要了解更多,请查看 linux.conf.au 中 Levente Kurusa 的演讲 Let's Write a Debugger!,于一月 22-26 日在悉尼举办。


via: https://opensource.com/article/18/1/how-debuggers-really-work

作者:Levente Kurusa 译者:stephenxs 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

本周工作中,我花了整整一周的时间来尝试调试一个段错误。我以前从来没有这样做过,我花了很长时间才弄清楚其中涉及的一些基本事情(获得核心转储、找到导致段错误的行号)。于是便有了这篇博客来解释如何做那些事情!

在看完这篇博客后,你应该知道如何从“哦,我的程序出现段错误,但我不知道正在发生什么”到“我知道它出现段错误时的堆栈、行号了! ”。

什么是段错误?

段错误 segmentation fault ”是指你的程序尝试访问不允许访问的内存地址的情况。这可能是由于:

  • 试图解引用空指针(你不被允许访问内存地址 0);
  • 试图解引用其他一些不在你内存(LCTT 译注:指不在合法的内存地址区间内)中的指针;
  • 一个已被破坏并且指向错误的地方的 C++ 虚表指针 C++ vtable pointer ,这导致程序尝试执行没有执行权限的内存中的指令;
  • 其他一些我不明白的事情,比如我认为访问未对齐的内存地址也可能会导致段错误(LCTT 译注:在要求自然边界对齐的体系结构,如 MIPS、ARM 中更容易因非对齐访问产生段错误)。

这个“C++ 虚表指针”是我的程序发生段错误的情况。我可能会在未来的博客中解释这个,因为我最初并不知道任何关于 C++ 的知识,并且这种虚表查找导致程序段错误的情况也是我所不了解的。

但是!这篇博客后不是关于 C++ 问题的。让我们谈论的基本的东西,比如,我们如何得到一个核心转储?

步骤1:运行 valgrind

我发现找出为什么我的程序出现段错误的最简单的方式是使用 valgrind:我运行

valgrind -v your-program

这给了我一个故障时的堆栈调用序列。 简洁!

但我想也希望做一个更深入调查,并找出些 valgrind 没告诉我的信息! 所以我想获得一个核心转储并探索它。

如何获得一个核心转储

核心转储 core dump 是您的程序内存的一个副本,并且当您试图调试您的有问题的程序哪里出错的时候它非常有用。

当您的程序出现段错误,Linux 的内核有时会把一个核心转储写到磁盘。 当我最初试图获得一个核心转储时,我很长一段时间非常沮丧,因为 - Linux 没有生成核心转储!我的核心转储在哪里?

这就是我最终做的事情:

  1. 在启动我的程序之前运行 ulimit -c unlimited
  2. 运行 sudo sysctl -w kernel.core_pattern=/tmp/core-%e.%p.%h.%t

ulimit:设置核心转储的最大尺寸

ulimit -c 设置核心转储的最大尺寸。 它往往设置为 0,这意味着内核根本不会写核心转储。 它以千字节为单位。 ulimit 是按每个进程分别设置的 —— 你可以通过运行 cat /proc/PID/limit 看到一个进程的各种资源限制。

例如这些是我的系统上一个随便一个 Firefox 进程的资源限制:

$ cat /proc/6309/limits 
Limit                     Soft Limit           Hard Limit           Units     
Max cpu time              unlimited            unlimited            seconds   
Max file size             unlimited            unlimited            bytes     
Max data size             unlimited            unlimited            bytes     
Max stack size            8388608              unlimited            bytes     
Max core file size        0                    unlimited            bytes     
Max resident set          unlimited            unlimited            bytes     
Max processes             30571                30571                processes 
Max open files            1024                 1048576              files     
Max locked memory         65536                65536                bytes     
Max address space         unlimited            unlimited            bytes     
Max file locks            unlimited            unlimited            locks     
Max pending signals       30571                30571                signals   
Max msgqueue size         819200               819200               bytes     
Max nice priority         0                    0                    
Max realtime priority     0                    0                    
Max realtime timeout      unlimited            unlimited            us   

内核在决定写入多大的核心转储文件时使用 软限制 soft limit (在这种情况下,max core file size = 0)。 您可以使用 shell 内置命令 ulimitulimit -c unlimited) 将软限制增加到 硬限制 hard limit

kernel.core\_pattern:核心转储保存在哪里

kernel.core_pattern 是一个内核参数,或者叫 “sysctl 设置”,它控制 Linux 内核将核心转储文件写到磁盘的哪里。

内核参数是一种设定您的系统全局设置的方法。您可以通过运行 sysctl -a 得到一个包含每个内核参数的列表,或使用 sysctl kernel.core_pattern 来专门查看 kernel.core_pattern 设置。

所以 sysctl -w kernel.core_pattern=/tmp/core-%e.%p.%h.%t 将核心转储保存到目录 /tmp 下,并以 core 加上一系列能够标识(出故障的)进程的参数构成的后缀为文件名。

如果你想知道这些形如 %e%p 的参数都表示什么,请参考 man core

有一点很重要,kernel.core_pattern 是一个全局设置 —— 修改它的时候最好小心一点,因为有可能其它系统功能依赖于把它被设置为一个特定的方式(才能正常工作)。

kernel.core\_pattern 和 Ubuntu

默认情况下在 ubuntu 系统中,kernel.core_pattern 被设置为下面的值:

$ sysctl kernel.core_pattern
kernel.core_pattern = |/usr/share/apport/apport %p %s %c %d %P

这引起了我的迷惑(这 apport 是干什么的,它对我的核心转储做了什么?)。以下关于这个我了解到的:

  • Ubuntu 使用一种叫做 apport 的系统来报告 apt 包有关的崩溃信息。
  • 设定 kernel.core_pattern=|/usr/share/apport/apport %p %s %c %d %P 意味着核心转储将被通过管道送给 apport 程序。
  • apport 的日志保存在文件 /var/log/apport.log 中。
  • apport 默认会忽略来自不属于 Ubuntu 软件包一部分的二进制文件的崩溃信息

我最终只是跳过了 apport,并把 kernel.core_pattern 重新设置为 sysctl -w kernel.core_pattern=/tmp/core-%e.%p.%h.%t,因为我在一台开发机上,我不在乎 apport 是否工作,我也不想尝试让 apport 把我的核心转储留在磁盘上。

现在你有了核心转储,接下来干什么?

好的,现在我们了解了 ulimitkernel.core_pattern ,并且实际上在磁盘的 /tmp 目录中有了一个核心转储文件。太好了!接下来干什么?我们仍然不知道该程序为什么会出现段错误!

下一步将使用 gdb 打开核心转储文件并获取堆栈调用序列。

从 gdb 中得到堆栈调用序列

你可以像这样用 gdb 打开一个核心转储文件:

$ gdb -c my_core_file

接下来,我们想知道程序崩溃时的堆栈是什么样的。在 gdb 提示符下运行 bt 会给你一个 调用序列 backtrace 。在我的例子里,gdb 没有为二进制文件加载符号信息,所以这些函数名就像 “??????”。幸运的是,(我们通过)加载符号修复了它。

下面是如何加载调试符号。

symbol-file /path/to/my/binary
sharedlibrary

这从二进制文件及其引用的任何共享库中加载符号。一旦我这样做了,当我执行 bt 时,gdb 给了我一个带有行号的漂亮的堆栈跟踪!

如果你想它能工作,二进制文件应该以带有调试符号信息的方式被编译。在试图找出程序崩溃的原因时,堆栈跟踪中的行号非常有帮助。:)

查看每个线程的堆栈

通过以下方式在 gdb 中获取每个线程的调用栈!

thread apply all bt full

gdb + 核心转储 = 惊喜

如果你有一个带调试符号的核心转储以及 gdb,那太棒了!您可以上下查看调用堆栈(LCTT 译注:指跳进调用序列不同的函数中以便于查看局部变量),打印变量,并查看内存来得知发生了什么。这是最好的。

如果您仍然正在基于 gdb 向导来工作上,只打印出栈跟踪与bt也可以。 :)

ASAN

另一种搞清楚您的段错误的方法是使用 AddressSanitizer 选项编译程序(“ASAN”,即 $CC -fsanitize=address)然后运行它。 本文中我不准备讨论那个,因为本文已经相当长了,并且在我的例子中打开 ASAN 后段错误消失了,可能是因为 ASAN 使用了一个不同的内存分配器(系统内存分配器,而不是 tcmalloc)。

在未来如果我能让 ASAN 工作,我可能会多写点有关它的东西。(LCTT 译注:这里指使用 ASAN 也能复现段错误)

从一个核心转储得到一个堆栈跟踪真的很亲切!

这个博客听起来很多,当我做这些的时候很困惑,但说真的,从一个段错误的程序中获得一个堆栈调用序列不需要那么多步骤:

  1. 试试用 valgrind

如果那没用,或者你想要拿到一个核心转储来调查:

  1. 确保二进制文件编译时带有调试符号信息;
  2. 正确的设置 ulimitkernel.core_pattern
  3. 运行程序;
  4. 一旦你用 gdb 调试核心转储了,加载符号并运行 bt
  5. 尝试找出发生了什么!

我可以使用 gdb 弄清楚有个 C++ 的虚表条目指向一些被破坏的内存,这有点帮助,并且使我感觉好像更懂了 C++ 一点。也许有一天我们会更多地讨论如何使用 gdb 来查找问题!


via: https://jvns.ca/blog/2018/04/28/debugging-a-segfault-on-linux/

作者:Julia Evans 译者:stephenxs 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

我们继续 无密码验证 的文章。上一篇文章中,我们用 Go 写了一个 HTTP 服务,用这个服务来做无密码验证 API。今天,我们为它再写一个 JavaScript 客户端。

我们将使用 这里的 这个单页面应用程序(SPA)来展示使用的技术。如果你还没有读过它,请先读它。

记住流程:

  • 用户输入其 email。
  • 用户收到一个带有魔法链接的邮件。
  • 用户点击该链接、
  • 用户验证成功。

对于根 URL(/),我们将根据验证的状态分别使用两个不同的页面:一个是带有访问表单的页面,或者是已验证通过的用户的欢迎页面。另一个页面是验证回调的重定向页面。

伺服

我们将使用相同的 Go 服务器来为客户端提供服务,因此,在我们前面的 main.go 中添加一些路由:

router.Handle("GET", "/...", http.FileServer(SPAFileSystem{http.Dir("static")}))
type SPAFileSystem struct {
    fs http.FileSystem
}

func (spa SPAFileSystem) Open(name string) (http.File, error) {
    f, err := spa.fs.Open(name)
    if err != nil {
        return spa.fs.Open("index.html")
    }
    return f, nil
}

这个伺服文件放在 static 下,配合 static/index.html 作为回调。

你可以使用你自己的服务器,但是你得在服务器上启用 CORS

HTML

我们来看一下那个 static/index.html 文件。

<!DOCTYPE html>
<html lang="en">
<head>
 <meta charset="utf-8">
 <meta name="viewport" content="width=device-width, initial-scale=1.0">
 <title>Passwordless Demo</title>
 <link rel="shortcut icon" href="data:,">
 <script src="/js/main.js" type="module"></script>
</head>
<body></body>
</html>

单页面应用程序的所有渲染由 JavaScript 来完成,因此,我们使用了一个空的 body 部分和一个 main.js 文件。

我们将使用 上篇文章 中的 Router。

渲染

现在,我们使用下面的内容来创建一个 static/js/main.js 文件:

import Router from 'https://unpkg.com/@nicolasparada/router'
import { isAuthenticated } from './auth.js'

const router = new Router()

router.handle('/', guard(view('home')))
router.handle('/callback', view('callback'))
router.handle(/^\//, view('not-found'))

router.install(async resultPromise => {
    document.body.innerHTML = ''
    document.body.appendChild(await resultPromise)
})

function view(name) {
    return (...args) => import(`/js/pages/${name}-page.js`)
        .then(m => m.default(...args))
}

function guard(fn1, fn2 = view('welcome')) {
    return (...args) => isAuthenticated()
        ? fn1(...args)
        : fn2(...args)
}

与上篇文章不同的是,我们实现了一个 isAuthenticated() 函数和一个 guard() 函数,使用它去渲染两种验证状态的页面。因此,当用户访问 / 时,它将根据用户是否通过了验证来展示主页或者是欢迎页面。

验证

现在,我们来编写 isAuthenticated() 函数。使用下面的内容来创建一个 static/js/auth.js 文件:

export function getAuthUser() {
    const authUserItem = localStorage.getItem('auth_user')
    const expiresAtItem = localStorage.getItem('expires_at')

    if (authUserItem !== null && expiresAtItem !== null) {
        const expiresAt = new Date(expiresAtItem)

        if (!isNaN(expiresAt.valueOf()) && expiresAt > new Date()) {
            try {
                return JSON.parse(authUserItem)
            } catch (_) { }
        }
    }

    return null
}

export function isAuthenticated() {
    return localStorage.getItem('jwt') !== null && getAuthUser() !== null
}

当有人登入时,我们将保存 JSON 格式的 web 令牌、它的过期日期,以及在 localStorage 上的当前已验证用户。这个模块就是这个用处。

  • getAuthUser() 用于从 localStorage 获取已认证的用户,以确认 JSON 格式的 Web 令牌没有过期。
  • isAuthenticated() 在前面的函数中用于去检查它是否没有返回 null

获取

在继续这个页面之前,我将写一些与服务器 API 一起使用的 HTTP 工具。

我们使用以下的内容去创建一个 static/js/http.js 文件:

import { isAuthenticated } from './auth.js'

function get(url, headers) {
    return fetch(url, {
        headers: Object.assign(getAuthHeader(), headers),
    }).then(handleResponse)
}

function post(url, body, headers) {
    return fetch(url, {
        method: 'POST',
        headers: Object.assign(getAuthHeader(), { 'content-type': 'application/json' }, headers),
        body: JSON.stringify(body),
    }).then(handleResponse)
}

function getAuthHeader() {
    return isAuthenticated()
        ? { authorization: `Bearer ${localStorage.getItem('jwt')}` }
        : {}
}

export async function handleResponse(res) {
    const body = await res.clone().json().catch(() => res.text())
    const response = {
        statusCode: res.status,
        statusText: res.statusText,
        headers: res.headers,
        body,
    }
    if (!res.ok) {
        const message = typeof body === 'object' && body !== null && 'message' in body
            ? body.message
            : typeof body === 'string' && body !== ''
                ? body
                : res.statusText
        const err = new Error(message)
        throw Object.assign(err, response)
    }
    return response
}

export default {
    get,
    post,
}

这个模块导出了 get()post() 函数。它们是 fetch API 的封装。当用户是已验证的,这二个函数注入一个 Authorization: Bearer <token_here> 头到请求中;这样服务器就能对我们进行身份验证。

欢迎页

我们现在来到欢迎页面。用如下的内容创建一个 static/js/pages/welcome-page.js 文件:

const template = document.createElement('template')
template.innerHTML = `
    <h1>Passwordless Demo</h1>
    <h2>Access</h2>
    <form id="access-form">
        <input type="email" placeholder="Email" autofocus required>
        <button type="submit">Send Magic Link</button>
    </form>
`

export default function welcomePage() {
    const page = template.content.cloneNode(true)

    page.getElementById('access-form')
        .addEventListener('submit', onAccessFormSubmit)

    return page
}

这个页面使用一个 HTMLTemplateElement 作为视图。这只是一个输入用户 email 的简单表单。

为了避免干扰,我将跳过错误处理部分,只是将它们输出到控制台上。

现在,我们来写 onAccessFormSubmit() 函数。

import http from '../http.js'

function onAccessFormSubmit(ev) {
    ev.preventDefault()

    const form = ev.currentTarget
    const input = form.querySelector('input')
    const email = input.value

    sendMagicLink(email).catch(err => {
        console.error(err)
        if (err.statusCode === 404 && wantToCreateAccount()) {
            runCreateUserProgram(email)
        }
    })
}

function sendMagicLink(email) {
    return http.post('/api/passwordless/start', {
        email,
        redirectUri: location.origin + '/callback',
    }).then(() => {
        alert('Magic link sent. Go check your email inbox.')
    })
}

function wantToCreateAccount() {
    return prompt('No user found. Do you want to create an account?')
}

它对 /api/passwordless/start 发起了 POST 请求,请求体中包含 emailredirectUri。在本例中它返回 404 Not Found 状态码时,我们将创建一个用户。

function runCreateUserProgram(email) {
    const username = prompt("Enter username")
    if (username === null) return

    http.post('/api/users', { email, username })
        .then(res => res.body)
        .then(user => sendMagicLink(user.email))
        .catch(console.error)
}

这个用户创建程序,首先询问用户名,然后使用 email 和用户名做一个 POST 请求到 /api/users。成功之后,给创建的用户发送一个魔法链接。

回调页

这是访问表单的全部功能,现在我们来做回调页面。使用如下的内容来创建一个 static/js/pages/callback-page.js 文件:

import http from '../http.js'

const template = document.createElement('template')
template.innerHTML = `
    <h1>Authenticating you</h1>
`

export default function callbackPage() {
    const page = template.content.cloneNode(true)

    const hash = location.hash.substr(1)
    const fragment = new URLSearchParams(hash)
    for (const [k, v] of fragment.entries()) {
        fragment.set(decodeURIComponent(k), decodeURIComponent(v))
    }
    const jwt = fragment.get('jwt')
    const expiresAt = fragment.get('expires_at')

    http.get('/api/auth_user', { authorization: `Bearer ${jwt}` })
        .then(res => res.body)
        .then(authUser => {
            localStorage.setItem('jwt', jwt)
            localStorage.setItem('auth_user', JSON.stringify(authUser))
            localStorage.setItem('expires_at', expiresAt)

            location.replace('/')
        })
        .catch(console.error)

    return page
}

请记住……当点击魔法链接时,我们会来到 /api/passwordless/verify_redirect,它将把我们重定向到重定向 URI,我们将放在哈希中的 JWT 和过期日期传递给 /callback

回调页面解码 URL 中的哈希,提取这些参数去做一个 GET 请求到 /api/auth_user,用 JWT 保存所有数据到 localStorage 中。最后,重定向到主页面。

主页

创建如下内容的 static/pages/home-page.js 文件:

import { getAuthUser } from '../auth.js'

export default function homePage() {
    const authUser = getAuthUser()

    const template = document.createElement('template')
    template.innerHTML = `
        <h1>Passwordless Demo</h1>
        <p>Welcome back, ${authUser.username}