【KLEE】源码阅读笔记----KLEE执行流程

本文架构

  • 1. 动机
  • 2.KLEE简介
  • 3.KLEE的代码工程结构
  • 4. 从KLEE主函数入手
    • main函数
      • step1: 初始化
      • step2:加载.bc文件
      • 进行符号执行
    • 读取测试用例
    • 输出日志信息

1. 动机

最近准备对KLEE进行修改使其符合我的需要,因此免不了需要对源码进行修改。读懂源码是对在其进行修改之前的必经之路。但其工程量巨大,该如何下手呢?

于是我将阅读源码过程中所得分享出来,希望能为同样学习KLEE的同行之人提供一些参考。内容可能写的存在纰漏,还请大家及时指出,不吝赐教。
程序分析-klee工具分析
符号执行, KLEE 与 LLVM

2.KLEE简介

KLEE(可读作“克利”)是一个基于符号执行的自动化测试工具,旨在帮助发现软件中的错误和漏洞。该工具可以用于分析 C/C++ 程序,并生成能够触发程序错误路径的测试用例。KLEE的主要目标是执行程序的所有可能路径,而不仅仅是具体输入下的一条路径。通过符号执行技术,它能够在未提供具体输入的情况下模拟程序执行的各种路径,从而发现隐藏在程序中的潜在漏洞。

klee-log

KLEE的核心思想是将程序中的输入视为符号(symbol),而不是具体的数值。这意味着在执行过程中,KLEE不会给定实际输入值,而是用符号代替输入,从而创建了一种执行路径的符号表示。然后,KLEE使用约束求解器来解决程序中各种条件语句的约束,以确定是否存在输入,能够导致程序执行到不同的路径上。

KLEE的工作流程可以分为几个主要步骤:

  • 符号执行: KLEE通过对程序进行符号执行,以符号形式代表程序的输入和执行路径。在执行过程中,它记录了执行路径上的每个条件分支以及相应的约束条件。
  • 路径探索: KLEE使用路径探索技术来导航程序的不同执行路径。它会尝试通过不同的路径执行程序,并在执行过程中收集约束条件。
  • 约束求解: 在执行过程中,KLEE将收集到的约束条件传递给约束求解器,以确定是否存在一组输入能够满足这些约束条件。如果找到了满足条件的输入,那么就找到了一条可以导致特定程序路径执行的输入序列。
  • 测试用例生成: 当约束求解器找到满足条件的输入时,KLEE会生成相应的测试用例,并将其用于进一步的测试和验证。

KLEE的强大之处在于它能够自动化地发现程序中的潜在错误,例如内存泄漏、空指针解引用、整数溢出等。通过覆盖程序的多个执行路径,KLEE可以提高测试覆盖率,从而增加程序的健壮性和可靠性。

3.KLEE的代码工程结构

从【Github】KLEE: Symbolic Execution Engine下载KLEE的源代码。

git clone https://github.com/klee/klee.git

使用你电脑中的IDE(本文使用的是jetbrain公司的CLion)打开后可以看到其工程结构如下所示:

klee-archi
以弄懂其大致流程为目标导向,我们只需要关注其includelib文件夹。
其中:

  • include文件夹包含了KLEE的所有接口信息,你可以在该文件下获取大部分KLEE的数据结构信息。
  • lib文件夹包含了KLEE核心内容的具体实现,即我们需要阅读的大部分源码。将该文件夹展开还能进一步获取信息:
    lib-archi
文件夹名存放内容说明
lib/Basic Low level support for both klee and kleaver which should be independent of LLVM.
lib/SupportHigher level support, but only used by klee. This can use LLVM facilities.
lib/ExprThe core kleaver expression library.
lib/SolverThe kleaver solver library.
lib/Moduleklee facilities for working with LLVM modules, including the shadow module/instruction structures we use during execution.
lib/CoreThe core symbolic virtual machine.

其中Core是我们最关心的内容,里面包含了对符号执行模拟器(Executor)、符号执行树(ExecutionTree)以及符号执行状态(ExecutionState)等关键概念的定义(.h)和实现(.cpp)。

