程序分析 clang系列学习 (二)

2023-11-12

这里,我主要通过clang API实现自定义的代码检测工具,采用的方式类似于clang-tidy主要是采用AST,个别的会用上CFG。

我实现的方式是libtooling方式,llvm-project下有lintooling示例代码clang-check,不过CMakeLists.txtadd_clang_toolclang_target_link_libraries 属于外部导入的命令,而且clang的API太多了,一个个有点看不过来,个人认为实现checker是一个比较好的学习方式。

clang API

我对clang的API还没有很深入的研究,现在写的Checker也只是照着别人的画瓢,边写边研究。

实现libtooling首先得写CMakeLists.txt,我的CMakeLists.txt代码如下(Clion构建的),这里我没有设置 LLVM_DIR 这个环境变量,直接在CMakeLists中设置:

cmake_minimum_required(VERSION 3.20)
project(ASTMatcher)

set(CMAKE_CXX_STANDARD 14)

# ${LLVM_PACKAGE_VERSION}: 12.0.0
set(LLVM_DIR llvm_path)

set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
add_compile_options(
        "$<$<CXX_COMPILER_ID:GNU,Clang,AppleClang>:-fno-rtti;-Wall>"
        "$<$<AND:$<CXX_COMPILER_ID:GNU>,$<CONFIG:DEBUG>>:-fno-omit-frame-pointer>"
)

set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}/bin")
find_package(LLVM REQUIRED CONFIG HINTS ${LLVM_DIR})
message(STATUS "Found LLVM ${LLVM_PACKAGE_VERSION}")
message(STATUS "Using LLVMConfig.cmake in: ${LLVM_DIR}")
list(APPEND CMAKE_MODULE_PATH "${LLVM_CMAKE_DIR}")
include(AddLLVM)


# Now set the LLVM header and library paths:
include_directories(SYSTEM ${LLVM_INCLUDE_DIRS})
link_directories(${LLVM_LIBRARY_DIRS})
add_definitions(${LLVM_DEFINITIONS})


include_directories(${CMAKE_CURRENT_BINARY_DIR})
add_executable(ASTMatcher main.cpp)

############## FINAL PROJECT CONFIG #################
if (LLVM_ENABLE_ASSERTIONS)
    add_definitions(-DLLVM_ENABLE_ASSERTIONS=${LLVM_ENABLE_ASSERTIONS})
endif()


target_link_libraries(ASTMatcher
        PRIVATE
        -Wl,--start-group
        clangAST
        clangBasic
        clangDriver
        clangFrontend
        clangIndex
        clangLex
        clangSema
        clangSerialization
        clangTooling
        clangFormat
        clangToolingInclusions
        clangToolingCore
        clangRewriteFrontend
        clangStaticAnalyzerFrontend
        clangCrossTU
        clangStaticAnalyzerCheckers
        clangStaticAnalyzerCore
        clangParse
        clangEdit
        clangRewrite
        clangAnalysis
        clangASTMatchers
        clangTransformer
        ${REQ_LLVM_LIBRARIES}
        -Wl,--end-group
)



find_package(Threads REQUIRED)
find_package(Curses REQUIRED)



set_target_properties(ASTMatcher
        PROPERTIES
        LINKER_LANGUAGE CXX
        PREFIX ""
        )

install(TARGETS ASTMatcher
        RUNTIME DESTINATION bin
        )

然后是主函数部分,可以按如下模板进行

int main(int argc, const char** argv) {
    clang::tooling::CommonOptionsParser op(argc, argv, SelfDefinedCategory);
    clang::tooling::ClangTool tool{op.getCompilations(), op.getSourcePathList()};
    int status = tool.run(clang::tooling::newFrontendActionFactory<SelfDefinedAction>().get());
    return status;
}

其中 SelfDefinedActionSelfDefinedCategory 需要自己改,参考 LACommenter.hLACommenter.cpp

Category可以用一行简单的代码盖过 static llvm::cl::OptionCategory LACCategory("lacommenter options");

Action 需要开发者继承 PluginASTAction

class LACPluginAction : public PluginASTAction {
public:
	bool ParseArgs(const CompilerInstance &CI,
		const std::vector<std::string> &args) override {
		return true;
	}

