简述Linux的信号处理

news2025/1/7 16:36:33

简述Linux的信号处理

  • 背景
  • 什么是信号?
    • 信号状态
    • 可靠信号与不可靠信号
  • 如何产生信号?
  • 信号处理?
    • 捕捉信号
      • signal函数
      • sigaction函数
    • 阻塞信号
    • 中断的系统调用
    • Async-signal-safe
    • 多线程与信号处理
  • 实战
    • 不可靠信号,多次产生信号信号处理函数会被重复调用吗?
    • 信号屏蔽字对不可靠信号是否产生作用?
    • 解读一下glog的FailureWriter
    • Signal Handler的Tips

背景

工作上有一个需求:希望在程序crash的情况下能够回收内存中的一些数据,将其落到硬盘上,所以研究了一下Signal Handle。

什么是信号?

信号是软件中断,提供了一种处理异步事件的方法,它会中断程序正常执行,然后去执行注册的信号处理函数。例如:终端用户键入中断键,会通过信号机制停止一个程序。在Linux系统下有31种信号(新版可能会有扩展),包括我们熟悉的:SIGINT(Ctrl + C)、SIGSEGV(段错误)、SIGTERM(终止信号)等。

信号状态

  • 信号产生(generation):硬件异常(除0)、软件条件(如alarm定时器超时)、终端产生的信号或调用kill函数
  • 信号递送(delivery):进程可以处理这个信号了
  • 信号未决的(pending):在信号generation和delivery之间的时间间隔内,信号的状态是pending

可靠信号与不可靠信号

可靠信号:

  • 定义:可靠信号又称为实时信号,信号代码从SIGRTMIN到SIGRTMAX之间的信号都是可靠信号。

  • 特性:可靠信号支持排队,即如果发送了多个相同的可靠信号到同一进程,这些信号都会被接收并排队等待处理。内核会为每个接收到的可靠信号分配一个sigqueue结构,并注册在进程的未决信号链中,因此不存在信号丢失的问题。

  • 应用:可靠信号通常用于需要确保信号被准确接收和处理的场景,如实时系统、多线程程序等。

不可靠信号:

  • 定义:不可靠信号又称为非实时信号,信号代码从1到32(如SIGHUP到SIGSYS)都是不可靠信号。
  • 特性:不可靠信号不支持排队,即如果发送了多个相同的不可靠信号到同一进程,这些信号可能会被合并或丢弃,只保留一个信号等待处理。此外,不可靠信号在每次处理完之后,通常会恢复成默认处理,这可能是调用者不希望看到的。
  • 应用:不可靠信号通常用于传统的UNIX系统信号处理,如进程终止(SIGINT)、非法内存访问(SIGSEGV)等。

如何产生信号?

很多条件都可以产生信号:

  1. 当用户按某些终端键时,引发终端产生的信号,比如Ctrl + C产生的SIGINT信号
  2. 硬件异常产生信号:除数为0、无效的内存引用等,这些由硬件检测到,并通知内核。内核为该条件发生时正在运行的进程产生适当的信号,例如:SIGSEGV
  3. 进程调用kill函数可将任意信号发送给另一个进程或进程组,不过一些限制:要么发送和接收是同一个所有者,要么发送进程具备超级用户权限
  4. 用户可用kill命令将信号发送给其他进程,只是对kill函数的封装
  5. 进程调用pthread_kill函数可以向任意一个线程发送信号
  6. 当检测到某种软件条件已经发生,并应将其通知有关进程时也产生信号
  7. raise函数

信号处理?

因为产生信号的事件对进程而言是随机出现的,所以进程不能判断怎么时候信号发生了,只能通过系统调用告诉内核“此信号发生时,请执行下列操作”。在某个信号出现时,可以告诉内核按下列3中方式之一进行处理,称之为Signal Handler:

  1. 忽略此信号,不做任何处理,SIGKILL和SIGSTOP是不可忽略的
  2. 捕捉信号,注册一个signal handler函数来处理信号
  3. 执行系统默认动作,大部分系统默认动作时终止进程,有些信号还会产生core文件

捕捉信号

signal函数

signal 是一个用于设置信号处理方式的函数,它允许程序在接收到特定信号时执行自定义的处理函数,或者采用默认的处理方式,也可以选择忽略该信号。