|-- AddressSpace.cpp
|-- AddressSpace.h
|-- CallPathManager.cpp
|-- CallPathManager.h
|-- Context.cpp
|-- Context.h
|-- CoreStats.cpp
|-- CoreStats.h
|-- ExecutionState.cpp
|-- ExecutionState.h
|-- Executor.cpp
|-- Executor.h
|-- ExecutorUtil.cpp
|-- ExternalDispatcher.cpp
|-- ExternalDispatcher.h
|-- GetElementPtrTypeIterator.h
|-- ImpliedValue.cpp
|-- ImpliedValue.h
|-- Memory.cpp
|-- Memory.h
|-- MemoryManager.cpp
|-- MemoryManager.h
|-- MergeHandler.cpp
|-- MergeHandler.h
|-- PTree.cpp
|-- PTree.h
|-- Searcher.cpp
|-- Searcher.h
|-- SeedInfo.cpp
|-- SeedInfo.h
|-- SpecialFunctionHandler.cpp
|-- SpecialFunctionHandler.h
|-- StatsTracker.cpp
|-- StatsTracker.h
|-- TimingSolver.cpp
|-- TimingSolver.h
|-- UserSearcher.cpp
|-- UserSearcher.h

但大家也可以看见,内容过多,虽然去除头文件但依然还有十几个文件。回想当初学习C语言时,面对繁多的函数定义,我们要了解一个程序的执行流程(基本执行思路)时,都是从主函数入手的。这给我们提供了一个有益的思路:从主函数开始我们的阅读!
当然,也有的文章直接从core下的文件入手,这里给供大家参考吧~
【安全客Blog】KLEE 源码阅读笔记

4. 从KLEE主函数入手

KLEE的主函数并不在我们上述的重要工程目录中。它位于tool/klee/main.cpp中我们点开这个文件后可以发现:(这TM也有近两千行代码~)。一点开Structure,五花八门的函数定义和结构体定义。

在这里插入图片描述
但!没关系,我们关注的只有main函数!(大概位于1200行左右,由于我添加了注释所以跟原始的可能有偏差)

main函数

让我们看看主函数主要做了什么。

step1: 初始化

  • step1: 初始化加载各种初始环境
  • step2: 调用parseArguments()分析你在命令行中对KLEE做出的配置。
  • step3: 设置异常处理函数SetInterruptFunction()
 KCommandLine::KeepOnlyCategories({&ChecksCat,   ... &ExecTreeCat});llvm::InitializeNativeTarget();//初始化环境parseArguments(argc, argv);//分析传入的命令行参数
..//设置异常处理函数// 会报一些错sys::SetInterruptFunction(interrupt_handle);

step2:加载.bc文件

现在开始加载.bc字节码文件进行符号分析

int main(int argc, char **argv, char **envp) // Load the bytecode...// 加载字节码std::string errorMsg;LLVMContext ctx;
...std::vector<std::unique_ptr<llvm::Module>> loadedModules;/*** loadFile 需要解析bc文件,bc文件是一个整体运行单元*/if (!klee::loadFile(InputFile, ctx, loadedModules, errorMsg)) {klee_error("error loading program '%s': %s", InputFile.c_str(),errorMsg.c_str());}// linkModule 将bc文件中的module合并为一个整体的modulestd::unique_ptr<llvm::Module> M(klee::linkModules(loadedModules,"" /* link all modules together */,errorMsg));if (!M) {//链接错误的话,报错klee_error("error loading program '%s': %s", InputFile.c_str(),errorMsg.c_str());}//链接完成,返回mainModulellvm::Module *mainModule = M.get();
...// 将这个module作为第一个项// Push the module as the first entryloadedModules.emplace_back(std::move(M));std::string LibraryDir = KleeHandler::getRunTimeLibraryPath(argv[0]);//配置module基本信息Interpreter::ModuleOptions Opts(LibraryDir.c_str(), EntryPoint, opt_suffix,/*Optimize=*/OptimizeModule,/*CheckDivZero=*/CheckDivZero,/*CheckOvershift=*/CheckOvershift);// 遍历已经加载完毕的modules,以找到主函数mainfor (auto &module : loadedModules) {mainFn = module->getFunction("main");if (mainFn)break;}// 找到入口点if (EntryPoint.empty())klee_error("entry-point cannot be empty");// 找到入口函数for (auto &module : loadedModules) {entryFn = module->getFunction(EntryPoint);if (entryFn)break;}if (!entryFn)klee_error("Entry function '%s' not found in module.", EntryPoint.c_str());//如果设置了POSIX运行时环境if (WithPOSIXRuntime) {...}// 如果设置了UBSan运行时if (WithUBSanRuntime) {...}// 如果设置了libcxxif (Libcxx) {...}
// genswitch (Libc) {..}// 检查是否成功加载字节码库for (const auto &library : LinkLibraries) {if (!klee::loadFile(library, mainModule->getContext(), loadedModules,errorMsg))klee_error("error loading bitcode library '%s': %s", library.c_str(),errorMsg.c_str());}int pArgc;char **pArgv;char **pEnvp;//如果设置了Environ 环境if (Environ != "") {std::vector<std::string> items;// 打开环境配置信息std::ifstream f(Environ.c_str());if (!f.good())klee_error("unable to open --environ file: %s", Environ.c_str());//读取环境配置文件while (!f.eof()) {std::string line;std::getline(f, line);line = strip(line);if (!line.empty())items.push_back(line);}//end of the while(!f.eif())f.close();//关闭文件pEnvp = new char *[items.size()+1];unsigned i=0;for (; i != items.size(); ++i)pEnvp[i] = strdup(items[i].c_str());pEnvp[i] = 0;} else {pEnvp = envp;}pArgc = InputArgv.size() + 1;pArgv = new char *[pArgc];for (unsigned i=0; i<InputArgv.size()+1; i++) {std::string &arg = (i==0 ? InputFile : InputArgv[i-1]);unsigned size = arg.size() + 1;char *pArg = new char[size];std::copy(arg.begin(), arg.end(), pArg);pArg[size - 1] = 0;pArgv[i] = pArg;}//end of for

