分类 技术 下的文章

如果你看一下 新的 Datadog Agent,你可能会注意到大部分代码库是用 Go 编写的,尽管我们用来收集指标的检查仍然是用 Python 编写的。这大概是因为 Datadog Agent 是一个 嵌入了 CPython 解释器的普通 Go 二进制文件,可以在任何时候按需执行 Python 代码。这个过程通过抽象层来透明化,使得你可以编写惯用的 Go 代码而底层运行的是 Python。

在 Go 应用程序中嵌入 Python 的原因有很多:

  • 它在过渡期间很有用;可以逐步将现有 Python 项目的部分迁移到新语言,而不会在此过程中丢失任何功能。
  • 你可以复用现有的 Python 软件或库,而无需用新语言重新实现。
  • 你可以通过加载去执行常规 Python 脚本来动态扩展你软件,甚至在运行时也可以。

理由还可以列很多,但对于 Datadog Agent 来说,最后一点至关重要:我们希望做到无需重新编译 Agent,或者说编译任何内容就能够执行自定义检查或更改现有检查。

嵌入 CPython 非常简单,而且文档齐全。解释器本身是用 C 编写的,并且提供了一个 C API 以编程方式来执行底层操作,例如创建对象、导入模块和调用函数。

在本文中,我们将展示一些代码示例,我们将会在与 Python 交互的同时继续保持 Go 代码的惯用语,但在我们继续之前,我们需要解决一个间隙:嵌入 API 是 C 语言,但我们的主要应用程序是 Go,这怎么可能工作?

介绍 cgo

很多好的理由 说服你为什么不要在堆栈中引入 cgo,但嵌入 CPython 是你必须这样做的原因。cgo 不是语言,也不是编译器。它是 外部函数接口 Foreign Function Interface (FFI),一种让我们可以在 Go 中使用来调用不同语言(特别是 C)编写的函数和服务的机制。

当我们提起 “cgo” 时,我们实际上指的是 Go 工具链在底层使用的一组工具、库、函数和类型,因此我们可以通过执行 go build 来获取我们的 Go 二进制文件。下面是使用 cgo 的示例程序:

package main

// #include <float.h>
import "C"
import "fmt"

func main() {
    fmt.Println("Max float value of float is", C.FLT_MAX)
}

在这种包含头文件情况下,import "C" 指令上方的注释块称为“ 序言 preamble ”,可以包含实际的 C 代码。导入后,我们可以通过“C”伪包来“跳转”到外部代码,访问常量 FLT_MAX。你可以通过调用 go build 来构建,它就像普通的 Go 一样。

如果你想查看 cgo 在这背后到底做了什么,可以运行 go build -x。你将看到 “cgo” 工具将被调用以生成一些 C 和 Go 模块,然后将调用 C 和 Go 编译器来构建目标模块,最后链接器将所有内容放在一起。

你可以在 Go 博客 上阅读更多有关 cgo 的信息,该文章包含更多的例子以及一些有用的链接来做进一步了解细节。

现在我们已经了解了 cgo 可以为我们做什么,让我们看看如何使用这种机制运行一些 Python 代码。

嵌入 CPython:一个入门指南

从技术上讲,嵌入 CPython 的 Go 程序并没有你想象的那么复杂。事实上,我们只需在运行 Python 代码之前初始化解释器,并在完成后关闭它。请注意,我们在所有示例中使用 Python 2.x,但我们只需做很少的调整就可以应用于 Python 3.x。让我们看一个例子:

package main

// #cgo pkg-config: python-2.7
// #include <Python.h>
import "C"
import "fmt"

func main() {
    C.Py_Initialize()
    fmt.Println(C.GoString(C.Py_GetVersion()))
    C.Py_Finalize()
}

上面的例子做的正是下面 Python 代码要做的事:

import sys
print(sys.version)

你可以看到我们在序言加入了一个 #cgo 指令;这些指令被会被传递到工具链,让你改变构建工作流程。在这种情况下,我们告诉 cgo 调用 pkg-config 来收集构建和链接名为 python-2.7 的库所需的标志,并将这些标志传递给 C 编译器。如果你的系统中安装了 CPython 开发库和 pkg-config,你只需要运行 go build 来编译上面的示例。

回到代码,我们使用 Py_Initialize()Py_Finalize() 来初始化和关闭解释器,并使用 Py_GetVersion C 函数来获取嵌入式解释器版本信息的字符串。

如果你想知道,所有我们需要放在一起调用 C 语言 Python API的 cgo 代码都是模板代码。这就是为什么 Datadog Agent 依赖 go-python 来完成所有的嵌入操作;该库为 C API 提供了一个 Go 友好的轻量级包,并隐藏了 cgo 细节。这是另一个基本的嵌入式示例,这次使用 go-python:

package main

import (
    python "github.com/sbinet/go-python"
)

func main() {
    python.Initialize()
    python.PyRun_SimpleString("print 'hello, world!'")
    python.Finalize()
}

这看起来更接近普通 Go 代码,不再暴露 cgo,我们可以在访问 Python API 时来回使用 Go 字符串。嵌入式看起来功能强大且对开发人员友好,是时候充分利用解释器了:让我们尝试从磁盘加载 Python 模块。

在 Python 方面我们不需要任何复杂的东西,无处不在的“hello world” 就可以达到目的:

# foo.py
def hello():
    """
    Print hello world for fun and profit.
    """
    print "hello, world!"

Go 代码稍微复杂一些,但仍然可读:

// main.go
package main

import "github.com/sbinet/go-python"

func main() {
    python.Initialize()
    defer python.Finalize()

    fooModule := python.PyImport_ImportModule("foo")
    if fooModule == nil {
        panic("Error importing module")
    }

    helloFunc := fooModule.GetAttrString("hello")
    if helloFunc == nil {
        panic("Error importing function")
    }

    // The Python function takes no params but when using the C api
    // we're required to send (empty) *args and **kwargs anyways.
    helloFunc.Call(python.PyTuple_New(0), python.PyDict_New())
}

构建时,我们需要将 PYTHONPATH 环境变量设置为当前工作目录,以便导入语句能够找到 foo.py 模块。在 shell 中,该命令如下所示:

$ go build main.go && PYTHONPATH=. ./main
hello, world!

可怕的全局解释器锁

为了嵌入 Python 必须引入 cgo ,这是一种权衡:构建速度会变慢,垃圾收集器不会帮助我们管理外部系统使用的内存,交叉编译也很难。对于一个特定的项目来说,这些问题是否是可以争论的,但我认为有一些不容商量的问题:Go 并发模型。如果我们不能从 goroutine 中运行 Python,那么使用 Go 就没有意义了。