	std::unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance &CI, StringRef file) override {
		LACRewriter.setSourceMgr(CI.getSourceManager(), CI.getLangOpts());
		return std::make_unique<LACommenterASTConsumer>(LACRewriter);
	}

private:
	Rewriter LACRewriter;
};

CreateASTConsumer 方法中创建自定义 ASTConsumer 指针并调用。而 ASTConsumer 则通过 Visitor 或者 Matcher方式完成AST的匹配。

AST匹配部分

AST匹配部分参考clang-tutor。实现AST checker有2种方式,Visitor和Matcher

  • Visitor方式比较原始,开发者编写类继承 RecursiveASTVisitor 类实现对某一些AST结点的访问,ASTConsumer 可以调用自定义Visitor。比如tutor中的HelloWorld 只重写了 VisitCXXRecordDecl 方法来实现访问 CXXRecordDecl 结点(这个结点包含了类定义,函数定义和联合体定义)从而实现用户自定义类,函数,联合体的计数CodeStyleChecker 通过访问 VarDecl, FieldDecl, CXXRecordDecl, FunctionDecl 这些结点查看用户自定义的identifier是否违反命名规范,这类方式的缺点就是只能单独访问结点,当需要考虑上下文情况时很复杂, 参考 blog

  • Matcher方式相对来说比较高效,可以用来查找满足指定模式的AST结点。比如在Obfuscator中通过下面代码来查找整型加法操作,其中匹配到的AST根节点类型是BinaryOperator,满足左操作数和右操作数要么是整型变量要么是整型常量。Matcher的使用方式相对复杂,首先开发者需要自定义 Handler 继承 MatchFinder::MatchCallback,重写 run 方法,将 Handler 与对应的match表达式添加到MatcherFinder 对象中, 通过MatcherFinder 对象匹配AST。可以参考Obfuscator的代码。

const auto MatcherAdd =
                binaryOperator(
                        hasOperatorName("+"),
                        hasLHS(anyOf(implicitCastExpr(hasType(isSignedInteger())).bind("lhs"),
                                     integerLiteral().bind("lhs"))),
                        hasRHS(anyOf(implicitCastExpr(hasType(isSignedInteger())).bind("rhs"),
                                     integerLiteral().bind("rhs"))))
                        .bind("op");

在开发checker过程中可以通过clang-query工具验证上面的match表达式。

被测样本代码int_add.cpp

//
// Created by xxx on 2022/2/6.
//

int foo(int a, int b) {
    return a + b;
}

short bar(short a, short b) {
    return a + b;
}

long bez(long a, long b) {
    return a + b;
}

int fez(long a, short b) {
    return a + b;
}

int fer(int a) {
    return a + 123;
}

int ber() {
    return 321 + 123;
}

命令行操作

clang-query int_add.cpp
clang-query> match binaryOperator(hasOperatorName("+"),hasLHS(anyOf(implicitCastExpr(hasType(isSignedInteger())).bind("lhs"),integerLiteral().bind("lhs"))),hasRHS(anyOf(implicitCastExpr(hasType(isSignedInteger())).bind("rhs"),integerLiteral().bind("rhs")))).bind("op")

匹配结果:

Match #1:

int_add.cpp:6:12: note: "lhs" binds here
    return a + b;
           ^
int_add.cpp:6:12: note: "op" binds here
    return a + b;
           ^~~~~
int_add.cpp:6:16: note: "rhs" binds here
    return a + b;
               ^
int_add.cpp:6:12: note: "root" binds here
    return a + b;
           ^~~~~

Match #2:

int_add.cpp:10:12: note: "lhs" binds here
    return a + b;
           ^
int_add.cpp:10:12: note: "op" binds here
    return a + b;
           ^~~~~
int_add.cpp:10:16: note: "rhs" binds here
    return a + b;
               ^
int_add.cpp:10:12: note: "root" binds here
    return a + b;
           ^~~~~

Match #3:

int_add.cpp:14:12: note: "lhs" binds here
    return a + b;
           ^
int_add.cpp:14:12: note: "op" binds here
    return a + b;
           ^~~~~
