目前市面上的许多安全公司都会在保护IOS应用程序或安卓APP时都会用到OLLVM技术。譬如说顶象IOS加固、网易IOS加固等等。故而我们今天研究下OLLVM是个什么。将从(1)OLLVM是什么?OLLVM与LLVM的关系;(2)OLLVM的三大功能;(3)OLLVM的配置过程;(4)OLLVM源码分析。(4)OLLVM使用四个方面进行说明。

(一)OLLVM是什么?

OLLVM是一款是由瑞士西北科技大学开发的一套开源的针对LLVM的代码混淆工具,旨在加强逆向的难度,整个项目包含数个包含独立功能的LLVM Pass,每个Pass会对应实现一种特定的混淆方式,这些Pass将在后面进行细说,通过这些Pass可以改变源程序的CFG和源程序的结构。后期转向商业项目strong.protect。Github目前已支持OLLVM-4.0.

与此同时,LLVM与OLLVM最大的区别在于混淆Pass的不同。混淆Pass作用于LLVM的IR中间语言,通过Pass混淆IR,最后后端依据IR生成的目标语言也会得到相应的混淆。得益于LLVM的三段式结构,即前端对代码进行语法分析词法分析形成AST并转换为中间IR语言,一系列优化Pass对IR中间语言进行优化操作,或混淆,或分析,或改变IR的操作码等等。最终在后端解释为相应平台嘚瑟机器码。OLLVM支持LLVM所支持的所有前端语言:C,C++,Objective-C,Fortran等等和LLVM所支持的所有目标平台:x86,x86-64,PowerPC,PowerPC-64, ARM, Thumb, SPARC, Alpha, CellSPU, MIPS, MSP430, SystemZ, 和 XCore。

(二)OLLVM的三大功能

OLLVM有三大功能,分别是:Instructions Substitution(指令替换)、Bogus Control Flow(混淆控制流)、Control Flow Flattening(控制流平展)。Github上也有OLLVM每个功能详细的介绍和举例:https://github.com/obfuscator-llvm/obfuscator/wiki/Features。操作指令可以是一个或多个参数。

(1)指令替换功能:随机选择一种功能上等效但更复杂的指令序列替换标准二元运算符;适用范围:加法操作、减法操作、布尔操作(与或非操作)且只能为整数类型。

操作指令:

  1.  
    -mllvm -sub: activate instructions substitution
  2.  
    -mllvm -sub_loop=3: if the pass is activated, applies it 3 times on a function. Default : 1.

示例代码:

  1.  
    //替换前
  2.  
    a = b - (-c)
  3.  
    %0 = load i32* %a, align 4
  4.  
    %1 = load i32* %b, align 4
  5.  
    %2 = sub i32 0, %1
  6.  
    %3 = sub nsw i32 %0, %2
  7.  
    //替换后
  8.  
    a = -(-b + (-c))
  9.  
    %0 = load i32* %a, align 4
  10.  
    %1 = load i32* %b, align 4
  11.  
    %2 = sub i32 0, %0
  12.  
    %3 = sub i32 0, %1
  13.  
    %4 = add i32 %2, %3
  14.  
    %5 = sub nsw i32 0, %4

(2)混淆控制流功能:1.在当前基本块之前添加基本块来修改函数调用图。2.原始基本块也被克隆并填充随机选择的垃圾指令。

操作指令:

  1.  
    -mllvm -bcf: activates the bogus control flow pass
  2.  
    -mllvm -bcf_loop=3: if the pass is activated, applies it 3 times on a function. Default: 1
  3.  
    -mllvm -bcf_prob=40: if the pass is activated, a basic bloc will be obfuscated with a probability of 40%. Default: 30

(3)控制流平展功能:目的是完全展平程序的控制流程图。我自己的理解是if...else变为switch..case..语句。

操作指令:

  1.  
    -mllvm -fla: activates control flow flattening
  2.  
    -mllvm -split: activates basic block splitting. Improve the flattening when applied together.
  3.  
    -mllvm -split_num=3: if the pass is activated, applies it 3 times on each basic block. Default: 1

(三)OLLVM环境搭建:

OLLVM版本号:OLLVM 4.0;Ubuntu环境:Ubuntu16.04;虚拟机中处理器数量为4个、运行内存3G,分配硬盘空间50g。

  1.  
    $ git clone -b llvm-4.0 https://github.com/obfuscator-llvm/obfuscator.git
  2.  
    $ mkdir build
  3.  
    $ cd build
  4.  
    $ cmake -DCMAKE_BUILD_TYPE=Release ../obfuscator/
  5.  
    $ make -j7