在处理并发、Python 和 cgo 之前,我们还需要知道一些事情:它就是 全局解释器锁 Global Interpreter Lock ,即 GIL。GIL 是语言解释器(CPython 就是其中之一)中广泛采用的一种机制,可防止多个线程同时运行。这意味着 CPython 执行的任何 Python 程序都无法在同一进程中并行运行。并发仍然是可能的,锁是速度、安全性和实现简易性之间的一个很好的权衡,那么,当涉及到嵌入时,为什么这会造成问题呢?

当一个常规的、非嵌入式的 Python 程序启动时,不涉及 GIL 以避免锁定操作中的无用开销;在某些 Python 代码首次请求生成线程时 GIL 就启动了。对于每个线程,解释器创建一个数据结构来存储当前的相关状态信息并锁定 GIL。当线程完成时,状态被恢复,GIL 被解锁,准备被其他线程使用。

当我们从 Go 程序运行 Python 时,上述情况都不会自动发生。如果没有 GIL,我们的 Go 程序可以创建多个 Python 线程,这可能会导致竞争条件,从而导致致命的运行时错误,并且很可能出现分段错误导致整个 Go 应用程序崩溃。

解决方案是在我们从 Go 运行多线程代码时显式调用 GIL;代码并不复杂,因为 C API 提供了我们需要的所有工具。为了更好地暴露这个问题,我们需要写一些受 CPU 限制的 Python 代码。让我们将这些函数添加到前面示例中的 foo.py 模块中:

# foo.py
import sys

def print_odds(limit=10):
    """
    Print odds numbers < limit
    """
    for i in range(limit):
        if i%2:
            sys.stderr.write("{}\n".format(i))

def print_even(limit=10):
    """
    Print even numbers < limit
    """
    for i in range(limit):
        if i%2 == 0:
            sys.stderr.write("{}\n".format(i))

我们将尝试从 Go 并发打印奇数和偶数,使用两个不同的 goroutine(因此涉及线程):

package main

import (
    "sync"

    "github.com/sbinet/go-python"
)

func main() {
    // The following will also create the GIL explicitly
    // by calling PyEval_InitThreads(), without waiting
    // for the interpreter to do that
    python.Initialize()

    var wg sync.WaitGroup
    wg.Add(2)

    fooModule := python.PyImport_ImportModule("foo")
    odds := fooModule.GetAttrString("print_odds")
    even := fooModule.GetAttrString("print_even")

    // Initialize() has locked the the GIL but at this point we don't need it
    // anymore. We save the current state and release the lock
    // so that goroutines can acquire it
    state := python.PyEval_SaveThread()

    go func() {
        _gstate := python.PyGILState_Ensure()
        odds.Call(python.PyTuple_New(0), python.PyDict_New())
        python.PyGILState_Release(_gstate)

        wg.Done()
    }()

    go func() {
        _gstate := python.PyGILState_Ensure()
        even.Call(python.PyTuple_New(0), python.PyDict_New())
        python.PyGILState_Release(_gstate)

        wg.Done()
    }()

    wg.Wait()

    // At this point we know we won't need Python anymore in this
    // program, we can restore the state and lock the GIL to perform
    // the final operations before exiting.
    python.PyEval_RestoreThread(state)
    python.Finalize()
}

在阅读示例时,你可能会注意到一个模式,该模式将成为我们运行嵌入式 Python 代码的习惯写法:

  1. 保存状态并锁定 GIL。
  2. 执行 Python。
  3. 恢复状态并解锁 GIL。

代码应该很简单,但我们想指出一个微妙的细节:请注意,尽管借用了 GIL 执行,有时我们通过调用 PyEval_SaveThread()PyEval_RestoreThread() 来操作 GIL,有时(查看 goroutines 里面)我们对 PyGILState_Ensure()PyGILState_Release() 来做同样的事情。

我们说过当从 Python 操作多线程时,解释器负责创建存储当前状态所需的数据结构,但是当同样的事情发生在 C API 时,我们来负责处理。

当我们用 go-python 初始化解释器时,我们是在 Python 上下文中操作的。因此,当调用 PyEval_InitThreads() 时,它会初始化数据结构并锁定 GIL。我们可以使用 PyEval_SaveThread()PyEval_RestoreThread() 对已经存在的状态进行操作。

在 goroutines 中,我们从 Go 上下文操作,我们需要显式创建状态并在完成后将其删除,这就是 PyGILState_Ensure()PyGILState_Release() 为我们所做的。

释放 Gopher

在这一点上,我们知道如何处理在嵌入式解释器中执行 Python 的多线程 Go 代码,但在 GIL 之后,另一个挑战即将来临:Go 调度程序。

当一个 goroutine 启动时,它被安排在可用的 GOMAXPROCS 线程之一上执行,参见此处 可了解有关该主题的更多详细信息。如果一个 goroutine 碰巧执行了系统调用或调用 C 代码,当前线程会将线程队列中等待运行的其他 goroutine 移交给另一个线程,以便它们有更好的机会运行; 当前 goroutine 被暂停,等待系统调用或 C 函数返回。当这种情况发生时,线程会尝试恢复暂停的 goroutine,但如果这不可能,它会要求 Go 运行时找到另一个线程来完成 goroutine 并进入睡眠状态。 goroutine 最后被安排给另一个线程,它就完成了。

考虑到这一点,让我们看看当一个 goroutine 被移动到一个新线程时,运行一些 Python 代码的 goroutine 会发生什么:

  1. 我们的 goroutine 启动,执行 C 调用并暂停。GIL 被锁定。
  2. 当 C 调用返回时,当前线程尝试恢复 goroutine,但失败了。
  3. 当前线程告诉 Go 运行时寻找另一个线程来恢复我们的 goroutine。
  4. Go 调度器找到一个可用线程并恢复 goroutine。
  5. goroutine 快完成了,并在返回之前尝试解锁 GIL。
  6. 当前状态中存储的线程 ID 来自原线程,与当前线程的 ID 不同。
  7. 崩溃!

所幸,我们可以通过从 goroutine 中调用运行时包中的 LockOSThread 函数来强制 Go runtime 始终保持我们的 goroutine 在同一线程上运行:

go func() {
    runtime.LockOSThread()

    _gstate := python.PyGILState_Ensure()
    odds.Call(python.PyTuple_New(0), python.PyDict_New())
    python.PyGILState_Release(_gstate)
    wg.Done()
}()

这会干扰调度器并可能引入一些开销,但这是我们愿意付出的代价。

结论