int_add.cpp:14:16: note: "rhs" binds here
    return a + b;
               ^
int_add.cpp:14:12: note: "root" binds here
    return a + b;
           ^~~~~

Match #4:

int_add.cpp:18:12: note: "lhs" binds here
    return a + b;
           ^
int_add.cpp:18:12: note: "op" binds here
    return a + b;
           ^~~~~
int_add.cpp:18:16: note: "rhs" binds here
    return a + b;
               ^
int_add.cpp:18:12: note: "root" binds here
    return a + b;
           ^~~~~

Match #5:

int_add.cpp:22:12: note: "lhs" binds here
    return a + 123;
           ^
int_add.cpp:22:12: note: "op" binds here
    return a + 123;
           ^~~~~~~
int_add.cpp:22:16: note: "rhs" binds here
    return a + 123;

int_add.cpp:22:12: note: "root" binds here
    return a + 123;
           ^~~~~~~

Match #6:

int_add.cpp:26:12: note: "lhs" binds here
    return 321 + 123;

int_add.cpp:26:12: note: "op" binds here
    return 321 + 123;
           ^~~~~~~~~
int_add.cpp:26:18: note: "rhs" binds here
    return 321 + 123;
                 ^~~
int_add.cpp:26:12: note: "root" binds here
    return 321 + 123;
           ^~~~~~~~~
6 matches.

UseAfterMoveCheck

这个Checker是从llvm-project 中的clang-tidy搬运过来的,之所以搬运是因为这个checker不是简单的进行ASTMatcher,还综合了CFG进行分析,个人认为是个熟悉clang CFG的好示例。

问题概述

C++中API std::move 的功能是将某个变量的值移动出去,该Checker的作用是对在 move 之后使用的变量报出 warning,但是如果 move 之后该变量重新赋值了那就不报错。

下面代码片段会报错,第3行 cout 了一个被 move 的变量 str

std::string str = "Hello, world!\n";
std::move(str);
std::cout << str << endl;

而下面代码片段不会报错,因为 cout 之前有重新赋值操作。

std::string str = "Hello, world!\n";
std::move(str);
str = "111";
std::cout << str << endl;

这个Checker的检测过程大致分为以下步骤

  • 在AST上通过 ASTMatcher 搜索出与 std::move 相关的语句MovingCall以及 move 的变量 MovedVariable

  • 针对每一个 move 操作,Checker首先根据AST构建CFG,在CFG中以包含 MovingCall 的block为source点,进行深度优先遍历,遍历过程中如果某一个block存在对 MovedVariable 的使用遍返回 True

示例

代码

//
// Created by xxx on 2022/2/11.
//

#include<iostream>
#include<vector>
#include<string>

void func(){
    std::string str = "Hello, world!\n"; // Move Variable 1
    std::vector<std::string> messages;
    int i;
    std::cin >> i;
    if (i == 1) {
        messages.emplace_back(std::move(str)); //MovingCall 1
        str = "";
    }

    if (i == 2) {
        std::cout << str;
    }

    std::string str1 = "damn it!\n"; // MoveVariable 2
    if (i == 3){
        std::move(str1); // MovingCall 2
    }
    std::cout << str1;
}

AST

AST太复杂了,这里挑些重点放上(有删减):

所有的 MovingCallMovedVariableMovingCall 表示调用 std::move() 的语句的AST根节点,MovedVariable 表示被move的变量定义位置的AST根节点。

第一对

MovingCall 对应 messages.emplace_back(std::move(str));

CXXMemberCallExpr 0x346f4c0 'void'
|-MemberExpr 0x346f478 .emplace_back 0x346f378
| `-DeclRefExpr 0x346e710 'std::vector<std::string>':'class 'messages' 
`-CallExpr 0x346f1c0 
  |-ImplicitCastExpr 0x346f1a8 
  | `-DeclRefExpr 0x346ef78 Function 'move' 
  `-DeclRefExpr 0x346e7f8 'std::string':'class 'str'

MovedVariable 对应 std::string str = "Hello, world!\n";