若是git clone一直失败,下不下来,尝试:

 git config --global http.postBuffer 20000000

若是cmake时一直报错,则将cmake那句替换为:

cmake -DCMAKE_BUILD_TYPE=Release -DLLVM_INCLUDE_TESTS=OFF ../obfuscator/

若是make时时间太长,则重新cmake后,多分配一些内存和处理器。

(四)OLLVM源码分析

参考博客:https://www.jianshu.com/p/942875aa73cc

所有的混淆性Pass都位于/ollvm/obfuscator/lib/Transforms/Obfuscation,利用Clion软件打开可以得到其结构。Obfuscation文件夹下包含以下文件:

4.1指令切割功能:

实现于SplitBasicBlock.cpp中,继承自FunctionPass,并重写了runOnFunction方法。

第一步:判断切割次数是否符合OLLVM的要求,对于splitNum在1~10 之外的情况,提示分割次数错误,即分割次数必须在1~10次之内。

第二步:对于符合要求的splitNum,调用toObfuscate函数进行处理,处理方式如下(该函数在Utils.h文件中)。主要是各种检查以及判断是否启用了split功能,判断依据就是Functions annotationsflag。

  1.  
    bool SplitBasicBlock::runOnFunction(Function &F) {
  2.  
    // Check if the number of applications is correct
  3.  
    if (!((SplitNum > 1) && (SplitNum <= 10))) {
  4.  
    errs()<<"Split application basic block percentage\
  5.  
    -split_num=x must be 1 < x <= 10";
  6.  
    return false;
  7.  
    }
  8.  
    Function *tmp = &F;
  9.  
    // Do we obfuscate
  10.  
    if (toObfuscate(flag, tmp, "split")) {
  11.  
    split(tmp);
  12.  
    ++Split;
  13.  
    }
  14.  
     
  15.  
    return false;
  16.  
    }

第三步:利用split函数进行分割处理。

(1)定义了一个vector数组origBB用于保存所有的block块,

(2)遍历origBB,对每一个blockcurr,如果它的size(即包含的指令数)只有1个或者包含PHI节点,则不分割该block。

(3)待分割的block,首先生成分割点,用test数组存放分割点,用shuffle打乱指令的顺序,使sort函数排序前splitN个数能尽量随机。

(4)分割block是调用splitBasicBlock函数分割基本块。

  1.  
    void SplitBasicBlock::split(Function *f) {
  2.  
    std::vector<BasicBlock *> origBB;
  3.  
    int splitN = SplitNum;
  4.  
    // Save all basic blocks
  5.  
    for (Function::iterator I = f->begin(), IE = f->end(); I != IE; ++I) {
  6.  
    origBB.push_back(&*I);
  7.  
    }
  8.  
    for (std::vector<BasicBlock *>::iterator I = origBB.begin(),IE = origBB.end(); I != IE; ++I) {
  9.  
    BasicBlock *curr = *I;
  10.  
    // No need to split a 1 inst bb
  11.  
    // Or ones containing a PHI node
  12.  
    if (curr->size() < 2 || containsPHI(curr)) {
  13.  
    continue;
  14.  
    }
  15.  
    // Check splitN and current BB size
  16.  
    if ((size_t)splitN > curr->size()) {
  17.  
    splitN = curr->size() - 1;
  18.  
    }
  19.  
    // Generate splits point
  20.  
    std::vector<int> test;
  21.  
    for (unsigned i = 1; i < curr->size(); ++i) {
  22.  
    test.push_back(i);
  23.  
    }
  24.  
    // Shuffle
  25.  
    if (test.size() != 1) {
  26.  
    shuffle(test);
  27.  
    std::sort(test.begin(), test.begin() + splitN);
  28.  
    }
  29.  
    // Split
  30.  
    BasicBlock::iterator it = curr->begin();
  31.  
    BasicBlock *toSplit = curr;
  32.  
    int last = 0;
  33.  
    for (int i = 0; i < splitN; ++i) {
  34.  
    for (int j = 0; j < test[i] - last; ++j) {
  35.  
    ++it;
  36.  
    }
  37.  
    last = test[i];
  38.  
    if(toSplit->size() < 2)
  39.  
    continue;
  40.  
    toSplit = toSplit->splitBasicBlock(it, toSplit->getName() + ".split");
  41.  
    }
  42.  
    ++Split;
  43.  
    }
  44.  
    }