注意事项:

  • 当信号发生后,第二次发生,信号会恢复到系统默认的处理动作上。(测试了Linux系统发现并不是这样的,所以不同的操作系统实现不一样)
  • 信号处理函数应该尽量简单快速,避免执行复杂的操作或长时间的阻塞操作,因为信号可能在任何时候中断程序的执行。
  • 信号处理可能会被其他信号中断,所以在信号处理函数中要考虑到这种情况。
  • 不同的操作系统对信号的处理可能会有所不同,所以在跨平台开发时需要注意兼容性问题。
  • 一旦设置了信号处理函数,它将在程序的整个生命周期内有效,除非再次调用 signal 函数来改变信号的处理方式。

sigaction函数

sigaction函数的功能是检测或修改(或检查并修改)与指定信号相关联的处理动作。

int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
  • signum:要捕捉的信号的编号。例如,SIGINT 表示中断信号(通常由 Ctrl+C 产生),SIGTERM 表示终止信号等。
  • act:指向一个 struct sigaction 结构体的指针,该结构体包含了要设置的信号处理程序的详细信息。如果此参数为 NULL,则不会更改信号的处理程序,但可以用来获取当前信号的处理程序(通过 oldact 参数)。
  • oldact:指向一个 struct sigaction 结构体的指针,用于存储先前的信号处理程序信息。如果此参数为 NULL,则不保存旧的信号处理程序信息。
struct sigaction {
    void (*sa_handler)(int);           // 信号处理函数
    void (*sa_sigaction)(int, siginfo_t *, void *); // 扩展的信号处理函数
    sigset_t sa_mask;                  // 在处理该信号时要阻塞的其他信号
    int sa_flags;                      // 控制信号处理行为的标志
    void (*sa_restorer)(void);         // 废弃字段(通常不使用)
};
  1. sa_handler:这是一个指向信号处理函数的指针。当某个信号发生时,操作系统会调用这个函数。该函数接受一个 int 类型的参数,表示信号编号(如 SIGINT, SIGTERM 等)。自定义信号处理函数用于处理信号,也可以是特殊值 SIG_DFL(执行该信号的默认处理动作)或 SIG_IGN(忽略该信号)。

  2. sa_sigaction:这是 sa_handler 的一个增强版本,适用于需要获取更详细信号信息的情况。当 sa_flags 中设置了 SA_SIGINFO 标志时,sa_sigaction 会被调用,而不是 sa_handler。它接受三个参数:信号编号、指向 siginfo_t 结构体的指针(提供关于信号的更多详细信息,如信号来源、进程 ID 等)和指向与信号相关的上下文信息的指针(如 CPU 寄存器的状态)。

  3. sa_mask:这是一个 sigset_t 类型的信号集,用于指定在处理当前信号时,应该被阻塞的其他信号。在信号处理程序运行时,sa_mask 中的信号会被暂时阻塞,以防止它们中断当前的信号处理。可以通过 sigemptyset() 清空信号集,或通过 sigaddset() 添加需要阻塞的信号。

  4. sa_flags:这是一组标志位,用于指定信号处理行为。常见的标志位包括:

    1. SA_RESTART:让被信号中断的系统调用自动重启。
    2. SA_SIGINFO:启用 sa_sigaction 处理信号,而非 sa_handler。
    3. SA_NOCLDSTOP:如果信号为 SIGCHLD,当子进程暂停时,不发送此信号。
    4. SA_RESETHAND:当调用信号处理函数时,将信号的处理函数重置为缺省值 SIG_DFL。
    5. SA_NODEFER:在调用信号处理程序时不将本信号添加到进程的信号屏蔽字中。
  5. sa_restorer:这是一个过时的字段,通常不需要设置和使用。它曾经用于指定信号处理函数返回时的清理函数,但现在已经被废弃。

示例:

#include <stdio.h>
#include <signal.h>
#include <unistd.h>

void signal_handler(int signum) {
    printf("Caught signal %d\n", signum);
}