进行符号执行

Interpreter是进行符号执行的重要部件

  Interpreter::InterpreterOptions IOpts;IOpts.MakeConcreteSymbolic = MakeConcreteSymbolic;KleeHandler *handler = new KleeHandler(pArgc, pArgv);Interpreter *interpreter =theInterpreter = Interpreter::create(ctx, IOpts, handler);// 条件为假则终止程序继续执行assert(interpreter);handler->setInterpreter(interpreter);//输出详细信息(info)for (int i = 0; i < argc; i++)//逐个输出你刚才设置的命令行参数handler->getInfoStream() << argv[i] << (i + 1 < argc ? " " : "\n");handler->getInfoStream() << "PID: " << getpid() << "\n";// 最终的module// Get the desired main function.  klee_main initializes uClibc// locale and other data and then calls main.auto finalModule = interpreter->setModule(loadedModules, Opts);entryFn = finalModule->getFunction(EntryPoint);if (!entryFn)klee_error("Entry function '%s' not found in module.", EntryPoint.c_str());externalsAndGlobalsCheck(finalModule);//重放路径std::vector<bool> replayPath;if (!ReplayPathFile.empty()) {//加载重放路径文件KleeHandler::loadPathFile(ReplayPathFile, replayPath);interpreter->setReplayPath(&replayPath);}
// 这一部分也是打印到日志info里面// 开始时间auto startTime = std::time(nullptr);{ // output clock info and start timestd::stringstream startInfo;startInfo << time::getClockInfo()<< "Started: "<< std::put_time(std::localtime(&startTime), "%Y-%m-%d %H:%M:%S") << '\n';handler->getInfoStream() << startInfo.str();handler->getInfoStream().flush();// 输出到info文件中}

