Stephan Avenwedde 发布的文章

跟随这个演示,学习如何在 Rust 中使用文件系统模块。

知道如何读写文件对各种用途都很有用。在 Rust 中,这项任务是通过标准库中的文件系统模块(std::fs)完成的。在这篇文章中,我将向你介绍如何使用这个模块。

为了演示这项任务,我准备了一些示例代码,也可以在 GitHub 上找到。

准备工作

在使用 Rust 时,失败的函数会返回 Result 类型。尤其是文件系统模块会返回专门的类型 std::io::Result<T, Error>。有了这些知识,你可以从 main() 函数中返回相同的类型:

fn  main() ->  std::io::Result<()> {
/* ...code comes here... */

Rust 文件写入

在 Rust 中执行文件的 I/O 操作是相对容易的。写入文件可以简化为一行:

use  std::fs;
fs::write("favorite_websites.txt", b"opensource.com")?;
Ok(())

使用错误传播操作符 (?),错误信息被传递到调用函数中,随后可以处理错误。由于 main() 是调用栈中唯一的其他函数,如果写操作失败,错误信息将被传递到控制台输出。

fs::write 函数的语法是非常先进的。第一个参数是文件路径,它必须是 std::path::Path 类型。第二个参数是内容,它实际上是一个字节切片([u8])。Rust 将传递的参数转换为正确的类型。幸运的是,这些类型基本上是下面的例子中所处理的唯一类型。

使用文件描述符类型 std::fs::File 可以实现对写操作更简洁的访问:

let mut file = fs::File::create("favorite_websites.txt")?;
file.write_all(b"opensource.com\n")?;
Ok(())

由于文件类型实现了 Write 特性,所以可以使用相关的方法来写入文件。然而,create 方法可以覆盖一个已经存在的文件。

为了获得对文件描述符的更多控制,必须使用 std::fs::OpenOptions 类型。这提供了类似于其他语言中的打开模式:

let mut file = fs::OpenOptions::new()
                            .append(true)
                            .open("favorite_websites.txt")?;
                            
file.write_all(b"sourceforge.net\n")?;

Rust 文件读取

适用于写的东西也适用于读。读取也可以通过简单的一行代码来完成:

let websites = fs::read_to_string("favorite_websites.txt")?;

以上一行读取文件的内容并返回一个字符串。除了读取字符串,还有 std::fs::read 函数,如果文件包含二进制数据,该函数会将数据读成一个字节向量。

下一个例子显示了如何将文件的内容读入内存,随后逐行打印到控制台:

let file = fs::File::open("favorite_websites.txt")?;
let lines = io::BufReader::new(file).lines();

for line in lines {
    if let Ok(_line) = line {
        println!(">>> {}", _line);
    }
}

总结

如果你已经熟悉了其他编程语言,你可能已经注意到没有 close- 函数(或类似的)来释放文件句柄。在 Rust 中,当相关变量超出作用域,文件句柄就会被释放。为了定义关闭行为,可以在文件表示的周围应用作用域 ({ })。我建议你熟悉 ReadWrite 特性,因为你可以在许多其他类型中找到这个特性的实现。


via: https://opensource.com/article/23/1/read-write-files-rust

作者:Stephan Avenwedde 选题:lkxed 译者:geekpi 校对:wxy

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

这是一个演示如何创建 POSIX 兼容的间隔定时器的教程。

 title=

对开发人员来说,定时某些事件是一项常见任务。定时器的常见场景是看门狗、任务的循环执行,或在特定时间安排事件。在这篇文章中,我将演示如何使用 timer\_create(...) 创建一个 POSIX 兼容的间隔定时器。

你可以从 GitHub 下载下面样例的源代码。

准备 Qt Creator

我使用 Qt Creator 作为该样例的 IDE。为了在 Qt Creator 运行和调试样例代码,请克隆 GitHub 上的仓库,打开 Qt Creator,在 “ 文件 File -> 打开文件或项目…… Open File or Project... ” 并选择 “CMakeLists.txt”:

Qt Creator open project

在 Qt Creator 中打开项目

选择工具链之后,点击 “ 配置项目 Configure Project ”。这个项目包括三个独立的样例(我们在这篇文章中将只会用到其中的两个)。使用绿色标记出来的菜单,可以在每个样例的配置之间切换,并为每个样例激活在终端运行 “ 在终端中运行 Run in terminal ”(用黄色标记)。当前用于构建和调试的活动示例可以通过左下角的“ 调试 Debug ” 按钮进行选择(参见下面的橙色标记)。

Project configuration

项目配置

线程定时器

让我们看看 simple_threading_timer.c 样例。这是最简单的一个。它展示了一个调用了超时函数 expired 的间隔定时器是如何被创建的。在每次过期时,都会创建一个新的线程,在其中调用函数 expired

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <signal.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>

void expired(union sigval timer_data);

pid_t gettid(void);

struct t_eventData{
    int myData;
};

int main()
{
    int res = 0;
    timer_t timerId = 0;

    struct t_eventData eventData = { .myData = 0 };

    /*  sigevent 指定了过期时要执行的操作  */
    struct sigevent sev = { 0 };

    /* 指定启动延时时间和间隔时间 
    * it_value和it_interval 不能为零 */

    struct itimerspec its = {   .it_value.tv_sec  = 1,
                                .it_value.tv_nsec = 0,
                                .it_interval.tv_sec  = 1,
                                .it_interval.tv_nsec = 0
                            };

    printf("Simple Threading Timer - thread-id: %d\n", gettid());

    sev.sigev_notify = SIGEV_THREAD;
    sev.sigev_notify_function = &amp;expired;
    sev.sigev_value.sival_ptr = &amp;eventData;

    /* 创建定时器 */
    res = timer_create(CLOCK_REALTIME, &amp;sev, &amp;timerId);

    if (res != 0){
        fprintf(stderr, "Error timer_create: %s\n", strerror(errno));
        exit(-1);
    }

    /* 启动定时器 */
    res = timer_settime(timerId, 0, &amp;its, NULL);

    if (res != 0){
        fprintf(stderr, "Error timer_settime: %s\n", strerror(errno));
        exit(-1);
    }

    printf("Press ETNER Key to Exit\n");
    while(getchar()!='\n'){}
    return 0;
}

void expired(union sigval timer_data){
    struct t_eventData *data = timer_data.sival_ptr;
    printf("Timer fired %d - thread-id: %d\n", ++data->myData, gettid());
}

这种方法的优点是在代码和简单调试方面用量小。缺点是由于到期时创建新线程而增加额外的开销,因此行为不太确定。

中断信号定时器

超时定时器通知的另一种可能性是基于 内核信号。内核不是在每次定时器过期时创建一个新线程,而是向进程发送一个信号,进程被中断,并调用相应的信号处理程序。

由于接收信号时的默认操作是终止进程(参考 signal 手册页),我们必须要提前设置好 Qt Creator,以便进行正确的调试。

当被调试对象接收到一个信号时,Qt Creator 的默认行为是:

  • 中断执行并切换到调试器上下文。
  • 显示一个弹出窗口,通知用户接收到信号。

这两种操作都不需要,因为信号的接收是我们应用程序的一部分。

Qt Creator 在后台使用 GDB。为了防止 GDB 在进程接收到信号时停止执行,进入 “ 工具 Tools -> 选项 Options ” 菜单,选择 “ 调试器 Debugger ”,并导航到 “ 本地变量和表达式 Locals & Expressions ”。添加下面的表达式到 “ 定制调试助手 Debugging Helper Customization ”:

handle SIG34 nostop pass

Signal no stop with error

Sig 34 时不停止

你可以在 GDB 文档 中找到更多关于 GDB 信号处理的信息。

接下来,当我们在信号处理程序中停止时,我们要抑制每次接收到信号时通知我们的弹出窗口:

Signal 34 pop up box

Signal 34 弹出窗口

为此,导航到 “GDB” 标签并取消勾选标记的复选框:

Timer signal windows

定时器信号窗口

现在你可以正确的调试 signal_interrupt_timer。真正的信号定时器的实施会更复杂一些:

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <signal.h>
#include <time.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>

#define UNUSED(x) (void)(x)

static void handler(int sig, siginfo_t *si, void *uc);
pid_t gettid(void);

struct t_eventData{
    int myData;
};

int main()
{
    int res = 0;
    timer_t timerId = 0;

    struct sigevent sev = { 0 };
    struct t_eventData eventData = { .myData = 0 };

    /* 指定收到信号时的操作 */
    struct sigaction sa = { 0 };

    /* 指定启动延时的时间和间隔时间 */
    struct itimerspec its = {   .it_value.tv_sec  = 1,
                                .it_value.tv_nsec = 0,
                                .it_interval.tv_sec  = 1,
                                .it_interval.tv_nsec = 0
                            };

    printf("Signal Interrupt Timer - thread-id: %d\n", gettid());

    sev.sigev_notify = SIGEV_SIGNAL; // Linux-specific
    sev.sigev_signo = SIGRTMIN;
    sev.sigev_value.sival_ptr = &amp;eventData;

    /* 创建定时器 */
    res = timer_create(CLOCK_REALTIME, &amp;sev, &amp;timerId);

    if ( res != 0){
        fprintf(stderr, "Error timer_create: %s\n", strerror(errno));
        exit(-1);
    }

    /* 指定信号和处理程序 */
    sa.sa_flags = SA_SIGINFO;
    sa.sa_sigaction = handler;

    /* 初始化信号 */
    sigemptyset(&amp;sa.sa_mask);

    printf("Establishing handler for signal %d\n", SIGRTMIN);

    /* 注册信号处理程序 */
    if (sigaction(SIGRTMIN, &amp;sa, NULL) == -1){
        fprintf(stderr, "Error sigaction: %s\n", strerror(errno));
        exit(-1);
    }

    /* 启动定时器 */
    res = timer_settime(timerId, 0, &amp;its, NULL);

    if ( res != 0){
        fprintf(stderr, "Error timer_settime: %s\n", strerror(errno));
        exit(-1);
    }

    printf("Press ENTER to Exit\n");
    while(getchar()!='\n'){}
    return 0;
}

static void
handler(int sig, siginfo_t *si, void *uc)
{
    UNUSED(sig);
    UNUSED(uc);
    struct t_eventData *data = (struct t_eventData *) si->_sifields._rt.si_sigval.sival_ptr;
    printf("Timer fired %d - thread-id: %d\n", ++data->myData, gettid());
}

与线程定时器相比,我们必须初始化信号并注册一个信号处理程序。这种方法性能更好,因为它不会导致创建额外的线程。因此,信号处理程序的执行也更加确定。缺点显然是正确调试需要额外的配置工作。

总结

本文中描述的两种方法都是接近内核的定时器的实现。不过,即使 timer\_create(...) 函数是 POSIX 规范的一部分,由于数据结构的细微差别,也不可能在 FreeBSD 系统上编译样例代码。除了这个缺点之外,这种实现还为通用计时应用程序提供了细粒度控制。


via: https://opensource.com/article/21/10/linux-timers

作者:Stephan Avenwedde 选题:lujun9972 译者:FigaroCao 校对:wxy

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

使用 C 扩展为 Python 提供特定功能。

在前一篇文章中,我介绍了 六个 Python 解释器。在大多数系统上,CPython 是默认的解释器,而且根据民意调查显示,它还是最流行的解释器。Cpython 的独有功能是使用扩展 API 用 C 语言编写 Python 模块。用 C 语言编写 Python 模块允许你将计算密集型代码转移到 C,同时保留 Python 的易用性。

在本文中,我将向你展示如何编写一个 C++ 扩展模块。使用 C++ 而不是 C,因为大多数编译器通常都能理解这两种语言。我必须提前说明缺点:以这种方式构建的 Python 模块不能移植到其他解释器中。它们只与 CPython 解释器配合工作。因此,如果你正在寻找一种可移植性更好的与 C 语言模块交互的方式,考虑下使用 ctypes 模块。

源代码

和往常一样,你可以在 GitHub 上找到相关的源代码。仓库中的 C++ 文件有以下用途:

  • my_py_module.cpp: Python 模块 MyModule 的定义
  • my_cpp_class.h: 一个头文件 - 只有一个暴露给 Python 的 C++ 类
  • my_class_py_type.h/cpp: Python 形式的 C++ 类
  • pydbg.cpp: 用于调试的单独应用程序

本文构建的 Python 模块不会有任何实际用途,但它是一个很好的示例。

构建模块

在查看源代码之前,你可以检查它是否能在你的系统上编译。我使用 CMake 来创建构建的配置信息,因此你的系统上必须安装 CMake。为了配置和构建这个模块,可以让 Python 去执行这个过程:

$ python3 setup.py build

或者手动执行:

$ cmake -B build
$ cmake --build build

之后,在 /build 子目录下你会有一个名为 MyModule. so 的文件。

定义扩展模块

首先,看一下 my_py_module.cpp 文件,尤其是 PyInit_MyModule 函数:

PyMODINIT_FUNC
PyInit_MyModule(void) {
    PyObject* module = PyModule_Create(&my_module);
    
    PyObject *myclass = PyType_FromSpec(&spec_myclass);
    if (myclass == NULL){
        return NULL;
    }
    Py_INCREF(myclass);
    
    if(PyModule_AddObject(module, "MyClass", myclass) < 0){
        Py_DECREF(myclass);
        Py_DECREF(module);
        return NULL;
    }
    return module;
}

这是本例中最重要的代码,因为它是 CPython 的入口点。一般来说,当一个 Python C 扩展被编译并作为共享对象二进制文件提供时,CPython 会在同名二进制文件中(<ModuleName>.so)搜索 PyInit_<ModuleName> 函数,并在试图导入时执行它。

无论是声明还是实例,所有 Python 类型都是 PyObject 的一个指针。在此函数的第一部分中,module 通过 PyModule_Create(...) 创建的。正如你在 module 详述(my_py_module,同名文件)中看到的,它没有任何特殊的功能。

之后,调用 PyType\_FromSpec 为自定义类型 MyClass 创建一个 Python 堆类型 定义。一个堆类型对应于一个 Python 类,然后将它赋值给 MyModule 模块。

注意,如果其中一个函数返回失败,则必须减少以前创建的复制对象的引用计数,以便解释器删除它们。

指定 Python 类型

MyClass 详述在 my\_class\_py\_type.h 中可以找到,它作为 PyType\_Spec 的一个实例:

static PyType_Spec spec_myclass = {
    "MyClass",                                  // name
    sizeof(MyClassObject) + sizeof(MyClass),    // basicsize
    0,                                          // itemsize
    Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,   // flags
    MyClass_slots                               // slots
};

它定义了一些基本类型信息,它的大小包括 Python 表示的大小(MyClassObject)和普通 C++ 类的大小(MyClass)。MyClassObject 定义如下:

typedef struct {
    PyObject_HEAD
    int         m_value;
    MyClass*    m_myclass;
} MyClassObject;

Python 表示的话就是 PyObject 类型,由 PyObject_HEAD 宏和其他一些成员定义。成员 m_value 视为普通类成员,而成员 m_myclass 只能在 C++ 代码内部访问。

PyType\_Slot 定义了一些其他功能:

static PyType_Slot MyClass_slots[] = {
    {Py_tp_new,     (void*)MyClass_new},
    {Py_tp_init,    (void*)MyClass_init},
    {Py_tp_dealloc, (void*)MyClass_Dealloc},
    {Py_tp_members, MyClass_members},
    {Py_tp_methods, MyClass_methods},
    {0, 0} /* Sentinel */
};

在这里,设置了一些初始化和析构函数的跳转,还有普通的类方法和成员,还可以设置其他功能,如分配初始属性字典,但这是可选的。这些定义通常以一个哨兵结束,包含 NULL 值。

要完成类型详述,还包括下面的方法和成员表:

static PyMethodDef MyClass_methods[] = {
    {"addOne", (PyCFunction)MyClass_addOne, METH_NOARGS,  PyDoc_STR("Return an incrmented integer")},
    {NULL, NULL} /* Sentinel */
};

static struct PyMemberDef MyClass_members[] = {
    {"value", T_INT, offsetof(MyClassObject, m_value)},
    {NULL} /* Sentinel */
};

在方法表中,定义了 Python 方法 addOne,它指向相关的 C++ 函数 MyClass_addOne。它充当了一个包装器,它在 C++ 类中调用 addOne() 方法。

在成员表中,只有一个为演示目的而定义的成员。不幸的是,在 PyMemberDef 中使用的 offsetof 不允许添加 C++ 类型到 MyClassObject。如果你试图放置一些 C++ 类型的容器(如 std::optional),编译器会抱怨一些内存布局相关的警告。

初始化和析构

MyClass_new 方法只为 MyClassObject 提供一些初始值,并为其类型分配内存:

PyObject *MyClass_new(PyTypeObject *type, PyObject *args, PyObject *kwds){
    std::cout << "MtClass_new() called!" << std::endl;

    MyClassObject *self;
    self = (MyClassObject*) type->tp_alloc(type, 0);
    if(self != NULL){ // -> 分配成功
        // 赋初始值
        self->m_value   = 0;
        self->m_myclass = NULL; 
    }
    return (PyObject*) self;
}

实际的初始化发生在 MyClass_init 中,它对应于 Python 中的 \_\_init\_\_() 方法:

int MyClass_init(PyObject *self, PyObject *args, PyObject *kwds){
    
    ((MyClassObject *)self)->m_value = 123;
    
    MyClassObject* m = (MyClassObject*)self;
    m->m_myclass = (MyClass*)PyObject_Malloc(sizeof(MyClass));

    if(!m->m_myclass){
        PyErr_SetString(PyExc_RuntimeError, "Memory allocation failed");
        return -1;
    }

    try {
        new (m->m_myclass) MyClass();
    } catch (const std::exception& ex) {
        PyObject_Free(m->m_myclass);
        m->m_myclass = NULL;
        m->m_value   = 0;
        PyErr_SetString(PyExc_RuntimeError, ex.what());
        return -1;
    } catch(...) {
        PyObject_Free(m->m_myclass);
        m->m_myclass = NULL;
        m->m_value   = 0;
        PyErr_SetString(PyExc_RuntimeError, "Initialization failed");
        return -1;
    }

    return 0;
}

如果你想在初始化过程中传递参数,必须在此时调用 PyArg\_ParseTuple。简单起见,本例将忽略初始化过程中传递的所有参数。在函数的第一部分中,PyObject 指针(self)被强转为 MyClassObject 类型的指针,以便访问其他成员。此外,还分配了 C++ 类的内存,并执行了构造函数。

注意,为了防止内存泄漏,必须仔细执行异常处理和内存分配(还有释放)。当引用计数将为零时,MyClass_dealloc 函数负责释放所有相关的堆内存。在文档中有一个章节专门讲述关于 C 和 C++ 扩展的内存管理。

包装方法

从 Python 类中调用相关的 C++ 类方法很简单:

PyObject* MyClass_addOne(PyObject *self, PyObject *args){
    assert(self);

    MyClassObject* _self = reinterpret_cast<MyClassObject*>(self);
    unsigned long val = _self->m_myclass->addOne();
    return PyLong_FromUnsignedLong(val);
}

同样,PyObject 参数(self)被强转为 MyClassObject 类型以便访问 m_myclass,它指向 C++ 对应类实例的指针。有了这些信息,调用 addOne() 类方法,并且结果以 Python 整数对象 返回。

3 种方法调试

出于调试目的,在调试配置中编译 CPython 解释器是很有价值的。详细描述参阅 官方文档。只要下载了预安装的解释器的其他调试符号,就可以按照下面的步骤进行操作。

GNU 调试器

当然,老式的 GNU 调试器(GDB) 也可以派上用场。源码中包含了一个 gdbinit 文件,定义了一些选项和断点,另外还有一个 gdb.sh 脚本,它会创建一个调试构建并启动一个 GDB 会话:

Gnu 调试器(GDB)对于 Python C 和 C++ 扩展非常有用

GDB 使用脚本文件 main.py 调用 CPython 解释器,它允许你轻松定义你想要使用 Python 扩展模块执行的所有操作。

C++ 应用

另一种方法是将 CPython 解释器嵌入到一个单独的 C++ 应用程序中。可以在仓库的 pydbg.cpp 文件中找到:

int main(int argc, char *argv[], char *envp[])
{
    Py_SetProgramName(L"DbgPythonCppExtension");
    Py_Initialize();

    PyObject *pmodule = PyImport_ImportModule("MyModule");
    if (!pmodule) {
        PyErr_Print();
        std::cerr << "Failed to import module MyModule" << std::endl;
        return -1;
    }

    PyObject *myClassType = PyObject_GetAttrString(pmodule, "MyClass");
    if (!myClassType) {
        std::cerr << "Unable to get type MyClass from MyModule" << std::endl;
        return -1;
    }

    PyObject *myClassInstance = PyObject_CallObject(myClassType, NULL);

    if (!myClassInstance) {
        std::cerr << "Instantioation of MyClass failed" << std::endl;
        return -1;
    }

    Py_DecRef(myClassInstance); // invoke deallocation
    return 0;
}

使用 高级接口,可以导入扩展模块并对其执行操作。它允许你在本地 IDE 环境中进行调试,还能让你更好地控制传递或来自扩展模块的变量。

缺点是创建一个额外的应用程序的成本很高。

VSCode 和 VSCodium LLDB 扩展

使用像 CodeLLDB 这样的调试器扩展可能是最方便的调试选项。仓库包含了一些 VSCode/VSCodium 的配置文件,用于构建扩展,如 task.jsonCMake Tools 和调用调试器(launch.json)。这种方法结合了前面几种方法的优点:在图形 IDE 中调试,在 Python 脚本文件中定义操作,甚至在解释器提示符中动态定义操作。

VSCodium 有一个集成的调试器。

用 C++ 扩展 Python

Python 的所有功能也可以从 C 或 C++ 扩展中获得。虽然用 Python 写代码通常认为是一件容易的事情,但用 C 或 C++ 扩展 Python 代码是一件痛苦的事情。另一方面,虽然原生 Python 代码比 C++ 慢,但 C 或 C++ 扩展可以将计算密集型任务提升到原生机器码的速度。

你还必须考虑 ABI 的使用。稳定的 ABI 提供了一种方法来保持旧版本 CPython 的向后兼容性,如 文档 所述。

最后,你必须自己权衡利弊。如果你决定使用 C 语言来扩展 Python 中的一些功能,你已经看到了如何实现它。


via: https://opensource.com/article/22/11/extend-c-python

作者:Stephan Avenwedde 选题:lkxed 译者:MjSeven 校对:wxy

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

VirtualBox 能帮助任何人(即使是命令行新手)安装一个新的虚拟机。

 title=

VirtualBox 能让任何人都可以轻松安装 Linux 虚拟机。你不需要有使用命令行的经验,就可以自己安装一个简单的 Linux 虚拟机。在虚拟机方面,我精通很多东西,但这篇文章将向新手展示如何安装一个 Linux 虚拟机。此外,这篇文章还概述了如何使用开源虚拟机管理程序 VirtualBox ,来运行以及安装一个测试目的的 Linux 系统。

一些术语

在开始之前,你需要了解在本安装教程中的两个操作系统(OS)之间的区别:

  • 主机系统 host system :这指的是你安装 VirtualBox 的操作系统(即本机的操作系统)。
  • 客体系统 guest system :这指的是你想要在主机系统之上运行的虚拟化系统。

在输入/输出、网络、文件访问、剪贴板、音频和视频方面,主机系统和客体系统都必须能够交互。

在本教程中,我将使用 Windows 10 作为 主机系统Fedora 33 作为 客体系统

安装前的准备

当我们谈论虚拟化时,实际上,我们指的是 硬件辅助虚拟化。硬件辅助虚拟化需要兼容的 CPU。过去十年来,几乎每个普通的 x86 CPU 都有这一功能。AMD 公司称这样的 x86 CPU 是具有 AMD 虚拟化技术(AMD-V) 的处理器,英特尔公司则称其是具有 Intel 虚拟化技术(VT-x) 的处理器。虚拟化功能增加了一些额外的 CPU 指令,你可以在 BIOS 中启用或禁用这些指令。

在安装虚拟机之前:

  • 确保在 BIOS 中启用了虚拟化技术(AMD-V 或 VT-x)。
  • 下载并安装好 VirtualBox

准备虚拟机

下载你要用的 Linux 发行版的镜像文件。下载 32 位还是 64 位的操作系统镜像都没有关系,因为在 32 位的主机系统上也可以启动 64 位的操作系统镜像(当然内存的使用会受限),反之亦然。

注意事项: 如果可以的话,请下载附带有 逻辑卷管理器)(LVM)的 Linux 发行版。LVM 会将文件系统与物理硬盘驱动器解耦。如果你的空间不足时,这能够让你增加客体系统的硬盘驱动器的大小。