为了嵌入 Python,Datadog Agent 必须接受一些权衡:

  • cgo 引入的开销。
  • 手动处理 GIL 的任务。
  • 在执行期间将 goroutine 绑定到同一线程的限制。

为了能方便在 Go 中运行 Python 检查,我们很乐意接受其中的每一项。但通过意识到这些权衡,我们能够最大限度地减少它们的影响,除了为支持 Python 而引入的其他限制,我们没有对策来控制潜在问题:

  • 构建是自动化和可配置的,因此开发人员仍然需要拥有与 go build 非常相似的东西。
  • Agent 的轻量级版本,可以使用 Go 构建标签,完全剥离 Python 支持。
  • 这样的版本仅依赖于在 Agent 本身硬编码的核心检查(主要是系统和网络检查),但没有 cgo 并且可以交叉编译。

我们将在未来重新评估我们的选择,并决定是否仍然值得保留 cgo;我们甚至可以重新考虑整个 Python 是否仍然值得,等待 Go 插件包 成熟到足以支持我们的用例。但就目前而言,嵌入式 Python 运行良好,从旧代理过渡到新代理再简单不过了。

你是一个喜欢混合不同编程语言的多语言者吗?你喜欢了解语言的内部工作原理以提高你的代码性能吗?


via: https://www.datadoghq.com/blog/engineering/cgo-and-python/

作者:Massimiliano Pippi 译者:Zioyi 校对:wxy

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

将配置文件与代码分离,使任何人都可以改变他们的配置,而不需要任何特殊的编程技巧。

 title=

将程序配置与代码分离是很重要的。它使非程序员能够改变配置而不需要修改程序的代码。如果是编译好的二进制可执行文件,这对非程序员来说是不可能的,因为它不仅需要访问源文件(我们在开源程序中会这样),而且还需要程序员的技能组合。很少有人有这种能力,而且大多数人都不想学习它。

对于像 Bash 这样的 shell 语言,由于 shell 脚本没有被编译成二进制格式,所以从定义上讲,源码是可以访问的。尽管有这种开放性,但对于非程序员来说,在 shell 脚本中钻研和修改它们并不是一个特别好的主意。即使是经验丰富的开发人员和系统管理员,也会意外地做出一些导致错误或更糟的改变。

因此,将配置项放在容易维护的文本文件中,提供了分离,并允许非程序员编辑配置,而不会有对代码进行意外修改的危险。许多开发者对用编译语言编写的程序都是这样做的,因为他们并不期望用户是开发者。由于许多相同的原因,对解释型 shell 语言这样做也是有意义的。

通常的方式

和其他许多语言一样, 你可以为 Bash 程序编写代码,来读取并解析 ASCII 文本的配置文件、读取变量名称并在程序代码执行时设置值。例如,一个配置文件可能看起来像这样:

var1=LinuxGeek46
var2=Opensource.com

程序将读取文件,解析每一行,并将值设置到每个变量中。

源引

Bash 使用一种更简单的方法来解析和设置变量, 叫做 源引 sourcing 。从一个可执行的 shell 程序中获取一个外部文件是一种简单的方法,可以将该文件的内容完整地引入 shell 程序中。在某种意义上,这很像编译语言的 include 语句,在运行时包括库文件。这样的文件可以包括任何类型的 Bash 代码,包括变量赋值。

(LCTT 译注:对于使用 source. 命令引入另外一个文件的行为,我们首倡翻译为“源引”。)

像往常一样,演示比解释更容易。

首先,创建一个 ~/bin 目录(如果它还不存在的话),并将其作为当前工作目录(PWD)。Linux 文件系统分层标准~/bin 定义为用户存储可执行文件的适当位置。

在这个目录下创建一个新文件。将其命名为 main,并使其可执行:

[dboth@david bin]$ touch main
[dboth@david bin]$ chmod +x main
[dboth@david bin]$

在这个可执行文件中添加以下内容:

#!/bin/bash
Name="LinuxGeek"
echo $Name

并执行这个 Bash 程序:

[dboth@david bin]$ ./main
LinuxGeek
[dboth@david bin]$

创建一个新的文件并命名为 ~/bin/data。这个文件不需要是可执行的。在其中添加以下信息:

# Sourced code and variables
echo "This is the sourced code from the data file."
FirstName="David"
LastName="Both"

main 程序中增加三行,看起来像这样:

#!/bin/bash
Name="LinuxGeek"
echo $Name
source ~/bin/data
echo "First name: $FirstName"
echo "LastName: $LastName"

重新运行该程序:

[dboth@david bin]$ ./main
LinuxGeek
This is the sourced code from the data file.
First name: David
LastName: Both
[dboth@david bin]$

关于源引还有一件非常酷的事情要知道。你可以使用一个单点(.)作为 source 命令的快捷方式。改变 main 文件,用 . 代替 source

#!/bin/bash
Name="LinuxGeek"
echo $Name
. ~/bin/data
echo "First name: $FirstName"
echo "LastName: $LastName"

并再次运行该程序。其结果应该与之前的运行完全相同。

运行 Bash

每一台使用 Bash 的 Linux 主机(几乎所有主机都是,因为 Bash 是所有发行版的默认 shell),都包括一些优秀的、内置的源引示例。

每当 Bash shell 运行时,它的环境必须被配置成可以使用的样子。有五个主要文件和一个目录用于配置 Bash 环境。它们和它们的主要功能如下:

  • /etc/profile: 系统级的环境和启动程序
  • /etc/bashrc: 系统级的函数和别名
  • /etc/profile.d/: 包含系统级的脚本的目录,用于配置各种命令行工具,如 vimmc 以及系统管理员创建的任何自定义配置脚本
  • ~/.bash_profile: 用户特定的环境和启动程序
  • ~/.bashrc: 用户特定的别名和函数
  • ~/.bash_logout: 用户特定的命令,在用户注销时执行

试着通过这些文件追踪执行顺序,确定它在非登录 Bash 初始化和登录 Bash 初始化中使用的顺序。我在我的 Linux 培训系列《使用和管理 Linux:从零到系统管理员》的第一卷第 17 章中这样做过。

给你一个提示。这一切都从 ~/.bashrc 脚本开始。

总结

这篇文章探讨了在 Bash 程序中引用代码和变量的方法。这种从配置文件中解析变量的方法是快速、简单和灵活的。它提供了一种将 Bash 代码与变量赋值分开的方法,以使非程序员能够设置这些变量的值。


via: https://opensource.com/article/21/6/bash-config

作者:David Both 选题:lujun9972 译者:geekpi 校对:wxy

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