int main() {
    struct sigaction act;
    // 指定处理函数
    act.sa_handler = signal_handler;
    // 清空信号掩码,表示不阻塞任何信号
    sigemptyset(&act.sa_mask);
    // 使用默认标志
    act.sa_flags = 0;
    // 注册 SIGINT 信号的处理程序
    sigaction(SIGINT, &act, NULL);
    // 无限循环,等待信号
    while (1) {
        printf("Waiting for signal...\n");
        sleep(1);
    }
    return 0;
}

阻塞信号

进程可以选用“阻塞信号递送”。如果为进程产生了一个阻塞的信号,而且对该信号的动作是系统默认动作或捕捉该信号,则为该进程将此信号保持为未决状态,直到该进程对此信号解除了阻塞,或者将对此信号的动作更改为忽略。进程可以调用sigpending函数来判断哪些信号是设置为阻塞并处于未决状态。

每个进程都有一个信号屏蔽字(signal mask),它规定了当前要阻塞递送到该进程的信号集。可以使用sigprocmask函数来检测和更改当前的信号屏蔽字。

示例程序:

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <setjmp.h>

// 信号处理函数(实际上在这个例子中不会被调用,因为SIGINT被阻塞了)
void handle_sigint(int signum) {
    printf("Caught SIGINT (signal %d), but this should not happen immediately.\n", signum);
}

// 全局变量用于设置jmpbuf,以便在需要时跳出循环
jmp_buf env;

// 另一个信号处理函数,用于设置全局变量并跳出循环(虽然在这个例子中不被直接用于SIGINT)
void handle_sigterm(int signum) {
    longjmp(env, 1);
}

int main() {
    sigset_t block_set, pending_set;
    struct sigaction act;

    // 设置SIGTERM的处理函数为handle_sigterm,以便我们可以优雅地跳出循环
    act.sa_handler = handle_sigterm;
    sigemptyset(&act.sa_mask);
    act.sa_flags = 0;
    if (sigaction(SIGTERM, &act, NULL) == -1) {
        perror("sigaction SIGTERM");
        exit(EXIT_FAILURE);
    }

    // 初始化jmpbuf,以便在需要时可以跳出循环
    if (setjmp(env) != 0) {
        printf("Received SIGTERM, exiting gracefully.\n");
        exit(EXIT_SUCCESS);
    }

    // 将SIGINT加入到阻塞信号集中
    sigemptyset(&block_set);
    sigaddset(&block_set, SIGINT);
    if (sigprocmask(SIG_BLOCK, &block_set, NULL) == -1) {
        perror("sigprocmask SIGINT");
        exit(EXIT_FAILURE);
    }

    printf("SIGINT is now blocked. Waiting for 5 seconds...\n");
    sleep(5);

    // 检查是否有SIGINT信号在等待(在这个例子中,应该不会有,因为我们还没有解除阻塞)
    sigemptyset(&pending_set);
    if (sigpending(&pending_set) == -1) {
        perror("sigpending");
        exit(EXIT_FAILURE);
    }
    if (sigismember(&pending_set, SIGINT)) {
        printf("SIGINT is pending, but this should not happen because it's blocked.\n");
    } else {
        printf("No SIGINT is pending, as expected.\n");
    }

    // 从阻塞信号集中移除SIGINT
    if (sigprocmask(SIG_UNBLOCK, &block_set, NULL) == -1) {
        perror("sigprocmask SIGINT unblock");
        exit(EXIT_FAILURE);
    }

    printf("SIGINT is now unblocked. You can now interrupt the program with Ctrl+C.\n");

    // 无限循环,等待信号(现在SIGINT可以被捕捉到了)
    while (1) {
        printf("Waiting for signals...\n");
        sleep(1);
    }

    // 注意:由于上面的无限循环,下面的代码实际上永远不会被执行到。
    // 为了测试SIGINT的处理,你可以发送SIGTERM信号来跳出循环(例如,使用kill命令)。

    return 0;
}

中断的系统调用

某些系统调用可以被信号中断,系统返回EINTR的errno码,此时需要根据系统调用返回值再次调用系统调用;有一些系统调用支持自动重启动,但是最好不要依赖它,因为各个系统(UNIX、Linux)实现都不一样,并且也很难确定哪些系统调用实现了自动重启动。

Async-signal-safe

Signal Handler中不是所有函数都可以被调用:假设程序正在执行malloc,此时由于捕捉到信号而插入执行该信号处理函数,其中有调用了malloc,这时可能破坏堆内存的维护链表。