VarDecl 0x344b2f8 used str 
`-ExprWithCleanups 0x344b530  
  `-CXXConstructExpr 0x344b500 
    `-MaterializeTemporaryExpr 0x344b4e8 
      `-CXXBindTemporaryExpr 0x344b4c8  
        `-ImplicitCastExpr 0x344b4a8 
          `-CXXConstructExpr 0x344b470 
            |-ImplicitCastExpr 0x344b388 
            | `-StringLiteral 0x344b360 "Hello, world!\n"
            `-CXXDefaultArgExpr 0x344b450 

第二对

MovingCall 对应 std::move(str1);

CallExpr 0x347c0a8 
|-ImplicitCastExpr 0x347c090 
| `-DeclRefExpr 0x347c058 'move'
`-DeclRefExpr 0x347c000 'std::string':'class 'str1' 

MovedVariable 对应 std::string str = "damn it!\n";

VarDecl 0x344b2f8 used str 
`-ExprWithCleanups 0x344b530  
  `-CXXConstructExpr 0x344b500 
    `-MaterializeTemporaryExpr 0x344b4e8 
      `-CXXBindTemporaryExpr 0x344b4c8  
        `-ImplicitCastExpr 0x344b4a8 
          `-CXXConstructExpr 0x344b470 
            |-ImplicitCastExpr 0x344b388 
            | `-StringLiteral 0x344b360 "damn it!\n"
            `-CXXDefaultArgExpr 0x344b450 

CFG

ViewCFG的输出

[B8 (ENTRY)]
   Succs (1): B7

 [B1]
   1: std::cout << str1 (OperatorCall)
   2: [B3.3].~std::string() (Implicit destructor)
   3: [B7.7].~std::vector<std::string>() (Implicit destructor)
   4: [B7.3].~std::string() (Implicit destructor)
   Preds (2): B2 B3
   Succs (1): B0

 [B2]
   1: std::move(str1)
   Preds (1): B3
   Succs (1): B1

 [B3]
   1: "damn it!\n" (CXXConstructExpr, std::string)
   2: [B3.1] (CXXConstructExpr, std::string)
   3: std::string str1 = "damn it!\n";
   4: ~std::string() (Temporary object destructor)
   5: ~std::allocator<char>() (Temporary object destructor)
   6: i == 3
   T: if [B3.6]
   Preds (2): B4 B5
   Succs (2): B2 B1

 [B4]
   1: std::cout << str (OperatorCall)
   Preds (1): B5
   Succs (1): B3

 [B5]
   1: i == 2
   T: if [B5.1]
   Preds (2): B6 B7
   Succs (2): B4 B3

 [B6]
   1: std::move(str) # use
   2: messages.emplace_back([B6.1]) # use
   3: str = "" (OperatorCall) # reinit, use
   Preds (1): B7
   Succs (1): B5

 [B7]
   1: "Hello, world!\n" (CXXConstructExpr, std::string)
   2: [B7.1] (CXXConstructExpr, std::string)
   3: std::string str = "Hello, world!\n";
   4: ~std::string() (Temporary object destructor)
   5: ~std::allocator<char>() (Temporary object destructor)
   6:  (CXXConstructExpr, std::vector<std::string>)
   7: std::vector<std::string> messages;
   8: int i;
   9: std::cin >> i (OperatorCall)
  10: i == 1
   T: if [B7.10]
   Preds (1): B8
   Succs (2): B6 B5

 [B0 (EXIT)]
   Preds (1): B1

CFG图示如下:请添加图片描述

  • CFG中每个结点对应一个 CFGBlockCFGBlock 中每一行表达式对应一个 CFGElem

  • 可以看到CFG中有的语句(Stmt)被拆分成了几个子表达式(sub-ExprCFGElem)。

检测步骤

算法大致流程

  • 首先算法每次处理一个 <MovingCall,MovedVariable> 对。图中有2个对,分别是变量 str 和变量 str1

  • 针对每个 <MovingCall,MovedVariable>, 算法从包含 MovingCallCFGBlock (第一个是 MovingCall Block)开始深度优先搜索。

  • 针对每个Block,算法找出该Block中 MovedVariable 所有使用(use)和重新定义(reinit)的位置,如果该block中有 use 位置,那么算法会遍历每个 use ,查找在 MovingCalluse 中间是否有 reinit,没有的话报出warning,返回 true 。然后如果该结点中不存在需要报出的情况并且没有 reinit 位置,接着深度优先遍历后续block,有的话返回 false