编者按:近些年来 Rust 语言由于其内存安全性和性能等优势得到了很多关注,尤其是 Linux 内核也在准备将其集成到其中,因此,我们特邀阿里云工程师苏子彬为我们介绍一下如何在 Linux 内核中集成 Rust 支持。

2021 年 4 月 14 号,一封主题名为《Rust support》的邮件出现在 LKML 邮件组中。这封邮件主要介绍了向内核引入 Rust 语言支持的一些看法以及所做的工作。邮件的发送者是 Miguel Ojeda,为内核中 Compiler attributes、.clang-format 等多个模块的维护者,也是目前 Rust for Linux 项目的维护者。

Rust for Linux 项目目前得到了 Google 的大力支持Miguel Ojeda 当前的全职工作就是负责 Rust for Linux 项目。

长期以来,内核使用 C 语言和汇编语言作为主要的开发语言,部分辅助语言包括 Python、Perl、shell 被用来进行代码生成、打补丁、检查等工作。2016 年 Linux 25 岁生日时,在对 Linus Torvalds 的一篇 采访中,他就曾表示过:

这根本不是一个新现象。我们有过使用 Modula-2 或 Ada 的系统人员,我不得不说 Rust 看起来比这两个灾难要好得多。

我对 Rust 用于操作系统内核并不信服(虽然系统编程不仅限于内核),但同时,毫无疑问,C 有很多局限性。

在最新的对 Rust support 的 RFC 邮件的回复中,他更是说:

所以我对几个个别补丁做了回应,但总体上我不讨厌它。

没有用他特有的回复方式来反击,应该就是暗自喜欢了吧。

目前 Rust for Linux 依然是一个独立于上游的项目,并且主要工作还集中的驱动接口相关的开发上,并非一个完善的项目。

项目地址: https://github.com/Rust-for-Linux/linux

为什么是 Rust

Miguel Ojeda 的第一个 RFC 邮件中,他已经提到了 “Why Rust”,简单总结下:

  • 安全子集 safe subset 中不存在未定义行为,包括内存安全和数据竞争;
  • 更加严格的类型检测系统能够进一步减少逻辑错误;
  • 明确区分 safeunsafe 代码;
  • 更加面向未来的语言:sum 类型、模式匹配、泛型、RAII、生命周期、共享及专属引用、模块与可见性等等;
  • 可扩展的独立标准库;
  • 集成的开箱可用工具:文档生成、代码格式化、linter 等,这些都基于编译器本身。

编译支持 Rust 的内核

根据 Rust for Linux 文档,编译一个包含 Rust 支持的内核需要如下步骤:

  1. 安装 rustc 编译器。Rust for Linux 不依赖 cargo,但需要最新的 beta 版本的 rustc。使用 rustup命令安装:
rustup default beta-2021-06-23
  1. 安装 Rust 标准库的源码。Rust for Linux 会交叉编译 Rust 的 core 库,并将这两个库链接进内核镜像。
rustup component add rust-src
  1. 安装 libclang 库。libclangbindgen 用做前端,用来处理 C 代码。libclang 可以从 llvm 官方主页 下载预编译好的版本。
  2. 安装 bindgen 工具,bindgen 是一个自动将 C 接口转为 RustFFI 接口的库:
cargo install --locked --version 0.56.0 bindgen
  1. 克隆最新的 Rust for Linux 代码:
git clone https://github.com/Rust-for-Linux/linux.git
  1. 配置内核启用 Rust 支持:
Kernel hacking
  -> Sample kernel code
    -> Rust samples
  1. 构建:
LIBCLANG_PATH=/path/to/libclang make -j LLVM=1 bzImage

这里我们使用 clang 作为默认的内核编译器,使用 gcc 理论上是可以的,但还处于 早期实验 阶段。

Rust 是如何集成进内核的

目录结构

为了将 Rust 集成进内核中,开发者首先对 Kbuild 系统进行修改,加入了相关配置项来开启/关闭 Rust 的支持。

此外,为了编译 rs 文件,添加了一些 Makefile 的规则。这些修改分散在内核目录中的不同文件里。

Rust 生成的目标代码中的符号会因为 Mangling 导致其长度超过同样的 C 程序所生成符号的长度,因此,需要对内核的符号长度相关的逻辑进行补丁。开发者引入了 “大内核符号”的概念,用来在保证向前兼容的情况下,支持 Rust 生成的目标文件符号长度。

其他 Rust 相关的代码都被放置在了 rust 目录下。

在 Rust 中使用 C 函数

Rust 提供 FFI( 外部函数接口 Foreign Function Interface )用来支持对 C 代码的调用。Bindgen 是一个 Rust 官方的工具,用来自动化地从 C 函数中生成 Rust 的 FFI 绑定。内核中的 Rust 也使用该工具从原生的内核 C 接口中生成 Rust 的 FFI 绑定。

quiet_cmd_bindgen = BINDGEN $@
      cmd_bindgen = \
    $(BINDGEN) $< $(shell grep -v '^\#\|^$$' $(srctree)/rust/bindgen_parameters) \
        --use-core --with-derive-default --ctypes-prefix c_types \
        --no-debug '.*' \
        --size_t-is-usize -o $@ -- $(bindgen_c_flags_final) -DMODULE

$(objtree)/rust/bindings_generated.rs: $(srctree)/rust/kernel/bindings_helper.h \
    $(srctree)/rust/bindgen_parameters FORCE
    $(call if_changed_dep,bindgen)

ABI

Rust 相关的代码会单独从 rs 编译为 .o,生成的目标文件是标准的 ELF 文件。在链接阶段,内核的链接器将 Rust 生成的目标文件与其他 C 程序生成的目标文件一起链接为内核镜像文件。因此,只要 Rust 生成的目标文件 ABI 与 C 程序的一致,就可以无差别的被链接(当然,被引用的符号还是要存在的)。

Rust 的 alloccore

目前 Rust for Linux 依赖于 core 库。在 core 中定义了基本的 Rust 数据结构与语言特性,例如熟悉的 Option<>Result<> 就是 core 库所提供。

这个库被交叉编译后被直接链接进内核镜像文件,这也是导致启用 Rust 的内核镜像文件尺寸较大的原因。在未来的工作中,这两个库会被进一步被优化,去除掉某些无用的部分,例如浮点操作,Unicode 相关的内容,Futures 相关的功能等。

之前的 Rust for Linux 项目还依赖于 Rust 的 alloc 库。Rust for Linux 定义了自己的 GlobalAlloc 用来管理基本的堆内存分配。主要被用来进行堆内存分配,并且使用 GFP_KERNEL 标识作为默认的内存分配模式。