读取测试用例

  // 读取用例文件if (!ReplayKTestDir.empty() || !ReplayKTestFile.empty()) {assert(SeedOutFile.empty());assert(SeedOutDir.empty());std::vector<std::string> kTestFiles = ReplayKTestFile;for (std::vector<std::string>::iteratorit = ReplayKTestDir.begin(), ie = ReplayKTestDir.end();it != ie; ++it)KleeHandler::getKTestFilesInDir(*it, kTestFiles);std::vector<KTest*> kTests;for (std::vector<std::string>::iteratorit = kTestFiles.begin(), ie = kTestFiles.end();it != ie; ++it) {KTest *out = kTest_fromFile(it->c_str());if (out) {kTests.push_back(out);} else {klee_warning("unable to open: %s\n", (*it).c_str());}}if (RunInDir != "") {int res = chdir(RunInDir.c_str());if (res < 0) {klee_error("Unable to change directory to: %s - %s", RunInDir.c_str(),sys::StrError(errno).c_str());}}unsigned i=0;for (std::vector<KTest*>::iteratorit = kTests.begin(), ie = kTests.end();it != ie; ++it) {KTest *out = *it;interpreter->setReplayKTest(out);llvm::errs() << "KLEE: replaying: " << *it << " (" << kTest_numBytes(out)<< " bytes)"<< " (" << ++i << "/" << kTestFiles.size() << ")\n";// XXX should put envp in .ktest ?interpreter->runFunctionAsMain(entryFn, out->numArgs, out->args, pEnvp);if (interrupted) break;}//end of forinterpreter->setReplayKTest(0);//当测试用例不为空while (!kTests.empty()) {kTest_free(kTests.back());kTests.pop_back();}}//当replay路径为空时,从SeedOutFile中读取用例生成种子else {std::vector<KTest *> seeds;for (std::vector<std::string>::iteratorit = SeedOutFile.begin(), ie = SeedOutFile.end();it != ie; ++it) {KTest *out = kTest_fromFile(it->c_str());if (!out) {klee_error("unable to open: %s\n", (*it).c_str());}seeds.push_back(out);}//输入测试用例for (std::vector<std::string>::iteratorit = SeedOutDir.begin(), ie = SeedOutDir.end();it != ie; ++it) {std::vector<std::string> kTestFiles;KleeHandler::getKTestFilesInDir(*it, kTestFiles);for (std::vector<std::string>::iteratorit2 = kTestFiles.begin(), ie = kTestFiles.end();it2 != ie; ++it2) {//从文件中读取用例KTest *out = kTest_fromFile(it2->c_str());if (!out) {klee_error("unable to open: %s\n", (*it2).c_str());}// 将out加入用例队列seeds.push_back(out);}// kTest是一种记录程序执行路径的文件格式,文件包含了程序// 执行结束时的状态信息,如寄存器值、内存的内容等if (kTestFiles.empty()) {klee_error("seeds directory is empty: %s\n", (*it).c_str());}}// 如果存在测试用例,开始使用其进行测试if (!seeds.empty()) {klee_message("KLEE: using %lu seeds\n", seeds.size());interpreter->useSeeds(&seeds);}// end of ifif (RunInDir != "") {int res = chdir(RunInDir.c_str());if (res < 0) {klee_error("Unable to change directory to: %s - %s",RunInDir.c_str(),sys::StrError(errno).c_str());}}// end of ifinterpreter->runFunctionAsMain(entryFn, pArgc, pArgv, pEnvp);
// 释放种子while (!seeds.empty()) {// 释放最后一个队列中的最后一个用例kTest_free(seeds.back());seeds.pop_back();}}

输出日志信息

  // 结束时间,计算测试执行时间。输出日志信息auto endTime = std::time(nullptr);{ // output end and elapsed timestd::uint32_t h;std::uint8_t m, s;std::tie(h,m,s) = time::seconds(endTime - startTime).toHMS();std::stringstream endInfo;endInfo << "Finished: "<< std::put_time(std::localtime(&endTime), "%Y-%m-%d %H:%M:%S") << '\n'<< "Elapsed: "<< std::setfill('0') << std::setw(2) << h<< ':'<< std::setfill('0') << std::setw(2) << +m<< ':'<< std::setfill('0') << std::setw(2) << +s<< '\n';handler->getInfoStream() << endInfo.str();handler->getInfoStream().flush();}// 释放所有参数// Free all the args.for (unsigned i=0; i<InputArgv.size()+1; i++)//释放数组delete[] pArgv[i];delete[] pArgv;delete interpreter;// 获取统计信息uint64_t queries =*theStatisticManager->getStatisticByName("SolverQueries");
...uint64_t forks =*theStatisticManager->getStatisticByName("Forks");handler->getInfoStream()<< "KLEE: done: explored paths = " << 1 + forks << "\n";// Write some extra information in the info file which users won't// necessarily care about or understand.?// 查询的相关信息,这的结果体现在info文件中if (queries)handler->getInfoStream()<< "KLEE: done: avg. constructs per query = "<< queryConstructs / queries << "\n";handler->getInfoStream()<< "KLEE: done: total queries = " << queries << "\n"<< "KLEE: done: valid queries = " << queriesValid << "\n"<< "KLEE: done: invalid queries = " << queriesInvalid << "\n"<< "KLEE: done: query cex = " << queryCounterexamples << "\n";// 一些统计的信息std::stringstream stats;stats << '\n'<< "KLEE: done: total instructions = " << instructions << '\n'<< "KLEE: done: completed paths = " << handler->getNumPathsCompleted()<< '\n'<< "KLEE: done: partially completed paths = "<< handler->getNumPathsExplored() - handler->getNumPathsCompleted()<< '\n'<< "KLEE: done: generated tests = " << handler->getNumTestCases()<< '\n';return 0;
}