那么检测上面代码第一个 <MovingCall,MovedVariable> 对的时候,算法从B6开始遍历,在第一个Block中, str 变量不存在 use 情况,但是在下面存在 reinitstr = ""),因此直接返回 false

而第二处算法从B2开始遍历,在第一个Block中并没有找到 usereinit,而在之后一个Block B1中找到了 usestd::cout << str1),并且在之前并没有 reinit,因此返回 true

结果如下:

complexMove.cpp:27:18: warning: variable used after it was moved
    std::cout << str1;
                 ^
complexMove.cpp:25:9: warning: move occurred here
        std::move(str1); // MovingCall 2
        ^
2 warnings generated.

代码

clang的API有点庞大,我对此也没有深入研究,很多东西都是推测出的,欢迎大佬们补充。

整个检测的API调用链大致是 ASTConsumer::HandleTranslationUnit -> MatchFinder::match(Ctx)(匹配所有的 MovingCallMovedVariable) -> MatchFinder::MatchCallback::run(针对每个匹配到的 MovingCallMovedVariable 进行检测)-> UseAfterMoveFinder::find (从AST建立CFG进行遍历)-> UseAfterMoveFinder::findInternal 深度优先遍历。

具体代码可以参考 llvm project clang-tidy 中的 UseAfterMoveCheck,这里它们还用到了一些 util

其中定义了2个类(我的解读可能比较片面,欢迎大家补充)

  • ExprSequence:这个类主要是用来判定一个Expr是否在另一个Expr后面,用到的API有inSequencepotentiallyAfter,这2个API基本等价。这个顺序的判定类似于后序遍历: 父节点最后,左子节点优先于右子节点。
Expr4
 -- EXpr2
   -- Expr1
 -- Expr3
Expr5 
  • StmtToBlockMap,将一个AST Stmt 结点映射到对应的CFGBlock 。主要用到了 blockContainingStmt 函数来判定一个CFGBlock 是否包含这个 Stmt

UseAfterMove的检测过程大致如上,这个检测主要是通过ASTMatcher找到潜在的Source点和Sink点,映射到CFG上,再尝试从Source点遍历到Sink点。

这里也可能产生一些误报,比如上面的CFG中,3个 if 条件最多只有1个可以为 True。加入没有用 str=""; 来进行重新赋值,那么checker会对 messages.emplace_back(std::move(str)); 产生误报。官方对此的解释是这个checker是flow-sensitive的但不是path-sensitive的。

最后,clang libtool在解析代码时如果代码有 #include<xxx> 时可能会报错,这是由于clang对c语言标准库的搜索路径存在的问题导致的,可以命令行输入 echo | clang -v -x c++ -E - 查看#include <...> 搜索路径。不足的可以添加到环境变量也可以在运行libtool时,在filename后添加-- -Iinclude_path(include_path是文件夹路径)来添加。完整的命令格式为 libtool testfile -- -Iinclude_path

相关代码上传到了github

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