不过在在最新的 拉取请求 中,社区已经将移植并修改了 Rust的 alloc 库,使其能够在尽量保证与 Rust 上游统一的情况下,允许开发者定制自己的内存分配器。不过目前使用自定义的 GFP_ 标识来分配内存依然是不支持的,但好消息是这个功能正在开发中。

“Hello World” 内核模块

用一个简单的 Hello World 来展示如何使用 Rust 语言编写驱动代码,hello_world.rs:

#![no_std]
#![feature(allocator_api, global_asm)]

use kernel::prelude::*;

module! {
    type: HelloWorld,
    name: b"hello_world",
    author: b"d0u9",
    description: b"A simple hello world example",
    license: b"GPL v2",
}

struct HelloWorld;

impl KernelModule for HelloWorld {
    fn init() -> Result<Self> {
        pr_info!("Hello world from rust!\n");

        Ok(HelloWorld)
    }
}

impl Drop for HelloWorld {
    fn drop(&mut self) {
        pr_info!("Bye world from rust!\n");
    }
}

与之对应的 Makefile

obj-m := hello_world.o

构建:

make -C /path/to/linux_src M=$(pwd) LLVM=1 modules

之后就和使用普通的内核模块一样,使用 insmod 工具或者 modprobe 工具加载就可以了。在使用体验上是没有区别的。

module! { }

这个宏可以被认为是 Rust 内核模块的入口,因为在其中定义了一个内核模块所需的所有信息,包括:AuthorLicenseDescription 等。其中最重要的是 type 字段,在其中需要指定内核模块结构的名字。在这个例子中:

module! {
    ...
    type: HelloWorld,
    ...
}

struct HelloWorld;

module_init()module_exit()

在使用 C 编写的内核模块中,这两个宏定义了模块的入口函数与退出函数。在 Rust 编写的内核模块中,对应的功能由 trait KernelModuletrait Drop 来实现。trait KernelModule 中定义 init() 函数,会在模块驱动初始化时被调用;trait Drop 是 Rust 的内置 trait,其中定义的 drop() 函数会在变量生命周期结束时被调用。

编译与链接

所有的内核模块文件会首先被编译成 .o 目标文件,之后由内核链接器将这些 .o 文件和自动生成的模块目标文件 .mod.o 一起链接成为 .ko 文件。这个 .ko 文件符合动态库 ELF 文件格式,能够被内核识别并加载。

其他

完整的介绍 Rust 是如何被集成进内核的文章可以在 我的 Github 上找到,由于写的仓促,可能存在一些不足,还请见谅。


作者:苏子彬,阿里云 PAI 平台开发工程师,主要从事 Linux 系统及驱动的相关开发,曾为 PAI 平台编写 FPGA 加速卡驱动。

了解有关项目编程语言的详细信息。

 title=

近来,GitHub 添加了一个小指标来展示项目的细节,包括项目使用的编程语言。在这之前,对一个新的贡献者来说,了解他们感兴趣的项目的信息是较为困难的。

这个补充很有帮助,但是如果您想知道有关本地存储库中项目的相同信息该怎么办呢? 这正是 Tokei 派上用场的地方。这是一个当你想和精通不同语言的人想要构建一个项目时可以告诉你项目的代码数据的特别有用的工具。

探索 Tokei

据其 README,“Tokei 是一个可以展示你的代码数据的程序。Tokei 将会展示文件的数量,和这些文件中不同语言的代码、注释、空白的行数。”它的 v.12.1.0 版本 elaborates 是这样子介绍的,“Tokei 是一个快速准确的代码分析 CLI 工具和库,可以使你轻松快速地在你的代码库中看到有多少空白、评论和代码行”。它能够识别超过 150 种编程语言。

$ ./tokei ~/exa/src ~/Work/wildfly/jaxrs
==================
Language   Files Lines Code Comments Blank
Java        46    6135  4324  945     632
XML         23    5211  4839  473     224
---------------------------------
Rust
Markdown
-----------------------------------
Total

安装 Tokei

在 Fedora 上安装 Tokei:

$ sudo dnf install tokei

用 Rust's Cargo 包管理器安装:

$ cargo install tokei

使用 Tokei

要列出当前目录中的代码统计:

$ tokei
===============================================================================
 Language            Files        Lines         Code     Comments       Blanks
===============================================================================
 Ada                    10         2840         1681          560          599
 Assembly                4         2508         1509          458          541
 GNU Style Assembly      4         2751         1528          748          475
 Autoconf               16         2294         1153          756          385
 Automake                1           45           34            0           11
 BASH                    4         1895         1602          133          160
 Batch                   2            4            4            0            0
 C                     330       206433       150241        23402        32790
 C Header              342        60941        24682        29143         7116
 CMake                  48         4572         3459          548          565
 C#                      9         1615          879          506          230
 C++                     5          907          599          136          172
 Dockerfile              2           16           10            0            6
 Fish                    1           87           77            5            5
 HTML                    1          545          544            1            0
 JSON                    5         8995         8995            0            0
 Makefile               10          504          293           72          139
 Module-Definition      12         1183         1046           65           72
 MSBuild                 1          141          140            0            1
 Pascal                  4         1443         1016          216          211
 Perl                    2          189          137           16           36
 Python                  4         1257          949          112          196
 Ruby                    1           23           18            1            4
 Shell                  15         1860         1411          222          227
 Plain Text             35        29425            0        26369         3056
 TOML                   64         3180         2302          453          425
 Visual Studio Pro|     30        14597        14597            0            0
 Visual Studio Sol|      6          655          650            0            5
 XML                     1          116           95           17            4
 YAML                    2           81           56           12           13
 Zsh                     1           59           48            8            3
-------------------------------------------------------------------------------
 Markdown               55         4677            0         3214         1463
 |- C                    1            2            2            0            0
 |- Rust                19          336          268           20           48
 |- TOML                23           61           60            0            1
 (Total)                           5076          330         3234         1512
-------------------------------------------------------------------------------
 Rust                  496       210966       188958         5348        16660
 |- Markdown           249        17676         1551        12502         3623
 (Total)                         228642       190509        17850        20283
===============================================================================
 Total                1523       566804       408713        92521        65570
===============================================================================
$

下面的命令打印出了支持的语言和拓展:

$ tokei -l
ABNF
ABAP
ActionScript
Ada
Agda
Alex
Alloy
Arduino C++
AsciiDoc
ASN.1
ASP
ASP.NET
Assembly
GNU Style Assembly

如果你在两个文件夹上运行 tokei 并指定其位置作为参数,它将以先入先出的规则打印单个文件的统计数据:

 title=