这一部分也就对应我们测试后生成的info文件中的内容
info

几个月前学习时写的草稿,若有错误和不足,还望不吝赐教。

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

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

相关文章

CS 144 check7: putting it all together

Exercises 经验&#xff1a;两边的TCP连接建立得尽快&#xff0c;如果服务器端启动了&#xff0c;客户端没有紧接着启动就连不上。 这是什么神奇的bug呢&#xff1f; 和我之前给域控刷SOC的版本一样。如果域控启动了&#xff0c;在我本地的电脑没有马上和域控的SOC通上信&…

Suno Api V4模型无水印开发「综合实战开发自己的音乐网站」 —— 「Suno Api系列」第14篇

历史文章 Suno AI API接入 - 将AI音乐接入到自己的产品中&#xff0c;支持120并发任务 Suno Api V4模型无水印开发「灵感模式」 —— 「Suno Api系列」第1篇 Suno Api V4模型无水印开发「自定义模式」 —— 「Suno Api系列」第2篇 Suno Api V4模型无水印开发「AI生成歌词」…

【物联网技术与应用】实验15:电位器传感器实验

实验15 电位器传感器实验 【实验介绍】 电位器可以帮助控制Arduino板上的LED闪烁的时间间隔。 【实验组件】 ● Arduino Uno主板* 1 ● 电位器模块* 1 ● USB电缆*1 ● 面包板* 1 ● 9V方型电池* 1 ● 跳线若干 【实验原理】 模拟电位器是模拟电子元件&#xff0c;模…

【YOLO】(基础篇一)YOLO介绍

YOLO YOLO&#xff08;You Only Look Once&#xff09;是一种用于实时物体检测的算法&#xff0c;由Joseph Redmon等人提出。它能够同时进行物体分类和定位&#xff0c;并且因其速度和效率而广受赞誉。 工作原理 假设我们要对这张猫的图片完成目标检测&#xff0c;需要框选出…

Linux-----进程处理(文件IO资源使用)

下面代码是通过父进程和子进程对同一个文件IO资源进行操作&#xff0c;父进程和子进程都对这个进程进行写入操作&#xff0c;我们都知道这两个进程实际上是并发的&#xff0c;所以需要一个同步机制来去操作同一个资源&#xff08;后面再深入去说明同步的api&#xff0c;这里使用…

golang标准库SSH操作示例

文章目录 前言一、了解SSH二、重要知识点1.安装ssh库2.ssh库重要知识牢记 三、模拟连接远程服务器并执行命令四、SSH与os/exec标准库下执行命令的几种方式对比五、SSH库下三种执行命令方式演示5.1. session.CombinedOutput()示例5.2. session.Run()示例5.3. session.Start()、s…

替代传统FTP传输,镭速大数据传输系统实现安全高效数据流转!

信息技术的快速进步让大数据成为了企业决策的关键支撑&#xff0c;但同时也带来了巨大的挑战。企业在运营过程中产生的数据量急剧增加&#xff0c;这对数据传输的速度、安全性和效率提出了更高的要求。然而&#xff0c;传统的FTP传输方式在处理大规模数据时显得力不从心&#x…

MyBatis如何处理延迟加载?

大家好&#xff0c;我是锋哥。今天分享关于【MyBatis如何处理延迟加载&#xff1f;】面试题。希望对大家有帮助&#xff1b; MyBatis如何处理延迟加载&#xff1f; 1000道 互联网大厂Java工程师 精选面试题-Java资源分享网 MyBatis 支持 延迟加载&#xff08;Lazy Loading&am…

Matlab环形柱状图

数据准备&#xff1a; 名称 数值 Aa 21 Bb 23 Cc 35 Dd 47 保存为Excel文件后&#xff1a; % Load data from Excel file filename data.xlsx; % Ensure the file is in the current folder or provide full path dataTable readtable(filena…

论文研读:AnimateDiff—通过微调SD,用图片生成动画

1.概述 AnimateDiff 设计了3个模块来微调通用的文生图Stable Diffusion预训练模型, 以较低的消耗实现图片到动画生成。 论文名&#xff1a;AnimateDiff: Animate Your Personalized Text-to-Image Diffusion Models without Specific Tuning 三大模块&#xff1a; 视频域适应…