程序分析 clang系列学习 (二) 的相关文章

  • 如何将包含 5000 条记录的 Excel 文件插入到 documentDB 中?

    我有一个 Excel 文件 最初约有 200 行 我能够将 Excel 文件转换为数据表 并且所有内容都正确插入到 documentdb 中 Excel 文件现在有 5000 行 在插入 30 40 条记录后不会插入 其余所有行不会插入到
  • 使用 OpenGL 着色器进行数学计算 (C++)

    我有一个矩阵 例如 100x100 尺寸 我需要对每个元素进行计算 matrix i j tt 8 5例如 我有一个巨大的矩阵 我想使用 OpenGL 着色器来实现该算法 我想使用着色器 例如 uniform float val unifo
  • 在Application_AquireRequestState事件中用POST数据重写Url

    我有一个在其中注册路线的代码Application AcquireRequestState应用程序的事件 注册路由后 我会在 Http 运行时缓存中设置一个标志 这样我就不会再次执行路由注册代码 在此事件中注册路线有特定原因Applicat
  • 访问“if”语句之外的变量

    我怎样才能使insuranceCost以外可用if陈述 if this comboBox5 Text Third Party Fire and Theft double insuranceCost 1 在 if 语句之外定义它 double
  • 导出类时编译器错误

    我正在使用 Visual Studio 2013 但遇到了一个奇怪的问题 当我导出一个类时 它会抛出 尝试引用已删除的函数 错误 但是 当该类未导出时 它的行为会正确 让我举个例子 class Foo note the export cla
  • Paradox 表 - Oledb 异常:外部表不是预期的格式

    我正在使用 Oledb 从 Paradox 表中读取一些数据 我遇到的问题是 当我将代码复制到控制台应用程序时 代码可以工作 但在 WinForms 中却不行 两者都以 x86 进行调试 我实际上只是复制代码 在 WinForms 应用程序
  • 矩阵向量变换

    我正在编写一个代码来制作软件蒙皮器 骨骼 皮肤动画 并且我正处于 优化 阶段 蒙皮器工作得很好 并且在 Core 上 1 09 毫秒内对 4900 个三角形网格与 22 个骨骼进行蒙皮Duo 2 Ghz 笔记本 我需要知道的是 1 有人可以
  • 是否有像 gccxml 这样的用于生成包装器的 C 标头解析器工具?

    我需要为一种新的编程语言编写一些 C 标头包装器 并且想要类似 gccxml 的东西 但不完全依赖 gcc 以及它在 Windows 系统上带来的问题 只需要读C而不是C 只要有完整的文档记录 任何格式的输出都可以 Linux Solari
  • 存储过程上的 OdbcCommand - 输出参数上出现“未提供参数”错误

    我正在尝试执行存储过程 通过 ODBC 驱动程序针对 SQL Server 2005 但收到以下错误 过程或函数 GetNodeID 需要参数 ID 但未提供该参数 ID 是我的过程的 OUTPUT 参数 在存储过程中指定了一个输入 mac
  • 在简单注入器中注册具有多个构造函数和字符串依赖项的类型

    我正在尝试弄清楚如何使用 Simple Injector 我在项目中使用了它 注册简单服务及其组件没有任何问题 但是 当组件具有两个以上实现接口的构造函数时 我想使用依赖注入器 public DAL IDAL private Logger
  • dropdownlist DataTextField 由属性组成?

    有没有一种方法可以通过 C 使 asp net 中的下拉列表的 datatextfield 属性由对象的多个属性组成 public class MyObject public int Id get set public string Nam
  • 使用 AdHocWorkspace 会导致“不支持语言‘C#’”。

    在VS2015中使用Microsoft CodeAnalysis CSharp Workspaces的RC2 这段代码会抛出异常 var tree CSharpSyntaxTree ParseText var workspace new A
  • 在VisualStudio DTE中,如何获取ActiveDocument的内容?

    我正在 VisualStudio 中编写脚本 并尝试获取当前 ActiveDocument 的内容 这是我当前的解决方案 var visualStudio new API VisualStudio 2010 var vsDTE visual
  • 从事务范围调用 WCF 服务方法

    我有这样的代码 using TransactionScope scope TransactionScopeFactory CreateTransactionScope some methodes calls for which scope
  • 不兼容的类型 - 是因为数组已经是指针吗?

    在下面的代码中 我创建一个基于书籍结构的对象 并让它保存多个 书籍 我设置的是一个数组 即定义 启动的对象 然而 每当我去测试我对指针的了解 实践有帮助 并尝试创建一个指向创建的对象的指针时 它都会给我错误 C Users Justin D
  • .NET JIT 编译的代码缓存在哪里?

    NET 程序首先被编译为 MSIL 代码 当它被执行时 JIT编译器会将其编译为本机机器代码 我想知道 这些JIT编译的机器代码存储在哪里 它只存储在进程的地址空间中吗 但由于程序的第二次启动比第一次快得多 我认为即使在执行完成后 该本机代
  • 如何使用收益返回和递归获得字母的每个组合?

    我有几个像这样的字符串列表 可能有几十个列表 1 A B C 2 1 2 3 3 D E F 这三个仅作为示例 用户可以从几十个具有不同数量元素的类似列表中进行选择 再举个例子 这对于用户来说也是一个完全有效的选择 25 empty 4 1
  • ASP.NET Core Razor Page 多路径路由

    我正在使用 ASP NET Core 2 0 Razor Pages 不是 MVC 构建系统 但在为页面添加多个路由时遇到问题 例如 所有页面都应该能够通过 abc com language 访问segment shop mypage 或
  • 使用 Chrome 和 Selenium 设置 LocalStorage

    我正在尝试使用 OpenQA Selenium 和 Chrome 设置本地存储键和值 我认为这相当微不足道 但我似乎无法让它发挥作用 我对 C 很陌生 所以我可能错过了一些东西 无论如何 我有这个功能 public static void
  • c# 模拟 IFormFile CopyToAsync() 方法

    我正在对一个异步函数进行单元测试 该函数将 IFormFile 列表转换为我自己的任意数据库文件类列表 将文件数据转换为字节数组的方法是 internal async Task