参考博客:https://www.jianshu.com/p/942875aa73cc

4.2.指令替换功能:

实现于Substitution.cpp中,同样继承自FunctionPass,并重写了runOnFunction方法。

第一步:调用toObfuscate函数进行处理,进入至substitute方法后,在这个方法中,可以看到,ollvm只对加、减、或、与、异或这五种操作进行替换,funcXXX变量都是函数数组,随机的选择一种变换进行操作。ObfTimes对应的是指令切割次数:-sub_loop。

  1.  
    bool Substitution::substitute(Function *f) {
  2.  
    Function *tmp = f;
  3.  
     
  4.  
    // Loop for the number of time we run the pass on the function
  5.  
    int times = ObfTimes;
  6.  
    do {
  7.  
    for (Function::iterator bb = tmp->begin(); bb != tmp->end(); ++bb) {
  8.  
    for (BasicBlock::iterator inst = bb->begin(); inst != bb->end(); ++inst) {
  9.  
    if (inst->isBinaryOp()) {
  10.  
    switch (inst->getOpcode()) {
  11.  
    case BinaryOperator::Add:
  12.  
    // case BinaryOperator::FAdd:
  13.  
    // Substitute with random add operation
  14.  
    (this->*funcAdd[llvm::cryptoutils->get_range(NUMBER_ADD_SUBST)])(
  15.  
    cast<BinaryOperator>(inst));
  16.  
    ++Add;
  17.  
    break;
  18.  
    case BinaryOperator::Sub:
  19.  
    // case BinaryOperator::FSub:
  20.  
    // Substitute with random sub operation
  21.  
    (this->*funcSub[llvm::cryptoutils->get_range(NUMBER_SUB_SUBST)])(
  22.  
    cast<BinaryOperator>(inst));
  23.  
    ++Sub;
  24.  
    break;
  25.  
    case Instruction::AShr:
  26.  
    //++Shi;
  27.  
    break;
  28.  
    .....
  29.  
    break;
  30.  
    } // End switch
  31.  
    } // End isBinaryOp
  32.  
    } // End for basickblock
  33.  
    } // End for Function
  34.  
    } while (--times > 0); // for times
  35.  
    return false;
  36.  
    }

第二步:以下代码中对应着funcAdd数组的四种替换方法的实现。

(1)将第二个操作数取反,然后改写成减法指令。

(2)将两个操作数都取反,结果相加之后再次取反。

(3)取一个随机数,将随机数与操作数1相加,然后将结果与操作数2相加,最后减去随机数。

(4)取一个随机数,将操作数1减去随机数,然后将结果与操作数2相加,最后加上随机数。

  1.  
    // Implementation of a = b - (-c)
  2.  
    void Substitution::addNeg(BinaryOperator *bo) {
  3.  
    BinaryOperator *op = NULL;
  4.  
    // Create sub
  5.  
    if (bo->getOpcode() == Instruction::Add) {
  6.  
    op = BinaryOperator::CreateNeg(bo->getOperand(1), "", bo);
  7.  
    op =
  8.  
    BinaryOperator::Create(Instruction::Sub, bo->getOperand(0), op, "", bo);
  9.  
     
  10.  
    // Check signed wrap
  11.  
    //op->setHasNoSignedWrap(bo->hasNoSignedWrap());
  12.  
    //op->setHasNoUnsignedWrap(bo->hasNoUnsignedWrap());
  13.  
     
  14.  
    bo->replaceAllUsesWith(op);
  15.  
    }/* else {
  16.  
    op = BinaryOperator::CreateFNeg(bo->getOperand(1), "", bo);
  17.  
    op = BinaryOperator::Create(Instruction::FSub, bo->getOperand(0), op, "",
  18.  
    bo);
  19.  
    }*/
  20.  
    }
  21.  
    ......

4.3.控制流平坦功能 :

实现于Flattening.cpp中,同样继承自FunctionPass,并重写了runOnFunction方法。

第一步:判断是否能够平展。若可以,则跳入flatten方法中执行。在函数开始,使用LowerSwitchPass去除switch,将switch结构换成if结构。保存所有的基本代码块,如果只有一个基本代码块,则不进行处理;如果第一个基本块的末尾是有条件的跳转指令,那么需要将它分割开,并且将它保存到origBB;

  1.  
    // Lower switch
  2.  
    FunctionPass *lower = createLowerSwitchPass();
  3.  
    lower->runOnFunction(*f);