现在,打开 VirtualBox,然后单击黄色的“ 新建 New ”按钮:

 title=

接下来,配置客体操作系统允许使用多少内存:

 title=

我的建议是:不要吝啬分配给客体操作系统使用的内存!当客体操作系统的内存不足时,客体系统将开始从随机存取存储器(RAM)向硬盘驱动器进行内存分页,这样会极大地恶化系统的性能和响应能力。如果底层的主机系统开始分页,你很可能不会注意到。对于具有图形化桌面环境的 Linux 工作站系统,我建议至少分配 4GB 内存。

接下来,创建虚拟磁盘:

 title=

虚拟磁盘的格式选择默认的选项 “VDI(VirtualBox 磁盘镜像)” 就可以了:

 title=

在以下的窗口中,我建议选择“ 动态分配 dynamically allocated ”,因为这允许你在之后增加虚拟磁盘的大小。如果你选择了“ 固定大小 fixed size ”,磁盘的速度可能会更快,但你将无法修改虚拟磁盘的大小了:

 title=

建议你使用附带有逻辑卷管理器(LVM)的 Linux 发行版,这样你就可以先创建一个较小的硬盘。如果之后你的客体系统的空间快用完了,你可以按需增加磁盘的大小。

注意:我选择的客体系统为 Fedora,在 Fedora 的官网说明:Fedora 至少需要分配 20GB 的空闲磁盘空间。我强烈建议你遵守该规范。在这里,我选择了 8GB,以便稍后演示如何用命令行增加磁盘空间。如果你是 Linux 新手,或者对命令行没有经验,请依旧选择 20GB。

 title=