随机推荐

  • mysql的流程控制if与case

    mysql中常用的流程控制有两种 1 if语句 基本语法 IF expr v1 v2 如果表达式 expr 成立 返回结果 v1 否则 返回结果 v2 用法跟三目运算符类似 适用只有两种结果 案例 SELECT IF 1 gt 0 正确 错
  • 疯壳AI语音及人脸识别教程2-4串口

    目录 1 1寄存器 1 1 2实验现象 17 视频地址 https fengke club GeekMart su f9cTSxNsp jsp 串口 官方QQ群 457586268 串行接口分为异步串行接口和同步串行接口两种 异步串行接口统
  • 这100套毕设项目,是给计算机系学弟学妹在毕业季的一波镇定剂!练手收藏

    又到了一年一度的毕业季了 有憧憬社会的 也有怀念校园生活的 不管如何我们都要努力向前 迎接变化 这次小编整理的100套Java毕设项目 给正在发愁的你和将来要项目练手的你一波助力 具体内容目录给大家看看 希望可以帮到你 需要更多学习方式和资
  • [论文阅读] (28)李沐老师视频学习——1.研究的艺术·跟读者建立联系

    娜璋带你读论文 系列主要是督促自己阅读优秀论文及听取学术讲座 并分享给大家 希望您喜欢 由于作者的英文水平和学术能力不高 需要不断提升 所以还请大家批评指正 非常欢迎大家给我留言评论 学术路上期待与您前行 加油 前一篇文章介绍AAAI20腾
  • depends工具查看exe和dll依赖关系

    应用场景 在使用QT等图形用户界面应用程序开发框架开发Windows程序时 通常需要将写到的程序发布到其它计算机中进使用 在使用Qt发布程序时 虽然使用windeployqt工具能够自动打包好大部分依赖库 但还是难免会漏掉一些第三方库导致发
  • C#学习05-类简介与派生继承

    基本概念 类是一种数据结构 它可以包含数据成员 函数成员以及嵌套类型 C 中类的声明 C 中类的声明即定义 不同于c 中声明与定义是分开的 C 类构造函数 类的 构造函数 是类的一个特殊的成员函数 当创建类的新对象时执行 构造函数的名称与类
  • Scrapy源码分析之Dupfilters模块(第二期)

    大家好 我是TheWeiJun 欢迎来到我的公众号 今天给大家带来Scrapy源码分析之Dupfilters模块源码详解 希望大家能够喜欢 如果你觉得我的文章内容有价值 记得点赞 关注 特别声明 本公众号文章只作为学术研究 不用于其它用途
  • 四层负载均衡的NAT模型与DR模型推导

    导读 本文首先讲述四层负载均衡技术的特点 然后通过提问的方式推导出四层负载均衡器的NAT模型和DR模型的工作原理 通过本文可以了解到四层负载均衡的技术特点 NAT模型和DR模型的工作原理 以及NAT模型和DR模型的优缺点 读者可以重点关注N
  • 【IDEA】IDEA 下 maven 一个诡异问题,一个正常项目 过了一夜 依赖很多 飘红

    文章目录 1 场景1 1 1 概述 2 场景再现2 1 场景1 1 1 概述 我有一个项目是flink 1 9 升级到 flink 1 10 升级完毕后 我都在服务器打包了 然后过了一夜后 第二天也能正常打包 然后下午的时候 去运行主类 本
  • 【Unity底层插件】Dll打包のBug

    1 修改官方demoRenderingPlugin cpp时 UnityPluginLoad不会被调用 解决方案 https forum unity3d com threads native plug in issues unityplug
  • STM32系统时钟频率更改

    注 此文仅作为个人学习记录 海创学习记录 图0 手册时钟图 stm32的系统时钟频率在驱动文件中一般情况下是被固定的 系统频率有几种 分别为24MHz 36MHz 48MHz 56MHz 72MHz 一般情况下 md s 默认设置为72MH
  • Android最佳实践——深入浅出WebSocket协议

    转自 https blog csdn net sbsujjbcy article details 52839540 首先明确一下概念 WebSocket协议是一种建立在TCP连接基础上的全双工通信的协议 概念强调了两点内容 TCP基础上 全
  • 树莓派+多个微雪电子Serial Expansion HAT扩展板叠加方法(扩展多个IO口、串口)

    微雪电子官方教程仅介绍了单层扩展板配置方法 因此本文参考官方教程的基础上 进行多个扩展板的叠加配置 文章目录 一 打开I2C接口 二 安装库 三 生成设备 四 堆叠教程 五 扩展IO口配置 总结 一 打开I2C接口 在终端执行 sudo r
  • Lyapunov稳定性分析1(正定函数、二次型正定判定)

    一 正定函数 1 1 定义 令V x 是向量x的标量函数 S是x空间包含原点的封闭有限区域 如果对于S中的所有x 都有 则V x 是正定的 半正定 正定函数更直观的描述如下图所示 如果条件 3 中不等式的符号反向 则称V x 是负定的 负半
  • 拼多多产品怎么引流?拼多多商品怎么引更多的流量?

    说到拼多多引流技巧 对于有资源的商家来说可能没有什么难事 而对于一些刚刚入手这个行业的商家来说未尝不是一种借鉴 博傲电商今天分享几点 首先是说下直通车引流 这个方法简单粗暴 看上去只要烧钱投放广告 流量都可以进来 是一个效果比较好的方法 但
  • android内存管理 面试题,Android面试题内存&性能篇

    Android面试题内存 性能篇 由本人整理汇总 后续将继续推出系列篇 如果喜欢请持续关注和推荐 内存分配 RAM random access memory 随机存取存储器 说白了就是内存 一般Java在内存分配时会涉及到以下区域 寄存器
  • DNS 协议是什么?完整查询过程?为什么选择使用 UDP 协议发起 DNS 查询?

    你可能了解 DNS 协议是什么 那你了解 DNS 完整查询过程是什么吗 它底层是基于 TCP 还是 UDP 喃 TCP 与 UDP 又各自负责 DNS 的哪些部分喃 引言 本文从以下几个方面循序渐进走进 DNS 协议 它的完整查询过程以及底
  • 数据结构---堆----C语言实现

    目录 堆排序介绍 功能介绍 功能实现 公式 功能合并 这里添加另一种方法 只用到了向下调整法就可排序 源码1 源码2 堆排序介绍 堆排序 Heapsort 是指利用堆这种数据结构所设计的一种排序算法 是一个近似完全二叉树的结构 并同时满足堆
  • Android中协调布局CoordinatorLayout的使用

    Android自5 0之后对UI做了较大的提升 一个重大的改进是推出了MaterialDesign库 而该库的基础即为协调布局CoordinatorLayout 几乎所有的design控件都依赖于该布局 今天我们就学习一下Coordinat
  • 程序分析 clang系列学习 (二)

    clang静态检测 clang API AST匹配部分 UseAfterMoveCheck 问题概述 示例 代码 AST CFG 检测步骤 算法大致流程 代码 这里 我主要通过clang API实现自定义的代码检测工具 采用的方式类似于cl