第二步:创建两个基本块,存放循环头和尾的指令。然后将first bb移到到loopEntry的前面,并且创建一条跳转指令,从first bb跳到loopEntry。紧接着创建了一条从loopEnd跳到loopEntry的指令。最后,创建了switch指令和switch default块,并且创建相应的跳转。

  1.  
    // Create main loop
  2.  
    loopEntry = BasicBlock::Create(f->getContext(), "loopEntry", f, insert);
  3.  
    loopEnd = BasicBlock::Create(f->getContext(), "loopEnd", f, insert);
  4.  
    load = new LoadInst(switchVar, "switchVar", loopEntry);
  5.  
    // Move first BB on top
  6.  
    insert->moveBefore(loopEntry);
  7.  
    BranchInst::Create(loopEntry, insert);
  8.  
    // loopEnd jump to loopEntry
  9.  
    BranchInst::Create(loopEntry, loopEnd);
  10.  
    BasicBlock *swDefault =
  11.  
    BasicBlock::Create(f->getContext(), "switchDefault", f, loopEnd);
  12.  
    BranchInst::Create(loopEnd, swDefault);
  13.  
    // Create switch instruction itself and set condition
  14.  
    switchI = SwitchInst::Create(&*f->begin(), swDefault, 0, loopEntry);
  15.  
    switchI->setCondition(load);
  16.  
    ......

第三步,删除first bb的跳转指令,改为跳转到loopEntry,将所有的基本块加入switch结构.接下来是根据原先的跳转来计算switch变量。

(1)若为没有后继(return BB)的基本块,直接跳过。

(2)若为只有一个后继的基本块,首先删除跳转指令,并且通过后继基本块来搜索对应的switch case,根据case创建一条存储指令,达到跳转的目的。

(3)两个后继的情况跟一个后继的处理方法相似,不同的是,创建一条select指令,根据条件的结果来选择分支。

4.4.虚假控制流功能 :

保护前后代码代码块CFG的变化:

实现过程:

第一步:进入runOnFunction后,调用bogus方法,这是实现控制流混淆的核心。我们一起来看看他干了啥事:(1)先是根据传递进来的参数值输出相应的信息,主要判断ObfTimes,混淆次数是否大于0.NumObfTimes关联着-bcf_loop选项的值;(2)跟之前一样保存基本块;(3)遍历基本块,随机决定当前基本块是否需要修改,ObfProbRate变量关联着-bcf_prob选项的值。如果命中,则调用addBogusFlow函数。

  1.  
    void bogus(Function &F) {
  2.  
    // For statistics and debug
  3.  
    ...
  4.  
    //First Step:
  5.  
    DEBUG_WITH_TYPE("opt", errs() << "bcf: How many times: "<< ObfTimes<< "\n");
  6.  
    if(ObfTimes <= 0){
  7.  
    DEBUG_WITH_TYPE("opt", errs() << "bcf: Incorrect value,"
  8.  
    << " must be greater than 1. Set to default: "
  9.  
    << defaultObfTime <<" \n");
  10.  
    ObfTimes = defaultObfTime;
  11.  
    }
  12.  
    NumTimesOnFunctions = ObfTimes;
  13.  
    int NumObfTimes = ObfTimes;
  14.  
    ...
  15.  
    //Second step:Put all the function's block in a list
  16.  
    std::list<BasicBlock *> basicBlocks;
  17.  
    for (Function::iterator i=F.begin();i!=F.end();++i) {
  18.  
    basicBlocks.push_back(&*i);
  19.  
    }
  20.  
    ...
  21.  
    //Third Step:Basic Blocks' selection
  22.  
    if((int)llvm::cryptoutils->get_range(100) <= ObfProbRate){
  23.  
    DEBUG_WITH_TYPE("opt", errs() << "bcf: Block "
  24.  
    << NumBasicBlocks <<" selected. \n");
  25.  
    ...
  26.  
    // Add bogus flow to the given Basic Block (see description)
  27.  
    BasicBlock *basicBlock = basicBlocks.front();
  28.  
    addBogusFlow(basicBlock, F);
  29.  
    }
  30.  
    }}