创建好硬盘驱动器后,从 VirtualBox 主窗口的列表中选择新创建的虚拟机,然后单击“ 设置 Settings ”。在设置菜单中,点击“ 系统 System ”,然后选择“ 处理器 Processor ”标签。默认情况下,VirtualBox 只向客体系统分配一个 CPU 内核。在现代多核 CPU 计算机上,分配至少两个内核是没有任何问题的,这能显著地加快客体系统的速度:

 title=

设置网络适配器

接下来,要处理的是网络设置。默认情况下, VirtualBox 会创建一个 NAT 连接,这对于大多数情况来说,是没有问题、不用做其他更改的:

 title=

你也可以创建多个网络适配器。以下是网络适配器最常见的类型:

  • NAT:NAT 适配器能自动执行 网络地址转换。从外部看,主机和客体系统使用着相同的 IP 地址。你无法通过网络从主机系统内访问客体系统。(尽管,你也可以通过定义 端口转发,来访问某些服务。)当你的主机系统可以访问互联网时,则你的客体系统也可以访问互联网。NAT 不再需要进一步的配置。

    • 如果你只需要让客体系统接入互联网就可以的话,请选择 “NAT”。
  • 桥接适配器 Bridged adapter :在此配置中,客体系统和主机系统可以共享相同的物理以太网设备。这两个系统都将拥有独立的 IP 地址。从外部看,网络中会有两个独立的系统,它们共享相同的物理以太网适配器。这种设置更灵活,但需要更多的配置。

    • 如果你想要共享客体系统的网络服务的话,请选择 “桥接适配器”。
  • 仅限主机的适配器 Host-only adapter :在此配置中,客体系统只能与主机,或在同一主机上运行的其他客体系统相互通信。主机系统也可以连接到客体系统。但客体系统不能接入互联网或物理网络。

    • 如果你想要获得高安全性,请选择 “仅限主机的适配器”。