Single UNIX Specifications说明了哪些函数可以被信号处理函数调用,这些函数是可重入的并被成为异步信号安全的(async-signal safe)。

多线程与信号处理

参考:https://cloud.tencent.com/developer/news/1260924

关键点:

  1. 每个线程都可以处理信号,操作系统会优先将信号递送给引发信号的线程,所以类似glog的FailureWriter才可以输出crash的backtrace
  2. 每个线程都有自己的阻塞信号集,控制自己响应哪些信号或阻塞哪些信号,API是phtread_sigmask
  3. 每个线程都有自己的未决信号队列,也有共享的未决信号队列(主线程)

实战

不可靠信号,多次产生信号信号处理函数会被重复调用吗?

    #include <iostream>
    #include <csignal>
    #include <cstdlib>
    #include <unistd.h>

    void HandleSIGINT(int signum) {
      std::cout << "\n捕获到SIGINT信号,程序即将退出..." << std::endl;
    }

    int main() {
     signal(SIGINT, HandleSIGINT);
      while (1) {
        sleep(1);
      }
    }

运行结果:从运行结果来看,即使signal函数也是支持反复处理信号的,和UNIX的设计还是不一样的。

yunjingguang@walle:~/work/signal$ ./signal_test
^C
捕获到SIGINT信号,程序即将退出...
^C
捕获到SIGINT信号,程序即将退出...
^C
捕获到SIGINT信号,程序即将退出...
^C

信号屏蔽字对不可靠信号是否产生作用?

    #include <iostream>
    #include <csignal>
    #include <cstdlib>
    #include <unistd.h>

    void HandleSIGINT(int signum) {
      std::cout << "\n捕获到SIGINT信号,程序即将退出..." << std::endl;
    }

    int main() {
     signal(SIGINT, HandleSIGINT);
     sigset_t block_set;
     sigemptyset(&block_set);
     sigaddset(&block_set, SIGINT);
     if (sigprocmask(SIG_BLOCK, &block_set, NULL) == -1) {
         perror("sigprocmask SIGINT");
         exit(EXIT_FAILURE);
     }

      while (1) {
        sleep(1);
      }
    }

运行结果:看起来已经将SIGINT屏蔽掉了

yunjingguang@walle:~/work/signal$ ./signal_test
^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C

解读一下glog的FailureWriter

注册信号处理函数,并且sa_flags是SA_SIGINFO,会进一步输出细节信息:

void InstallFailureSignalHandler() {
#ifdef HAVE_SIGACTION
  // Build the sigaction struct.
  struct sigaction sig_action;
  memset(&sig_action, 0, sizeof(sig_action));
  sigemptyset(&sig_action.sa_mask);
  sig_action.sa_flags |= SA_SIGINFO;
  sig_action.sa_sigaction = &FailureSignalHandler;

  for (auto kFailureSignal : kFailureSignals) {
    CHECK_ERR(sigaction(kFailureSignal.number, &sig_action, nullptr));
  }
  kFailureSignalHandlerInstalled = true;
#elif defined(GLOG_OS_WINDOWS)
  for (size_t i = 0; i < ARRAYSIZE(kFailureSignals); ++i) {
    CHECK_NE(signal(kFailureSignals[i].number, &FailureSignalHandler), SIG_ERR);
  }
  kFailureSignalHandlerInstalled = true;
#endif  // HAVE_SIGACTION
}

// Dumps signal and stack frame information, and invokes the default
// signal handler once our job is done.
#if defined(GLOG_OS_WINDOWS)
void FailureSignalHandler(int signal_number)
#else
void FailureSignalHandler(int signal_number, siginfo_t* signal_info,
                          void* ucontext)
#endif
{
  std::call_once(signaled, &HandleSignal, signal_number
#if !defined(GLOG_OS_WINDOWS)
                 ,
                 signal_info, ucontext
#endif
  );
}

Signal Handler的Tips

  1. callback使用C语言的函数指针,保证生命周期的安全性
  2. std::once_flag,解决重入的问题
  3. sem_post是async-signal-safe的,可以在Signal Handler中调用,用于通知其他线程开始收尾
  4. 在信号处理函数中获取pthread id,获得是发生问题的线程的ID,它会中断

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2271968.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