洛谷 P1014:Cantor 表

【题目来源】https://www.luogu.com.cn/problem/P1014https://www.acwing.com/problem/content/5510/【题目描述】 现代数学的著名证明之一是 Georg Cantor 证明了有理数是可枚举的。 他是用下面这一张表来证明这一命题的&#xff1a; 1/1 1/2 1/3 1/4 1/5 … 2/1 2/2 …

【网络安全零基础入门】PHP环境搭建、安装Apache、安装与配置MySQL(非常详细)零基础入门到精通,收藏这一篇就够(01)_php安装配置教程

这是大白给粉丝朋友准备的网络安全零基础入门第八章PHP入门的知识章节里的环境搭建。 喜欢的朋友们&#xff0c;记得给大白点赞支持和收藏一下&#xff0c;关注我&#xff0c;学习黑客技术。 一、php简介 php定义&#xff1a;一种服务器端的 HTML脚本/编程语言&#xff0c;是…

RBTree(红黑树)

目录 红黑树的概念 红黑树的性质 红黑树节点的定义 红黑树的插入 1. 按照二叉搜索的树规则插入新节点 2. 检测新节点插入后&#xff0c;红黑树的性质是否造到破坏 红黑树的检测 红黑树的删除 红黑树和AVL树的比较 红黑树的概念 红黑树&#xff0c;是一种二叉搜索树&…

JS 三种添加元素的方式、区别( write、createElement、innerHTML )

文章目录 1. 区别结论2. write 不同场合的效果3. createElement 和 innerHTML 耗时对比 1. 区别结论 方式说明document.write不建议使用, 使用时要小心, 不同场合, 效果不同document.createElement添加少量元素时建议使用, 结构清晰易读innerHTML添加大量元素时建议使用 2. wr…

300多种复古手工裁剪拼贴艺术时尚字母、数字、符号海报封面Vlog视频MOV+PNG素材

300复古时尚大小写字母、数字、符号拼贴海报封面平面设计Vlog视频标题动画 Overlay - Cut-Out Letters Animations Pack - Animated Letters, Numbers, and Symbols 使用 Cut-Out Letters Animations Pack 提升您的内容&#xff01;包含 300多个高品质动画资源&#xff0c;包括…

SpringCloudAlibaba技术栈-Dubbo

1、什么是Dubbo? 简单来说&#xff0c;dubbo就像是个看不见的手&#xff0c;负责专门从注册中心nacos调用注册到nacos上面的服务的&#xff0c;因为在微服务环境下不同的功能模块可能在不同的服务器上。dubbo调用服务就像是在调用本地的服务一样。 分布式调用与高并发处理 Du…

剪映学习01

1.剪映界面介绍 1.点击左上角的的登录账户可以登录剪映&#xff0c;它可以和抖音账号共用&#xff0c;所以我们剪辑完视频后可以直接从抖音发布。 左侧的导航栏有一些功能&#xff0c;我们点击模板&#xff0c;剪映它会显示当下比较火的模板&#xff0c;如果我们剪视频需要用到…

OpenLinkSaas使用手册-简介

OpenLinkSaas是针对软件研发人员/团队的效能工具。对个人而言是工具加成长导航路线&#xff0c;对团队而言是团队管理和项目管理。 OpenLinkSaas虽然功能众多&#xff0c;但可以按需配置所需功能&#xff0c;也可以制作自己的发行版。 OpenLinkSaas的由来 软件研发是一个比较…

QT调用Sqlite数据库

QT设计UI界面&#xff0c;后台访问数据库&#xff0c;实现数据库数据的增删改查。 零售商店系统 数据库表&#xff1a; 分别是顾客表&#xff0c;订单详情表&#xff0c;订单表&#xff0c;商品表 表内字段详情如下&#xff1a; 在QT的Pro文件中添加sql&#xff0c;然后添加头…

vue3使用vant日历组件(calendar),自定义日历下标的两种方法

在vue3中使用vant日历组件&#xff08;calendar&#xff09;自定义下标的两种方法&#xff0c;推荐使用第二种&#xff1a; 日期下方加小圆点&#xff1a; 一、使用伪元素样式实现(::after伪元素小圆点样式会被覆盖&#xff0c;只能添加一个小圆点) 代码如下&#xff08;示例…