分配操作系统镜像

在设置菜单中,点击“ 存储 Storage ”,然后选择虚拟光盘驱动器。单击右侧的 “光盘”图标,然后点击“ 选择一个磁盘文件…… Choose a disk file… ”,然后分配你想要安装的、已下载的 Linux 发行版镜像:

 title=

安装 Linux

现在,就已经配置好了虚拟机。右上角关闭“ 设置 Settings ”菜单,返回主窗口。点击“绿色箭头”(即“开始”按钮)。虚拟机将从虚拟光盘驱动器启动,你将发现你已经进入到 Linux 发行版的安装程序中:

 title=

设置分区

安装程序将在安装过程中要求你提供分区信息。选择“ 自定义 Custom ”:

 title=

注意: 我假设,你创建这一虚拟机的目的是为了测试。此外,你也无需关心客体系统的休眠,因为此功能会由 VirtualBox 来隐式地提供。因此,你可以省略交换分区,以节省主机系统的磁盘空间。请记住,如果你需要的话,你可以稍后自己添加交换分区。在 《Linux 系统交换空间的介绍》 这篇文章中,作者 David Both 进一步解释了如何添加交换分区,并选择交换分区正确的大小。

Fedora 33 及之后更高的版本提供了一个 zram 分区,zram 分区可以用于存放分页和交换、并经过压缩过后的硬盘数据。zram 分区可以按需地调整大小,并且它比硬盘交换分区快得多。