计算机网络--路由器问题

一、路由器问题 1.计算下一跳 计算机网络--根据IP地址和路由表计算下一跳-CSDN博客 2.更新路由表 计算机网络--路由表的更新-CSDN博客 3.根据题目要求给出路由表 4.路由器收到某个分组&#xff0c;解释这个分组是如何被转发的 5.转发分组之路由器的选择 二、举个例子 …

Python安装(新手详细版)

前言 第一次接触Python&#xff0c;可能是爬虫或者是信息AI开发的小朋友&#xff0c;都说Python 语言简单&#xff0c;那么多学一些总是有好处的&#xff0c;下面从一个完全不懂的Python 的小白来安装Python 等一系列工作的记录&#xff0c;并且遇到的问题也会写出&#xff0c…

2025 年前端新技术如何塑造未来开发生态?

开发领域&#xff1a;前端开发 | AI 应用 | Web3D | 元宇宙 技术栈&#xff1a;JavaScript、React、ThreeJs、WebGL、Go 经验经验&#xff1a;6 年 前端开发经验&#xff0c;专注于图形渲染和 AI 技术 开源项目&#xff1a;AI智简未来、晓智元宇宙、数字孪生引擎 大家好&#x…

1-markdown转网页样式页面 --[制作网页模板] 【测试代码下载】

markdown转网页 将Markdown转换为带有样式的网页页面通常涉及以下几个步骤&#xff1a;首先&#xff0c;需要使用Markdown解析器将Markdown文本转换为HTML&#xff1b;其次&#xff0c;应用CSS样式来美化HTML内容。此外&#xff0c;还可以加入JavaScript以增加交互性。下面我将…

数据逻辑(十)——逻辑函数的两种标准形式

目录 1 最小项和最大项 1.1 最小项 1.2 最大项 2 逻辑函数的最小项之和 3 逻辑函数的最大项之积 4 最小项之和以及最大项之积的联系和应用场景 4.1 最小项之和以及最大项目之积的联系 4.2 最小项之和以及最大项之积的应用场景 逻辑函数的两种标准形式分别是以最小项之和…

【Ubuntu使用技巧】Ubuntu22.04无人值守Crontab工具实战详解

一个愿意伫立在巨人肩膀上的农民...... Crontab是Linux和类Unix操作系统下的一个任务调度工具&#xff0c;用于周期性地执行指定的任务或命令。Crontab允许用户创建和管理计划任务&#xff0c;以便在特定的时间间隔或时间点自动运行命令或脚本。这些任务可以按照分钟、小时、日…

鸿蒙Flutter实战:15-Flutter引擎Impeller鸿蒙化、性能优化与未来

Flutter 技术原理 Flutter 是一个主流的跨平台应用开发框架&#xff0c;基于 Dart 语言开发 UI 界面&#xff0c;它将描述界面的 Dart 代码直接编译成机器码&#xff0c;并使用渲染引擎调用 GPU/CPU 渲染。 渲染引擎的优势 使用自己的渲染引擎&#xff0c;这也是 Flutter 与其…

UniApp | 从入门到精通:开启全平台开发的大门

UniApp | 从入门到精通:开启全平台开发的大门 一、前言二、Uniapp 基础入门2.1 什么是 Uniapp2.2 开发环境搭建三、Uniapp 核心语法与组件3.1 模板语法3.2 组件使用四、页面路由与导航4.1 路由配置4.2 导航方法五、数据请求与处理5.1 发起请求5.2 数据缓存六、样式与布局6.1 样…

法拉利F80发布 360万欧元限量799辆 25年Q4交付

今日&#xff0c;法拉利旗下全新超级跑车——F80正式发布&#xff0c;新车将作为法拉利GTO和法拉利LaFerrari&#xff08;参数丨图片&#xff09; Aterta的继任者&#xff0c;搭载V6混合动力系统&#xff0c;最大综合输出功率高达1632马力。售价360万欧元&#xff0c;全球限量生…

【pytorch练习】使用pytorch神经网络架构拟合余弦曲线