第二步:进入到addBogusFlow函数后;(1)先切割基本块,将其分为两块,一部分是phi节点信息、调试信息等等;另一部分是原始块中的所有指令;(2)复制基本块的所有信息,添加花指令信息;(3)现在相当于有三个模块,一块是与混淆无关的basickbloak,一块是由basickboak切割出来的originalBB,一块是由addBogusFlow产生的alteredBB,将三者拼凑起来成下图左所示。(4)在addBogusFlow函数的最后,将originalBB的最后一条语句分割出来,然后拼接成下图右所示:

  1.  
    // Creating the altered basic block on which the first basicBlock will jump
  2.  
    Twine * var3 = new Twine("alteredBB");
  3.  
    BasicBlock *alteredBB = createAlteredBasicBlock(originalBB, *var3, &F);
  4.  
    DEBUG_WITH_TYPE("gen", errs() << "bcf: Altered basic block: ok\n");
  1.  
    // Jump to the original basic block if the condition is true or
  2.  
    // to the altered block if false.
  3.  
    BranchInst::Create(originalBB, alteredBB, (Value *)condition, basicBlock);
  4.  
    DEBUG_WITH_TYPE("gen",errs() << "bcf: Terminator instruction in first basic block: ok\n");
  5.  
    // The altered block loop back on the original one.
  6.  
    BranchInst::Create(originalBB, alteredBB);
  7.  
    DEBUG_WITH_TYPE("gen", errs() << "bcf: Terminator instruction in altered block: ok\n");

                           