默认情况下,tokei 仅仅输出有关语言的数据,但是使用 --files 标记可提供单个文件统计信息:

$ tokei ~/exa/src --files
===========================================================================================
 Language                              Files      Lines      Code             Comments     Blanks
===========================================================================================
 Rust                                   54          9339        7070             400       1869
 |- Markdown                            33          1306           0            1165        141
 (Total)                                           10645        7070        1565       2010
-------------------------------------------------------------------------------------------
 /home/ssur/exa/src/theme/default_theme.rs           130         107           0         23
 /home/ssur/exa/src/output/render/times.rs            30          24           0          6
 /home/ssur/exa/src/output/render/users.rs            98          76           0         22
 /home/ssur/exa/src/output/render/size.rs            182         141           3         38
 /home/ssur/exa/src/output/render/octal.rs           116          88           0         28
 /home/ssur/exa/src/output/render/mod.rs              33          20           3         10
 /home/ssur/exa/src/output/render/inode.rs            28          20           0          8
 /home/ssur/exa/src/output/render/links.rs            87          65           0         22
 /home/ssur/exa/src/output/render/groups.rs          123          93           0         30
 |ome/ssur/exa/src/output/render/filetype.rs          31          26           0          5
 /home/ssur/exa/src/output/render/blocks.rs           57          40           0         17
 /home/ssur/exa/src/output/render/git.rs             108          87           0         21
 |/ssur/exa/src/output/render/permissions.rs         204         160           3         41
 /home/ssur/exa/src/output/grid.rs                    67          51           3         13
 /home/ssur/exa/src/output/escape.rs                  26          18           4          4
 /home/ssur/exa/src/theme/lsc.rs                     235         158          39         38
 /home/ssur/exa/src/options/theme.rs                 159         124           6         29
 /home/ssur/exa/src/options/file_name.rs              46          39           0          7
 /home/ssur/exa/src/options/flags.rs                  84          63           6         15
 /home/ssur/exa/src/fs/mod.rs                         10           8           0          2
 /home/ssur/exa/src/fs/feature/mod.rs                 33          25           0          8
-- /home/ssur/exa/src/output/time.rs ---------------------------------------------------------------
 |- Rust                                             215          170          5         40
 |- Markdown                                          28            0         25          3

总结

我发现使用 tokei 来了解我的代码统计数据十分容易。另一个使用 tokei 的好处就是它可以用作为一个很容易集成到其他项目的库。访问 Tokei 的 Crate.io pageDocs.rs 网站来了解其更多用法。如果你想参与其中,你也可以通过它的 GitHub 仓库 来为 Tokei 作贡献。

你是否觉得 Tokei 很有用呢?可以在下方的评论区告诉我们。


via: https://opensource.com/article/21/6/tokei

作者:Sudeshna Sur 选题:lujun9972 译者:zepoch 校对:wxy

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

第一步是选择配置文件的格式:INI、JSON、YAML 或 TOML。

 title=

有时,程序需要足够的参数,将它们全部作为命令行参数或环境变量既不让人愉快也不可行。 在这些情况下,你将需要使用配置文件。

有几种流行的配置文件格式。其中包括古老的(虽然有时定义不明确)INI 格式,虽然流行但有时难以手写的 JSON 格式,使用广泛但有时在细节方面令人意外的 YAML 格式,以及很多人还没有听说过的最新出现的 TOML。

你的首要任务是选择一种格式,然后记录该选择。解决了这个简单的部分之后就是时候解析配置了。

有时,在配置中拥有一个与“抽象“数据相对应的类是一个不错的想法。因为这段代码不会对配置做任何事情,所以这是展示解析逻辑最简单的方式。

想象一下文件处理器的配置:它包括一个输入目录、一个输出目录和要提取的文件。

配置类的抽象定义可能类似于:

from __future__ import annotations
import attr

@attr.frozen
class Configuration:
    @attr.frozen
    class Files:
        input_dir: str
        output_dir: str
    files: Files
    @attr.frozen
    class Parameters:
        patterns: List[str]
    parameters: Parameters

为了使特定于格式的代码更简单,你还需要编写一个函数来从字典中解析此类。请注意,这假设配置将使用破折号,而不是下划线。 这种差异并不少见。

def configuration_from_dict(details):
    files = Configuration.Files(
        input_dir=details["files"]["input-dir"],
        output_dir=details["files"]["output-dir"],
    )
    parameters = Configuration.Paraneters(
        patterns=details["parameters"]["patterns"]
    )
    return Configuration(
        files=files,
        parameters=parameters,
    )

JSON

JSON(JavaScript Object Notation)是一种类似于 JavaScript 的格式。

以下是 JSON 格式的示例配置:

json_config = """
{
    "files": {
        "input-dir": "inputs",
        "output-dir": "outputs"
    },
    "parameters": {
        "patterns": [
            "*.txt",
            "*.md"
        ]
    }
}
"""

解析逻辑使用 json 模块将 JSON 解析为 Python 的内置数据结构(字典、列表、字符串),然后从字典中创建类:

import json
def configuration_from_json(data):
    parsed = json.loads(data)
    return configuration_from_dict(parsed)

INI

INI 格式,最初只在 Windows 上流行,之后成为配置标准格式。

这是与 INI 相同的配置:

ini_config="""
[files]
input-dir = inputs
output-dir = outputs

[parameters]
patterns = ['*.txt', '*.md']
"""

Python 可以使用内置的 configparser 模块解析它。解析器充当类似 dict 的对象,因此可以直接传递给 configuration_from_dict

import configparser

def configuration_from_ini(data):
    parser = configparser.ConfigParser()
    parser.read_string(data)
    return configuration_from_dict(parser)

YAML

YAML(Yet Another Markup Language)是 JSON 的扩展,旨在更易于手动编写。为了实现了这一点,部分原因是有一个很长的规范。

以下是 YAML 中的相同配置:

yaml_config = """
files:
  input-dir: inputs
  output-dir: outputs
parameters:
  patterns:
  - '*.txt'
  - '*.md'
"""

要让 Python 解析它,你需要安装第三方模块。最受欢迎的是PyYAMLpip install pyyaml)。 YAML 解析器还返回可以传递给 configuration_from_dict 的内置 Python 数据类型。但是,YAML 解析器需要一个字节流,因此你需要将字符串转换为字节流。

import io
import yaml
def configuration_from_yaml(data):
    fp = io.StringIO(data)
    parsed = yaml.safe_load(fp)
    return configuration_from_dict(parsed)

TOML