在本篇博客中&#xff0c;我们将通过一个简单的例子&#xff0c;讲解如何使用 PyTorch 实现一个神经网络模型来拟合余弦函数。本文将详细分析每个步骤&#xff0c;从数据准备到模型的训练与评估&#xff0c;帮助大家更好地理解如何使用 PyTorch 进行模型构建和训练。 一、背景 …

电脑steam api dll缺失了怎么办?

电脑故障解析与自救指南&#xff1a;Steam API DLL缺失问题的全面解析 在软件开发与电脑维护的广阔天地里&#xff0c;我们时常会遇到各种各样的系统报错与文件问题&#xff0c;其中“Steam API DLL缺失”便是让不少游戏爱好者和游戏开发者头疼的难题之一。作为一名深耕软件开…

Conda 安装 Jupyter Notebook

文章目录 1. 安装 Conda下载与安装步骤&#xff1a; 2. 创建虚拟环境3. 安装 Jupyter Notebook4. 启动 Jupyter Notebook5. 安装扩展功能&#xff08;可选&#xff09;6. 更新与维护7. 总结 Jupyter Notebook 是一款非常流行的交互式开发工具&#xff0c;尤其适合数据科学、机器…

组合的能力

在《德鲁克最后的忠告》一书中&#xff0c;有这样一段话&#xff1a; 企业将由各种积木组建而成&#xff1a;人员、产品、理念和建筑。积木的设计组合至少和其供给一样重要。……对于一切程序、应用软件以及附件来说&#xff0c;重要的是掌握将已有的软件模块组合的能力&…

去掉el-table中自带的边框线

1.问题:el-table中自带的边框线 2.解决后的效果: 3.分析:明明在el-table中没有添加border,但是会出现边框线. 可能的原因: 由 Element UI 的默认样式或者表格的某些内置样式引起的。比如,<el-table> 会通过 border-collapse 或 border-spacing 等属性影响边框的显示。 4…

大模型与EDA工具

EDA工具&#xff0c;目标是硬件设计&#xff0c;而硬件设计&#xff0c;您也可以看成是一个编程过程。 大模型可以辅助软件编程&#xff0c;相信很多人都体验过了。但大都是针对高级语言的软件编程&#xff0c;比如&#xff1a;C&#xff0c;Java&#xff0c;Python&#xff0c…

【HarmonyOS之旅】基于ArkTS开发(一) -> Ability开发一

目录 1 -> FA模型综述 1.1 -> 整体架构 1.2 -> 应用包结构 1.3 -> 生命周期 1.4 -> 进程线程模型 2 -> PageAbility开发 2.1 -> 概述 2.1.1 ->功能简介 2.1.2 -> PageAbility的生命周期 2.1.3 -> 启动模式 2.2 -> featureAbility接…

BART:用于自然语言生成、翻译和理解的去噪序列到序列预训练

摘要&#xff1a; 我们提出了BART&#xff0c;一种用于预训练序列到序列模型的去噪自编码器。BART通过以下方式训练&#xff1a;(1) 使用任意的噪声函数对文本进行破坏&#xff0c;(2) 学习一个模型来重建原始文本。它采用了一种标准的基于Transformer的神经机器翻译架构&#…

Promise编码小挑战

题目 我们将实现一个 createImage 函数&#xff0c;该函数返回一个 Promise&#xff0c;用于处理图片加载的异步操作。此外&#xff0c;还会实现暂停执行的 wait 函数。 Part 1: createImage 函数 该函数会&#xff1a; 创建一个新的图片元素。将图片的 src 设置为提供的路径…

Dubbo扩展点加载机制

加载机制中已经存在的一些关键注解&#xff0c;如SPI、©Adaptive> ©Activateo然后介绍整个加载机制中最核心的ExtensionLoader的工作流程及实现原理。最后介绍扩展中使用的类动态编译的实 现原理。 Java SPI Java 5 中的服务提供商https://docs.oracle.com/jav…

【Web】软件系统安全赛CachedVisitor——记一次二开工具的经历

明天开始考试周&#xff0c;百无聊赖开了一把CTF&#xff0c;还顺带体验了下二开工具&#xff0c;让无聊的Z3很开心&#x1f642; CachedVisitor这题 大概描述一下&#xff1a;从main.lua加载一段visit.script中被##LUA_START##(.-)##LUA_END##包裹的lua代码 main.lua loca…