第三步,接着执行dof函数,遍历模块的所有基本块,搜索出条件永远为true的比较语句。用(x - 1) * x % 2 == 0 || y < 0这一永真句替换掉我们这找到的true的比较语句。

  1.  
    doF(*F.getParent());
  2.  
    ...
  3.  
    bool doF(Module &M){
  4.  
    ...
  5.  
    // The global values
  6.  
    Twine * varX = new Twine("x");
  7.  
    Twine * varY = new Twine("y");
  8.  
    ...
  9.  
    GlobalVariable * x = new GlobalVariable(M, Type::getInt32Ty(M.getContext()), false,
  10.  
    GlobalValue::CommonLinkage, (Constant * )x1,

参考博客:http://www.ench4nt3r.com/2018/02/26/post/#%E8%99%9A%E5%81%87%E6%8E%A7%E5%88%B6%E6%B5%81

(五)OLLVM混淆前后示例

保护前CPP源码:

  1.  
    #include <stdio.h>
  2.  
    int main() {
  3.  
    int t=3;
  4.  
    if(t<4){
  5.  
    t++;
  6.  
    }else{
  7.  
    }
  8.  
    printf("hello llvm\n");
  9.  
    return 0;
  10.  
    }

5.1 指令替换功能:

保护命令:

  1.  
    '/home/kyriehe/Desktop/ollvm/build/bin/clang' -emit-llvm test.c -mllvm -sub -S -o testsub.ll
  2.  
     
  3.  
    '/home/kyriehe/Desktop/ollvm/build/bin/clang' test.c -mllvm -sub -o test

保护前后的.ll文件关键代码段的对比:

                                                   

5.2 控制流平坦功能 :

保护命令:

  1.  
    '/home/kyriehe/Desktop/ollvm/build/bin/clang' -emit-llvm test.c -mllvm -fla -S -o testfla.ll
  2.  
     
  3.  
    '/home/kyriehe/Desktop/ollvm/build/bin/clang' test.c -mllvm -fla -o test

保护前后的.ll文件关键代码段的对比:

                                      

5.2 混淆控制流功能 :

保护命令:

  1.  
    '/home/kyriehe/Desktop/ollvm/build/bin/clang' -emit-llvm test.c -mllvm -bcf -S -o testfla.ll
  2.  
     
  3.  
    '/home/kyriehe/Desktop/ollvm/build/bin/clang' test.c -mllvm -bcf -o test

保护前后的.ll文件关键代码段的对比:

                                              

总结一下,相比于指令替换和控制流平坦功能,混淆控制流更为复杂,相对较难破解,但目前市面上已经有了针对于OLLVM混淆的反混淆脚本,能够轻易干掉经OLLVM保护后的Android应用程序,所以我们可能还需要更深入的思考保护方法。

OLLVM简单入门的更多相关文章

  1. 用IntelliJ IDEA创建Gradle项目简单入门

    Gradle和Maven一样,是Java用得最多的构建工具之一,在Maven之前,解决jar包引用的问题真是令人抓狂,有了Maven后日子就好过起来了,而现在又有了Gradle,Maven有的功能它都 ...

  2. [原创]MYSQL的简单入门

    MYSQL简单入门: 查询库名称:show databases; information_schema mysql test 2:创建库 create database 库名 DEFAULT CHAR ...

  3. Okio 1.9简单入门

    Okio 1.9简单入门 Okio库是由square公司开发的,补充了java.io和java.nio的不足,更加方便,快速的访问.存储和处理你的数据.而OkHttp的底层也使用该库作为支持. 该库极 ...

  4. emacs最简单入门,只要10分钟

    macs最简单入门,只要10分钟  windwiny @2013    无聊的时候又看到鼓吹emacs的文章,以前也有几次想尝试,结果都是玩不到10分钟就退出删除了. 这次硬着头皮,打开几篇文章都看完 ...

  5. 【java开发系列】—— spring简单入门示例

    1 JDK安装 2 Struts2简单入门示例 前言 作为入门级的记录帖,没有过多的技术含量,简单的搭建配置框架而已.这次讲到spring,这个应该是SSH中的重量级框架,它主要包含两个内容:控制反转 ...

  6. Docker 简单入门

    Docker 简单入门 http://blog.csdn.net/samxx8/article/details/38946737

  7. Springmvc整合tiles框架简单入门示例(maven)

    Springmvc整合tiles框架简单入门示例(maven) 本教程基于Springmvc,spring mvc和maven怎么弄就不具体说了,这边就只简单说tiles框架的整合. 先贴上源码(免积 ...

  8. git简单入门

    git简单入门 标签(空格分隔): git git是作为程序员必备的技能.在这里就不去介绍版本控制和git产生的历史了. 首先看看常用的git命令: git init git add git comm ...

  9. 程序员,一起玩转GitHub版本控制,超简单入门教程 干货2

    本GitHub教程旨在能够帮助大家快速入门学习使用GitHub,进行版本控制.帮助大家摆脱命令行工具,简单快速的使用GitHub. 做全栈攻城狮-写代码也要读书,爱全栈,更爱生活. 更多原创教程请关注 ...

随机推荐

  1. C# 9.0 正式发布了(C# 9.0 on the record)

    翻译自 Mads Torgersen 2020年11月10日的博文<C# 9.0 on the record> [1],Mads Torgersen 是微软 C# 语言的首席设计师,也是微 ...

  2. MSSQL 模糊搜索全文(过程、函数、触发器等)

    --SQL Server数据库查找含有某个关键字的存储过程.函数.触发器等 --SQL Server数据库查找含有某个关键字的存储过程,SQL语句如下: SELECT OBJECT_NAME(b.pa ...

  3. 1. 线性DP 53. 最大子序和.

    53. 最大子序和. https://leetcode-cn.com/problems/maximum-subarray/ func maxSubArray(nums []int) int { dp ...

  4. 使用GitHub API上传文件及GitHub做图床

    本文介绍GitHub API基础及上传文件到仓库API,并应用API将GitHub作为图床 GitHub API官方页面 GitHub API版本 当前版本为v3,官方推荐在请求头中显示添加版本标识. ...

  5. Nacos一致性算法

    1. CAP原则 又称CAP定理,指的是在一个分布式系统中,一致性(Consistency).可用性(Availability).分区容错性(Partition tolerance).CAP 原则指的 ...

  6. hive显示列名

    查询时显示列名:hive> set hive.cli.print.header;hive.cli.print.header=falsehive> set hive.cli.print.he ...

  7. NLP之统计句法分析(PCFG+CYK算法)

    一.认识句法分析 首先,了解一下句法分析到底是什么意思?是做什么事情呢?顾名思义,感觉是学习英语时候讲的各种句法语法.没错!这里就是把句法分析过程交给计算机处理,让它分析一个句子的句法组成,然后更好理 ...

  8. 二分查找 leetcode704

    class Solution {    public int search(int[] nums, int target) {        int l=0;        int r=nums.le ...

  9. PHP获取数组中重复值的键值

    $array = array ( 0=>'a', 1=>'b', 2=>'a', 5=>'b', 6=>'c', 40=>'d' ); $keyarr =[];$r ...

  10. img标签到底是行内元素还是块级元素

    面试官问你<img>是什么元素时你怎么回答 写这篇文章源自我之前的一次面试,题目便是问img标签属于块级元素还是行内元素,当时想都没想就说了是行内(inline)元素,面试官追问为什么能够 ...