TOML(Tom's Own Markup Language)旨在成为 YAML 的轻量级替代品。其规范比较短,已经在一些地方流行了(比如 Rust 的包管理器 Cargo 就用它来进行包配置)。

这是与 TOML 相同的配置:

toml_config = """
[files]
input-dir = "inputs"
output-dir = "outputs"

[parameters]
patterns = [ "*.txt", "*.md",]
"""

为了解析 TOML,你需要安装第三方包。最流行的一种被简单地称为 toml。 与 YAML 和 JSON 一样,它返回基本的 Python 数据类型。

import toml
def configuration_from_toml(data):
    parsed = toml.loads(data)
    return configuration_from_dict(parsed)

总结

选择配置格式是一种微妙的权衡。但是,一旦你做出决定,Python 就可以使用少量代码来解析大多数流行的格式。


via: https://opensource.com/article/21/6/parse-configuration-files-python

作者:Moshe Zadka 选题:lujun9972 译者:zepoch 校对:wxy

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

昨天我和一些人在闲聊的时候,他们说他们并不真正了解栈是如何工作的,而且也不知道如何去查看栈空间。

这是一个快速教程,介绍如何使用 GDB 查看 C 程序的栈空间。我认为这对于 Rust 程序来说也是相似的。但我这里仍然使用 C 语言,因为我发现用它更简单,而且用 C 语言也更容易写出错误的程序。

我们的测试程序

这里是一个简单的 C 程序,声明了一些变量,从标准输入读取两个字符串。一个字符串在堆上,另一个字符串在栈上。

#include <stdio.h>
#include <stdlib.h>

int main() {
    char stack_string[10] = "stack";
    int x = 10;
    char *heap_string;

    heap_string = malloc(50);

    printf("Enter a string for the stack: ");
    gets(stack_string);
    printf("Enter a string for the heap: ");
    gets(heap_string);
    printf("Stack string is: %s\n", stack_string);
    printf("Heap string is: %s\n", heap_string);
    printf("x is: %d\n", x);
}

这个程序使用了一个你可能从来不会使用的极为不安全的函数 gets 。但我是故意这样写的。当出现错误的时候,你就知道是为什么了。

第 0 步:编译这个程序

我们使用 gcc -g -O0 test.c -o test 命令来编译这个程序。

-g 选项会在编译程序中将调式信息也编译进去。这将会使我们查看我们的变量更加容易。

-O0 选项告诉 gcc 不要进行优化,我要确保我们的 x 变量不会被优化掉。

第一步:启动 GDB

像这样启动 GDB:

$ gdb ./test

它打印出一些 GPL 信息,然后给出一个提示符。让我们在 main 函数这里设置一个断点:

(gdb) b main

然后我们就可以运行程序:

(gdb) b main
Starting program: /home/bork/work/homepage/test
Breakpoint 1, 0x000055555555516d in main ()

(gdb) run
Starting program: /home/bork/work/homepage/test

Breakpoint 1, main () at test.c:4
4   int main() {

好了,现在程序已经运行起来了。我们就可以开始查看栈空间了。

第二步:查看我们变量的地址

让我们从了解我们的变量开始。它们每个都在内存中有一个地址,我们可以像这样打印出来:

(gdb) p &x
$3 = (int *) 0x7fffffffe27c
(gdb) p &heap_string
$2 = (char **) 0x7fffffffe280
(gdb) p &stack_string
$4 = (char (*)[10]) 0x7fffffffe28e

因此,如果我们查看那些地址的堆栈,那我们应该能够看到所有的这些变量!

概念:栈指针

我们将需要使用栈指针,因此我将尽力对其进行快速解释。

有一个名为 ESP 的 x86 寄存器,称为“ 栈指针 stack pointer ”。 基本上,它是当前函数的栈起始地址。 在 GDB 中,你可以使用 $sp 来访问它。 当你调用新函数或从函数返回时,栈指针的值会更改。

第三步:在 main 函数开始的时候,我们查看一下在栈上的变量

首先,让我们看一下 main 函数开始时的栈。 现在是我们的堆栈指针的值:

(gdb) p $sp
$7 = (void *) 0x7fffffffe270

因此,我们当前函数的栈起始地址是 0x7fffffffe270,酷极了。

现在,让我们使用 GDB 打印出当前函数堆栈开始后的前 40 个字(即 160 个字节)。 某些内存可能不是栈的一部分,因为我不太确定这里的堆栈有多大。 但是至少开始的地方是栈的一部分。

我已粗体显示了 stack_stringheap_stringx 变量的位置,并改变了颜色:

  • x 是红色字体,并且起始地址是 0x7fffffffe27c
  • heap_string 是蓝色字体,起始地址是 0x7fffffffe280
  • stack_string 是紫色字体,起始地址是 0x7fffffffe28e

你可能会在这里注意到的一件奇怪的事情是 x 的值是 0x5555,但是我们将 x 设置为 10! 那是因为直到我们的 main 函数运行之后才真正设置 x ,而我们现在才到了 main 最开始的地方。

第三步:运行到第十行代码后,再次查看一下我们的堆栈

让我们跳过几行,等待变量实际设置为其初始化值。 到第 10 行时,x 应该设置为 10

首先我们需要设置另一个断点:

(gdb) b test.c:10
Breakpoint 2 at 0x5555555551a9: file test.c, line 11.

然后继续执行程序:

(gdb) continue
Continuing.

Breakpoint 2, main () at test.c:11
11      printf("Enter a string for the stack: ");

好的! 让我们再来看看堆栈里的内容! gdb 在这里格式化字节的方式略有不同,实际上我也不太关心这些(LCTT 译注:可以查看 GDB 手册中 x 命令,可以指定 c 来控制输出的格式)。 这里提醒一下你,我们的变量在栈上的位置:

  • x 是红色字体,并且起始地址是 0x7fffffffe27c
  • heap_string 是蓝色字体,起始地址是 0x7fffffffe280
  • stack_string 是紫色字体,起始地址是 0x7fffffffe28e

在继续往下看之前,这里有一些有趣的事情要讨论。

stack_string 在内存中是如何表示的

现在(第 10 行),stack_string 被设置为字符串stack。 让我们看看它在内存中的表示方式。

我们可以像这样打印出字符串中的字节(LCTT 译注:可以通过 c 选项直接显示为字符):

(gdb) x/10x stack_string
0x7fffffffe28e: 0x73    0x74    0x61    0x63    0x6b    0x00    0x00    0x00
0x7fffffffe296: 0x00    0x00

stack 是一个长度为 5 的字符串,相对应 5 个 ASCII 码- 0x730x740x610x630x6b0x73 是字符 s 的 ASCII 码。 0x74t 的 ASCII 码。等等...

同时我们也使用 x/1s 可以让 GDB 以字符串的方式显示:

(gdb) x/1s stack_string
0x7fffffffe28e: "stack"

heap_stringstack_string 有何不同

你已经注意到了 stack_stringheap_string 在栈上的表示非常不同:

  • stack_string 是一段字符串内容(stack
  • heap_string 是一个指针,指向内存中的某个位置

这里是 heap_string 变量在内存中的内容:

0xa0  0x92  0x55  0x55  0x55  0x55  0x00  0x00

这些字节实际上应该是从右向左读:因为 x86 是小端模式,因此,heap_string 中所存放的内存地址 0x5555555592a0

另一种方式查看 heap_string 中存放的内存地址就是使用 p 命令直接打印 :

(gdb) p heap_string
$6 = 0x5555555592a0 ""

整数 x 的字节表示

x 是一个 32 位的整数,可由 0x0a 0x00 0x00 0x00 来表示。

我们还是需要反向来读取这些字节(和我们读取 heap_string 需要反过来读是一样的),因此这个数表示的是 0x000000000a 或者是 0x0a,它是一个数字 10;

这就让我把把 x 设置成了 10

第四步:从标准输入读取

好了,现在我们已经初始化我们的变量,我们来看一下当这段程序运行的时候,栈空间会如何变化:

printf("Enter a string for the stack: ");
gets(stack_string);
printf("Enter a string for the heap: ");
gets(heap_string);

我们需要设置另外一个断点:

(gdb) b test.c:16
Breakpoint 3 at 0x555555555205: file test.c, line 16.

然后继续执行程序:

(gdb) continue
Continuing.

我们输入两个字符串,为栈上存储的变量输入 123456789012 并且为在堆上存储的变量输入 bananas;

让我们先来看一下 stack_string(这里有一个缓存区溢出)

(gdb) x/1s stack_string
0x7fffffffe28e: "123456789012"

这看起来相当正常,对吗?我们输入了 12345679012,然后现在它也被设置成了 12345679012(LCTT 译注:实测 gcc 8.3 环境下,会直接段错误)。

但是现在有一些很奇怪的事。这是我们程序的栈空间的内容。有一些紫色高亮的内容。

令人奇怪的是 stack_string 只支持 10 个字节。但是现在当我们输入了 13 个字符以后,发生了什么?

这是一个典型的缓冲区溢出,stack_string 将自己的数据写在了程序中的其他地方。在我们的案例中,这还没有造成问题,但它会使你的程序崩溃,或者更糟糕的是,使你面临非常糟糕的安全问题。

例如,假设 stack_string 在内存里的位置刚好在 heap_string 之前。那我们就可能覆盖 heap_string 所指向的地址。我并不确定 stack_string 之后的内存里有一些什么。但我们也许可以用它来做一些诡异的事情。

确实检测到了有缓存区溢出

当我故意写很多字符的时候:

 ./test
Enter a string for the stack: 01234567891324143
Enter a string for the heap: adsf
Stack string is: 01234567891324143
Heap string is: adsf
x is: 10
*** stack smashing detected ***: terminated
fish: Job 1, './test' terminated by signal SIGABRT (Abort)

这里我猜是 stack_string 已经到达了这个函数栈的底部,因此额外的字符将会被写在另一块内存中。

当你故意去使用这个安全漏洞时,它被称为“堆栈粉碎”,而且不知何故有东西在检测这种情况的发生。

我也觉得这很有趣,虽然程序被杀死了,但是当缓冲区溢出发生时它不会立即被杀死——在缓冲区溢出之后再运行几行代码,程序才会被杀死。 好奇怪!

这些就是关于缓存区溢出的所有内容。

现在我们来看一下 heap_string

我们仍然将 bananas 输入到 heap_string 变量中。让我们来看一下内存中的样子。

这是在我们读取了字符串以后,heap_string 在栈空间上的样子:

需要注意的是,这里的值是一个地址。并且这个地址并没有改变,但是我们来看一下指向的内存上的内容。

(gdb) x/10x 0x5555555592a0
0x5555555592a0: 0x62    0x61    0x6e    0x61    0x6e    0x61    0x73    0x00
0x5555555592a8: 0x00    0x00

看到了吗,这就是字符串 bananas 的字节表示。这些字节并不在栈空间上。他们存在于内存中的堆上。

堆和栈到底在哪里?

我们已经讨论过栈和堆是不同的内存区域,但是你怎么知道它们在内存中的位置呢?

每个进程都有一个名为 /proc/$PID/maps 的文件,它显示了每个进程的内存映射。 在这里你可以看到其中的栈和堆。

$ cat /proc/24963/maps
... lots of stuff omitted ...
555555559000-55555557a000 rw-p 00000000 00:00 0                          [heap]
... lots of stuff omitted ...
7ffffffde000-7ffffffff000 rw-p 00000000 00:00 0                          [stack]

需要注意的一件事是,这里堆地址以 0x5555 开头,栈地址以 0x7fffff 开头。 所以很容易区分栈上的地址和堆上的地址之间的区别。

像这样使用 gdb 真的很有帮助

这有点像旋风之旅,虽然我没有解释所有内容,但希望看到数据在内存中的实际情况可以使你更清楚地了解堆栈的实际情况。

我真的建议像这样来把玩一下 gdb —— 即使你不理解你在内存中看到的每一件事,我发现实际上像这样看到我程序内存中的数据会使抽象的概念,比如“栈”和“堆”和“指针”更容易理解。

更多练习

一些关于思考栈的后续练习的想法(没有特定的顺序):

  • 尝试将另一个函数添加到 test.c 并在该函数的开头创建一个断点,看看是否可以从 main 中找到堆栈! 他们说当你调用一个函数时“堆栈会变小”,你能在 gdb 中看到这种情况吗?
  • 从函数返回一个指向栈上字符串的指针,看看哪里出了问题。 为什么返回指向栈上字符串的指针是不好的?
  • 尝试在 C 中引起堆栈溢出,并尝试通过在 gdb 中查看堆栈溢出来准确理解会发生什么!
  • 查看 Rust 程序中的堆栈并尝试找到变量!
  • 噩梦课程 中尝试一些缓冲区溢出挑战。每个问题的答案写在 README 文件中,因此如果你不想被宠坏,请避免先去看答案。 所有这些挑战的想法是给你一个二进制文件,你需要弄清楚如何导致缓冲区溢出以使其打印出 flag 字符串。

via: https://jvns.ca/blog/2021/05/17/how-to-look-at-the-stack-in-gdb/

作者:Julia Evans 选题:lujun9972 译者:amwps290 校对:wxy

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