为了简单,我们只添加以下两个 挂载点 Mount Point

 title=

保存更改,接下来我们继续安装。

安装 VirtualBox 增强功能

完成安装后,从硬盘驱动器启动,并登录到虚拟机。现在,你可以安装 VirtualBox 增强功能 VirtualBox Guest Additions ,其中包括特殊的设备驱动程序和系统应用程序,它们能提供以下功能:

  • 共享剪贴板
  • 共享文件夹
  • 更好的性能
  • 可自由扩展的窗口大小

点击顶部菜单栏的“ 设备 Devices ”,然后选择“ 插入增强功能的 CD 镜像…… Insert Guest Additions CD image... ”,来安装 VirtualBox 增强功能:

 title=

在大多数 Linux 发行版上,带有增强功能的 CD 镜像会自动挂载,并且能够在文件管理器中找到。Fedora 会问你是否要运行安装脚本。单击“ 运行 Run ”,并授予该安装进程 root 权限:

 title=

安装完成后,需要重新启动系统。

LVM:扩大磁盘空间

我在之前给 Fedora 虚拟机分配了 8GB 硬盘空间,是一个愚蠢的决定,因为 Fedora 很快就会告警空间不足:

 title=

正如我提到的,Fedora 官网建议安装时分配 20GB 的磁盘空间。因为 8GB 是 Fedora 33 安装启动就需要的最少空间。没有安装其他软件(除了 VirtualBox 增强功能)的一个新安装的系统就几乎占用了整个 8GB 的可用空间。这时候,不要打开 GNOME 软件中心或任何其他可能从互联网下载文件的东西。

幸运的是,我选择了附带有 LVM 的 Fedora,这样我就可以用命令行轻松地修复这个问题。

要增加虚拟机中文件系统的空间,你必须先增加主机系统上分配的虚拟硬盘驱动器。

关闭虚拟机。如果你的主机系统运行的是 Windows,请打开终端,并进入到 C:\Program Files\Oracle\VirtualBox 目录下。使用以下命令,将磁盘大小扩大到 12,000MB:

VBoxManage.exe modifyhd "C:\Users\StephanA\VirtualBox VMs\Fedora_33\Fedora_33.vdi" --resize 12000

然后启动虚拟机,并打开“ 磁盘 Disks ”工具。你可以看到你刚刚新创建且未分配的可用空间。选择“ 可用空间 Free Space ”,然后单击 “+” 按钮:

 title=

现在,创建一个新的分区。选择你要使用的可用空间的大小:

 title=

如果你不想在新分区上创建文件系统或任何其他内容,请选择“ 其他 Other ”:

 title=

选择“ 无文件系统 No Filesystem ”:

 title=

现在,磁盘空间应该如下图所示:

 title=

虚拟机有了一个新的分区设备:/dev/sda3。通过输入 vgscan ,来检查你的 LVM 卷组,找到 fedora_localhost_live 这一 LVM 卷组 :

 title=

现在,已经万事俱备了。在新分区 /dev/sda3 中扩展卷组 fedora_localhost_live

vgextend fedora_localhost-live /dev/sda3

 title=

由于卷组比逻辑卷大,你可以增加逻辑卷的大小。命令 vgdisplay 显示了共有 951 个可用的物理扩展(PE):

 title=

将逻辑卷增加 951 个物理扩展:

lvextend -l+951 /dev/mapper/fedora_localhost--live-root

 title=

在增加了逻辑卷后,最后一件事就是调整文件系统的大小:

resize2fs /dev/mapper/fedora_localhost--live-root

 title=

这样磁盘空间就增加完成了!检查“ 磁盘使用分析器 Disk Usage Analyzer ”,你就可以看到扩展空间已经可用于文件系统了。

总结

使用虚拟机,你可以检查在一个特定的操作系统或一个特定版本的操作系统、软件是如何操作的。除此之外,你还可以尝试任何想测试的 Linux 发行版,而不必担心系统损坏。对于资深用户来说,VirtualBox 在测试、网络和模拟方面提供了广泛的可能性。


via: https://opensource.com/article/21/6/try-linux-virtualbox

作者:Stephan Avenwedde 选题:lujun9972 译者:chai001125 校对:wxy

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

使用开源的 PuTTY 工具,从 Windows 建立到 Linux 的 SSH 连接。

 title=

在 Linux 世界中, 安全外壳 secure shell (SSH)协议是最为常用的、通过命令行控制远程计算机的方式。SSH 是真正的 Linux 原创,但是它在 Windows 世界中也越来越流行。甚至有了一份官方的 Windows 的 SSH 文档,那篇文档阐述了使用 OpenSSH 控制 Windows 的方法。

这篇文章展示了如何使用了流行的开源工具 PuTTY,建立一个从 Windows 到 Fedora 33 Linux 系统的 SSH 连接。

使用 SSH 的方法

SSH 使用客户端-服务器模式,即 SSH 客户端会创建到 SSH 服务端的连接。SSH 服务器通常会作为 守护进程 Daemon 运行,所以它常被称为 SSHD。你很难找到一个不自带 SSH 守护进程的 Linux 发行版。在 Fedora 33 中,已安装了 SSH 守护进程,但是并未激活。

你可以使用 SSH 控制几乎所有的 Linux 机器,无论它是作为虚拟机还是作为网络上的物理设备运行。一个常见的用例是 无头 headless 配置的嵌入式设备,如树莓派。SSH 也可以用做一个其它网络服务的隧道。因为 SSH 连接是加密的,所以你可以使用 SSH 作为一个任何默认不提供加密的协议的传输层。

在这篇文章中,我将解释使用 SSH 的四个方式:1、如何在 Linux 端配置 SSH 守护进程;2、如何设置远程控制台连接;3、如何通过网络复制文件,4. 如何将 SSH 作为某些协议的隧道。

1、配置 SSHD

将 Linux 系统(文中是 Fedora 33)作为 SSH 服务器,允许 PuTTY SSH 客户端进行连接。首先,检查守护进程的 SSH 配置。配置文件放在 /etc/ssh/sshd_config,它包含了许多选项,通过取消掉相关行的注释就可以激活:

#       $OpenBSD: sshd_config,v 1.100 2016/08/15 12:32:04 naddy Exp $

# This is the sshd server system-wide configuration file.  See
# sshd_config(5) for more information.

# This sshd was compiled with PATH=/usr/local/sbin:/usr/sbin:/sbin:/usr/local/bin:/usr/bin:/bin

# The strategy used for options in the default sshd_config shipped with
# OpenSSH is to specify options with their default value where
# possible, but leave them commented.  Uncommented options override the
# default value.

Include /etc/ssh/sshd_config.d/*.conf

#Port 22
#AddressFamily any
#ListenAddress 0.0.0.0
#ListenAddress ::

没有取消任何注释的默认配置在这个示例中应该是可以工作的。要检查 SSH 守护进程是否已经运行,输入 systemctl status sshd

$ systemctl status sshd
● sshd.service - OpenSSH server daemon
   Loaded: loaded (/usr/lib/systemd/system/sshd.service; enabled; vendor preset: enabled)
   Active: active (running) since Fri 2018-06-22 11:12:05 UTC; 2 years 11 months ago
     Docs: man:sshd(8)
           man:sshd_config(5)
 Main PID: 577 (sshd)
    Tasks: 1 (limit: 26213)
   CGroup: /system.slice/sshd.service
           └─577 /usr/sbin/sshd -D -oCiphers=[[email protected]][5],chacha20-[...]

如果它处于 未激活 inactive 状态,使用 systemctl start sshd 命令启动它。

2、设置远程控制台

在 Windows 下 下载 PuTTY 安装程序,然后安装并打开它。你应看到一个像这样的窗口:

 title=

在“ 主机名(或 IP 地址) Host Name (or IP address) ”输入框,键入你的 Linux 系统的连接信息。本文设置了一个 Fedora 33 虚拟机,它使用桥接网络适配器,使我可以由 IP 地址 192.168.1.60 连接这个系统。点击“ 打开 Open ”,应会如图示的打开一个窗口:

 title=

这是 SSH 的安全措施之一,是为了防止 中间人攻击 man-in-the-middle attack 。消息中的指纹应该匹配 Linux 系统中放在 /etc/ssh/ssh_host_ed25519_key.pub 的密钥。PuTTY 将这个密钥以 MD5 哈希值 的方式打印出来。要检查它的真实性,切换到 Linux 系统并打开一个控制台,然后输入:

ssh-keygen -l -E md5 -f /etc/ssh/ssh_host_ed25519_key.pub

输出应该和 PuTTY 展示的指纹一致:

$ ssh-keygen -l -E md5 -f /etc/ssh/ssh_host_ed25519_key.pub
256 MD5:E4:5F:01:05:D0:F7:DC:A6:32 no comment (ED25519)

点击“ Yes ”以确认 PuTTY 的安全提示。主机系统的指纹现在存储在 PuTTY 的信任列表中,其位于 Windows 的注册表中的:

HKEY_CURRENT_USER\SOFTWARE\SimonTatham\PuTTY\SshHostKeys

输入正确的登录凭证,然后你应该进入控制台了,位置在你的用户主目录。

 title=

3、通过网络复制文件

除了远程控制台,你同样可以使用 PuTTY 通过 SSH 来传输文件。PuTTY 的安装目录在 C:\Program Files (x86)\PuTTY,在该目录下寻找 ppscp.exe。你既可以使用它从 Linux 系统复制文件,也可以复制文件到 Linux 系统。

使用 Windows + R 然后输入 cmd 来打开命令提示符,从你的 Linux 用户主目录复制 MYFile.txt 到你的 Windows 主目录,输入:

C:\"Program Files (x86)"\PuTTY\pscp.exe [email protected]:/home/stephan/MyFile.txt .

要从 Windows 主目录复制文件到 Linux 用户主目录,输入:

C:\"Program Files (x86)"\PuTTY\pscp.exe MyFile.txt [email protected]:/home/stephan/

就像你也许已经发现的那样,复制的命令通常构造为:

pscp.exe <source> <target>

4、隧道化一个协议

假设你拥有一个 Linux 机器,为某些特别的应用运行一个基于 HTTP 的服务。你想从你的 Windows 机器通过互联网访问这个 HTTP 服务。而且,你不能将相关的 TCP 端口暴露在公网,因为:

  1. 这个服务通过 HTTP 而非 HTTPS 运行
  2. 根本没有用户管理和登录系统

乍一看,建立这种架构不产生可怕的漏洞似乎是不可能的。但是 SSH 可简单的为这种情况建立一个安全的解决方案。

我将用我的软件项目 Pythonic 来演示这个过程。在容器中运行。Pythonic 作为容器运行,开放两个 TCP 端口:TCP 端口 7000(主要编辑器)和 TCP 端口 8000(code-server 代码编辑器)。

要在一个 Linux 机器上安装 Pythonic ,运行:

podman pull pythonicautomation/pythonic
podman run -d -p 7000:7000 -p 8000:8000 pythonic

转向你的 Windows 机器,打开 PuTTy,转到 “ 连接 Connection -> SSH -> 隧道 Tunnels ”。加入你要转发的两个 TCP 端口:

  • 源:7000 / 目标:localhost:7000
  • 源:8000 / 目标:localhost:8000

 title=

然后返回 “ 会话 Session ” 部分,并像之前那样建立一个 SSH 链接。打开网页浏览器,然后转到 http://localhost:7000;你应该看见像这样的窗口:

 title=

你成功的设置了端口转发!

警告: 如果你选择在公网上暴露 TCP 端口 22 ,不要使用易于猜测的登录凭证。你将接受来自全世界的登录请求,它们使用常见的、标准的登录凭证以尝试登录你的 Linux 机器。相反,只允许已知的用户登录。这种登录限制可以通过 公钥加密 来实现,它使用一个密钥对,其中公钥存储在 SSH 主机上,而私钥保留在客户端。

调试

如果你难以连接你的 Linux 机器,你可以跟踪你的 SSH 守护进程的处理过程:

journalctl -f -u sshd

这是一个普通的登录进程,但是其日志级别为 DEBUG,它看起来是这样的 :

 title=

了解更多

这篇文章几乎没有涉及到使用 SSH 的方法。如果你正在寻找一个特定用例的信息,你也许可以在互联网中找到无数的教程。我在工作中使用 PuTTY ,因为它易于设置,在两个操作系统间又具有良好的可操作性,使得它成为连接解决方案里的瑞士军刀。

(文内图片来自:Stephan Avenwedde,CC BY-SA 4.0


via: https://opensource.com/article/21/6/ssh-windows

作者:Stephan Avenwedde 选题:lujun9972 译者:yjacks 校对:wxy

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

GNU 调试器是一个发现程序缺陷的强大工具。

如果你是一个程序员,想在你的软件增加某些功能,你首先考虑实现它的方法:例如写一个方法、定义一个类,或者创建新的数据类型。然后你用编译器或解释器可以理解的编程语言来实现这个功能。但是,如果你觉得你所有代码都正确,但是编译器或解释器依然无法理解你的指令怎么办?如果软件大多数情况下都运行良好,但是在某些环境下出现缺陷怎么办?这种情况下,你得知道如何正确使用调试器找到问题的根源。

GNU 调试器 GNU Project Debugger GDB)是一个发现项目缺陷的强大工具。它通过追踪程序运行过程中发生了什么来帮助你发现程序错误或崩溃的原因。(LCTT 校注:GDB 全程是“GNU Project Debugger”,即 “GNU 项目调试器”,但是通常我们简称为“GNU 调试器”)

本文是 GDB 基本用法的实践教程。请跟随示例,打开命令行并克隆此仓库:

git clone https://github.com/hANSIc99/core_dump_example.git

快捷方式

GDB 的每条命令都可以缩短。例如:显示设定的断点的 info break 命令可以被缩短为 i break。你可能在其他地方看到过这种缩写,但在本文中,为了清晰展现使用的函数,我将所写出整个命令。

命令行参数

你可以将 GDB 附加到每个可执行文件。进入你克隆的仓库(core_dump_example),运行 make 进行编译。你现在能看到一个名为 coredump 的可执行文件。(更多信息,请参考我的文章《创建和调试 Linux 的转储文件》。)

要将 GDB 附加到这个可执行文件,请输入: gdb coredump

你的输出应如下所示:

gdb coredump output

返回结果显示没有找到调试符号。

调试信息是 目标文件 object file (可执行文件)的组成部分,调试信息包括数据类型、函数签名、源代码和操作码之间的关系。此时,你有两种选择:

  • 继续调试汇编代码(参见下文“无符号调试”)
  • 使用调试信息进行编译,参见下一节内容

使用调试信息进行编译

为了在二进制文件中包含调试信息,你必须重新编译。打开 Makefile,删除第 9 行的注释标签(#)后重新编译:

CFLAGS =-Wall -Werror -std=c++11 -g

-g 告诉编译器包含调试信息。运行 make clean,接着运行 make,然后再次调用 GDB。你得到如下输出后就可以调试代码了:

GDB output with symbols

新增的调试信息会增加可执行文件的大小。在这种情况下,执行文件增加了 2.5 倍(从 26,088 字节 增加到 65,480 字节)。

输入 run -c1,使用 -c1 开关启动程序。当程序运行到达 State_4 时将崩溃:

gdb output crash on c1 switch

你可以检索有关程序的其他信息,info source 命令提供了当前文件的信息:

gdb info source output

  • 101 行代码
  • 语言: C++
  • 编译器(版本、调优、架构、调试标志、语言标准)
  • 调试格式:DWARF 2
  • 没有预处理器宏指令(使用 GCC 编译时,宏仅在 使用 -g3 标志编译 时可用)。

info shared 命令打印了动态库列表机器在虚拟地址空间的地址,它们在启动时被加载到该地址,以便程序运行:

gdb info shared output

如果你想了解 Linux 中的库处理方式,请参见我的文章 在 Linux 中如何处理动态库和静态库

调试程序

你可能已经注意到,你可以在 GDB 中使用 run 命令启动程序。run 命令接受命令行参数,就像从控制台启动程序一样。-c1 开关会导致程序在第 4 阶段崩溃。要从头开始运行程序,你不用退出 GDB,只需再次运行 run 命令。如果没有 -c1 开关,程序将陷入死循环,你必须使用 Ctrl+C 来结束死循环。

gdb output stopped by sigint

你也可以一步一步运行程序。在 C/C++ 中,入口是 main 函数。使用 list main 命令打开显示 main 函数的部分源代码:

gdb output list main

main 函数在第 33 行,因此可以输入 break 33 在 33 行添加断点:

gdb output breakpoint added

输入 run 运行程序。正如预期的那样,程序在 main 函数处停止。输入 layout src 并排查看源代码:

gdb output break at main

你现在处于 GDB 的文本用户界面(TUI)模式。可以使用键盘向上和向下箭头键滚动查看源代码。

GDB 高亮显示当前执行行。你可以输入 nextn)命令逐行执行命令。如果你没有指定新的命令,GBD 会执行上一条命令。要逐行运行代码,只需按回车键。

有时,你会发现文本的输出有点显示不正常:

gdb output corrupted

如果发生这种情况,请按 Ctrl+L 重置屏幕。

使用 Ctrl+X+A 可以随时进入和退出 TUI 模式。你可以在手册中找到 其他的键绑定

要退出 GDB,只需输入 quit

设置监察点

这个示例程序的核心是一个在无限循环中运行的状态机。n_state 变量枚举了当前所有状态:

while(true){
        switch(n_state){
        case State_1:
                std::cout << "State_1 reached" << std::flush;
                n_state = State_2;
                break;
        case State_2:
                std::cout << "State_2 reached" << std::flush;
                n_state = State_3;
                break;
        
        (.....)
        
        }
}

如果你希望当 n_state 的值为 State_5 时停止程序。为此,请在 main 函数处停止程序并为 n_state 设置监察点:

watch n_state == State_5

只有当所需的变量在当前上下文中可用时,使用变量名设置监察点才有效。

当你输入 continue 继续运行程序时,你会得到如下输出:

gdb output stop on watchpoint_1

如果你继续运行程序,当监察点表达式评估为 false 时 GDB 将停止:

gdb output stop on watchpoint_2

你可以为一般的值变化、特定的值、读取或写入时来设置监察点。

更改断点和监察点

输入 info watchpoints 打印先前设置的监察点列表:

gdb output info watchpoints

删除断点和监察点

如你所见,监察点就是数字。要删除特定的监察点,请先输入 delete 后输入监察点的编号。例如,我的监察点编号为 2;要删除此监察点,输入 delete 2

注意: 如果你使用 delete 而没有指定数字,所有 监察点和断点将被删除。

这同样适用于断点。在下面的截屏中,我添加了几个断点,输入 info breakpoint 打印断点列表:

gdb output info breakpoints

要删除单个断点,请先输入 delete 后输入断点的编号。另外一种方式:你可以通过指定断点的行号来删除断点。例如,clear 78 命令将删除第 78 行设置的断点号 7。

禁用或启用断点和监察点

除了删除断点或监察点之外,你可以通过输入 disable,后输入编号禁用断点或监察点。在下文中,断点 3 和 4 被禁用,并在代码窗口中用减号标记:

disabled breakpoints

也可以通过输入类似 disable 2 - 4 修改某个范围内的断点或监察点。如果要重新激活这些点,请输入 enable,然后输入它们的编号。

条件断点

首先,输入 delete 删除所有断点和监察点。你仍然想使程序停在 main 函数处,如果你不想指定行号,可以通过直接指明该函数来添加断点。输入 break main 从而在 main 函数处添加断点。

输入 run 从头开始运行程序,程序将在 main 函数处停止。

main 函数包括变量 n_state_3_count,当状态机达到状态 3 时,该变量会递增。

基于 n_state_3_count 的值添加一个条件断点,请输入:

break 54 if n_state_3_count == 3

Set conditional breakpoint

继续运行程序。程序将在第 54 行停止之前运行状态机 3 次。要查看 n_state_3_count 的值,请输入:

print n_state_3_count

print variable

使断点成为条件断点

你也可以使现有断点成为条件断点。用 clear 54 命令删除最近添加的断点,并通过输入 break 54 命令添加一个简单的断点。你可以输入以下内容使此断点成为条件断点:

condition 3 n_state_3_count == 9

3 指的是断点编号。

modify breakpoint

在其他源文件中设置断点

如果你的程序由多个源文件组成,你可以在行号前指定文件名来设置断点,例如,break main. cpp:54

捕捉点

除了断点和监察点之外,你还可以设置捕获点。捕获点适用于执行系统调用、加载共享库或引发异常等事件。

要捕获用于写入 STDOUT 的 write 系统调用,请输入:

catch syscall write

catch syscall write output

每当程序写入控制台输出时,GDB 将中断执行。

在手册中,你可以找到一整章关于 断点、监察点和捕捉点 的内容。

评估和操作符号

print 命令可以打印变量的值。一般语法是 print <表达式> <值>。修改变量的值,请输入:

set variable <variable-name> <new-value>.

在下面的截屏中,我将变量 n_state_3_count 的值设为 123

catch syscall write output

/x 表达式以十六进制打印值;使用 & 运算符,你可以打印虚拟地址空间内的地址。

如果你不确定某个符号的数据类型,可以使用 whatis 来查明。

whatis output

如果你要列出 main 函数范围内可用的所有变量,请输入 info scope main :

info scope main output

DW_OP_fbreg 值是指基于当前子程序的堆栈偏移量。

或者,如果你已经在一个函数中并且想要列出当前堆栈帧上的所有变量,你可以使用 info locals :

info locals output

查看手册以了解更多 检查符号 的内容。

附加调试到一个正在运行的进程

gdb attach <进程 ID> 命令允许你通过指定进程 ID(PID)附加到一个已经在运行的进程进行调试。幸运的是,coredump 程序将其当前 PID 打印到屏幕上,因此你不必使用 pstop 手动查找 PID。

启动 coredump 应用程序的一个实例:

./coredump

coredump application

操作系统显示 PID 为 2849。打开一个单独的控制台窗口,移动到 coredump 应用程序的根目录,然后用 GDB 附加到该进程进行调试:

gdb attach 2849

attach GDB to coredump

当你用 GDB 附加到进程时,GDB 会立即停止进程运行。输入 layout srcbacktrace 来检查调用堆栈:

layout src and backtrace output

输出显示在 main.cpp 第 92 行调用 std::this_thread::sleep_for<...>(. ..) 函数时进程中断。

只要你退出 GDB,该进程将继续运行。

你可以在 GDB 手册中找到有关 附加调试正在运行的进程 的更多信息。

在堆栈中移动

在命令窗口,输入 up 两次可以在堆栈中向上移动到 main.cpp :

moving up the stack to main.cpp

通常,编译器将为每个函数或方法创建一个子程序。每个子程序都有自己的栈帧,所以在栈帧中向上移动意味着在调用栈中向上移动。

你可以在手册中找到有关 堆栈计算 的更多信息。

指定源文件

当调试一个已经在运行的进程时,GDB 将在当前工作目录中寻找源文件。你也可以使用 目录命令 手动指定源目录。

评估转储文件

阅读 创建和调试 Linux 的转储文件 了解有关此主题的信息。

参考文章太长,简单来说就是:

  1. 假设你使用的是最新版本的 Fedora
  2. 使用 -c1 开关调用 coredump:coredump -c1

Crash meme

  1. 使用 GDB 加载最新的转储文件:coredumpctl debug
  2. 打开 TUI 模式并输入 layout src

coredump output

backtrace 的输出显示崩溃发生在距离 main.cpp 五个栈帧之外。回车直接跳转到 main.cpp 中的错误代码行:

up 5 output

看源码发现程序试图释放一个内存管理函数没有返回的指针。这会导致未定义的行为并引起 SIGABRT

无符号调试

如果没有源代码,调试就会变得非常困难。当我在尝试解决逆向工程的挑战时,我第一次体验到了这一点。了解一些 汇编语言 的知识会很有用。

我们用例子看看它是如何运行的。

找到根目录,打开 Makefile,然后像下面一样编辑第 9 行:

CFLAGS =-Wall -Werror -std=c++11 #-g

要重新编译程序,先运行 make clean,再运行 make,最后启动 GDB。该程序不再有任何调试符号来引导源代码的走向。

no debugging symbols

info file 命令显示二进制文件的内存区域和入口点:

info file output

.text 区段始终从入口点开始,其中包含实际的操作码。要在入口点添加断点,输入 break *0x401110 然后输入 run 开始运行程序:

breakpoint at the entry point

要在某个地址设置断点,使用取消引用运算符 * 来指定地址。

选择反汇编程序风格

在深入研究汇编之前,你可以选择要使用的 汇编风格。 GDB 默认是 AT&T,但我更喜欢 Intel 语法。变更风格如下:

set disassembly-flavor intel

changing assembly flavor

现在输入 layout asm 调出汇编代码窗口,输入 layout reg 调出寄存器窗口。你现在应该看到如下输出:

layout asm and layout reg output

保存配置文件

尽管你已经输入了许多命令,但实际上还没有开始调试。如果你正在大量调试应用程序或尝试解决逆向工程的难题,则将 GDB 特定设置保存在文件中会很有用。

该项目的 GitHub 存储库中的 gdbinit 配置文件包含最近使用的命令:

set disassembly-flavor intel
set write on
break *0x401110
run -c2
layout asm
layout reg

set write on 命令使你能够在程序运行期间修改二进制文件。

退出 GDB 并使用配置文件重新启动 GDB : gdb -x gdbinit coredump

阅读指令

应用 c2 开关后,程序将崩溃。程序在入口函数处停止,因此你必须写入 continue 才能继续运行:

continuing execution after crash

idiv 指令进行整数除法运算:RAX 寄存器中为被除数,指定参数为除数。商被加载到 RAX 寄存器中,余数被加载到 RDX 中。

从寄存器角度,你可以看到 RAX 包含 5,因此你必须找出存储堆栈中位置为 rbp-0x4 的值。

读取内存

要读取原始内存内容,你必须指定比读取符号更多的参数。在汇编输出中向上滚动一点,可以看到堆栈的划分:

stack division output

你最感兴趣的应该是 rbp-0x4 的值,因为它是 idiv 的存储参数。你可以从截图中看到rbp-0x8 位置的下一个变量,所以 rbp-0x4 位置的变量是 4 字节宽。

在 GDB 中,你可以使用 x 命令查看任何内存内容:

x/ < 可选参数 nfu > < 内存地址 addr >

可选参数:

  • n:单元大小的重复计数(默认值:1)
  • f:格式说明符,如 printf
  • u:单元大小

    • b:字节
    • h:半字(2 个字节)
    • w: 字(4 个字节)(默认)
    • g: 双字(8 个字节)

要打印 rbp-0x4 的值,请输入 x/u $rbp-4 :

print value

如果你能记住这种模式,则可以直接查看内存。参见手册中的 查看内存 部分。

操作汇编

子程序 zeroDivide() 发生运算异常。当你用向上箭头键向上滚动一点时,你会找到下面信息:

0x401211 <_Z10zeroDividev>              push   rbp
0x401212 <_Z10zeroDividev+1>            mov    rbp,rsp

这被称为 函数前言

  1. 调用函数的基指针(rbp)存放在栈上
  2. 栈指针(rsp)的值被加载到基指针(rbp

完全跳过这个子程序。你可以使用 backtrace 查看调用堆栈。在 main 函数之前只有一个堆栈帧,所以你可以用一次 up 回到 main :

Callstack assembly

在你的 main 函数中,你会找到下面信息:

0x401431 <main+497>     cmp    BYTE PTR [rbp-0x12],0x0
0x401435 <main+501>     je     0x40145f <main+543>
0x401437 <main+503>     call   0x401211<_Z10zeroDividev>

子程序 zeroDivide() 仅在 jump equal (je)true 时进入。你可以轻松地将其替换为 jump-not-equal (jne) 指令,该指令的操作码为 0x75(假设你使用的是 x86/64 架构;其他架构上的操作码不同)。输入 run 重新启动程序。当程序在入口函数处停止时,设置操作码:

set *(unsigned char*)0x401435 = 0x75

最后,输入 continue 。该程序将跳过子程序 zeroDivide() 并且不会再崩溃。

总结

你会在许多集成开发环境(IDE)中发现 GDB 运行在后台,包括 Qt Creator 和 VSCodium 的 本地调试 扩展。

GDB in VSCodium

了解如何充分利用 GDB 的功能很有用。一般情况下,并非所有 GDB 的功能都可以在 IDE 中使用,因此你可以从命令行使用 GDB 的经验中受益。


via: https://opensource.com/article/21/1/gnu-project-debugger

作者:Stephan Avenwedde 选题:lkxed 译者:Maisie-x 校对:wxy

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