可以转载,请注明出处!

以下内容是参照官方文档,对一些代码样例做出的解释,想要彻底掌握IR的语法规则,还是需要仔细熟读IR的官方文档。这里只是对IR的入门介绍,等上道之后,建议再去看官方文档,因为工作原因,后面我可能会按照自己的结构划分对官网上的主要内容做一个介绍,搞一个专栏:

  • LLVM IR官方文档地址:http://llvm.org/docs/LangRef.html#br-instruction
  • 参考官方文档,我按照自己的理解外加翻译,对常用指令、类型、函数、变量等等做了一个总结。后面我会随着自己对llvm的理解加深,在这个专栏一直持续弥补改正、并更新新的内容。https://blog.csdn.net/qq_42570601/category_10200372.html
  • 发现一个翻译不错的中文网站:https://llvm.liuxfe.com/

1.基本语法介绍

用vim编辑一个c程序代码:

#include <stdio.h>int main()
{int a = 10;int b = 11;return a + b;
}

将c的源码转为LLVM IR,代码如下:

; ModuleID = 'test3.c'
source_filename = "test3.c"
target datalayout = "e-m:e-i64:64-f80:128-n8:16:32:64-S128"
target triple = "x86_64-unknown-linux-gnu"; Function Attrs: noinline nounwind optnone uwtable
define i32 @main() #0 {entry:%retval = alloca i32, align 4%a = alloca i32, align 4%b = alloca i32, align 4store i32 0, i32* %retval, align 4store i32 10, i32* %a, align 4store i32 11, i32* %b, align 4%0 = load i32, i32* %a, align 4%1 = load i32, i32* %b, align 4%add = add nsw i32 %0, %1ret i32 %add
}attributes #0 = { noinline nounwind optnone uwtable "correctly-rounded-divide-sqrt-fp-math"="false" "disable-tail-calls"="false" "less-precise-fpmad"="false" "no-frame-pointer-elim"="true" "no-frame-pointer-elim-non-leaf" "no-infs-fp-math"="false" "no-jump-tables"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "no-trapping-math"="false" "stack-protector-buffer-size"="8" "target-cpu"="x86-64" "target-features"="+fxsr,+mmx,+sse,+sse2,+x87" "unsafe-fp-math"="false" "use-soft-float"="false" }!llvm.module.flags = !{!0}
!llvm.ident = !{!1}!0 = !{i32 1, !"wchar_size", i32 4}
!1 = !{!"clang version 6.0.0 (tags/RELEASE_600/final)"}

根据官方文档的描述:
注解是以 ; 分隔且直到当前行的结尾,所以; Function Attrs: noinline nounwind optnone uwtable这一行是注解;
@代表全局标识符(函数,全局变量);
%代表局部标识符(寄存器名称也就是局部变量,类型)。
所以在llvm IR看来,int main这个函数,或者说他的函数返回值是个全局变量,其内部的a 和b是局部变量。

define i32 @main() #0 {entry:
...
ret i32 %add
}

上面IR是定义一个函数main,函数的返回值类型是i32;#0是后面attributes 属性组中的属性,暂时先不用管;每个函数的定义都会包含一个基本块(BasicBlock),entry是基本块的开始,ret i32 %add是返回类型为i32,名称为 %add的变量中存放的函数的值,也就是基本块结束;
i32:32位的整数,对应c中的int类型,i后面跟几,这个整数就会占几位(bit),i32的话就是32位,4个字节;i后面的数字可以随意写,可以得知这其实是在设置整数位的长度。

  %retval = alloca i32, align 4%a = alloca i32, align 4%b = alloca i32, align 4

alloca指令的官方解释大致意思是:用于分配内存堆栈给当前执行的函数,当这个函数返回其调用者时自动释放。这里就是给%retval变量分配一个4byte的内存,待变量不再使用后,将内存释放,有点c语言中声明一个变量的意思,也有点像c中malloc。
align 4:在官方文档中没找到align的解释,我对他的理解是“对齐方式”:若一个结构中含有两个int,一个char,则他应该占用4*3=12字节,虽然char本身只占用一个字节的空间,但由于要向4“对齐”所以即便是数据没有4个字节,也要为其分配4个字节。所以这里的对齐方式为4个字节。

  store i32 0, i32* %retval, align 4store i32 10, i32* %a, align 4store i32 11, i32* %b, align 4

store指令官方的解释大致是,将数据写入到指定的内存中。所以这里很好理解,将i32类型的整数10存放到变量(寄存器名称)%a对应的内存中,对齐方式是4byte。

  %0 = load i32, i32* %a, align 4%1 = load i32, i32* %b, align 4

load指令官方解释大致意思是,读取指定内存中的数据。这里的意思是读取变量%a对应的内存中的数据,将其存放到类型为i32的零时变量%0中,%0的对齐方式为4byte。load后面紧跟的类型是有限制的,必须为frist class type,这个后面熟悉了可以看官方文档。

  %add = add nsw i32 %0, %1

add指令是一个二元运算符,返回它对应的两个操作数的和,操作数也是有要求的,必须为整数或整数值向量,且两个操作数的类型必须要相同。有一个fadd指令,也是求两个操作数的和,只不过对操作数的限制是必须为浮点数或者浮点值向量,且操作数类型相同。add加,sub减,mul乘,div除,rem取余,官网对这一块列举的很详细,也很全面,后面对代码熟悉了可以直接看文档。

2.if语句介绍

c程序代码:

#include <stdio.h>int main()
{int a = 10;if(a%2 == 0)return 0;else return 1;
}

LLVM IR代码:

define i32 @main() #0 {entry:%retval = alloca i32, align 4%a = alloca i32, align 4store i32 0, i32* %retval, align 4store i32 10, i32* %a, align 4%0 = load i32, i32* %a, align 4%rem = srem i32 %0, 2%cmp = icmp eq i32 %rem, 0br i1 %cmp, label %if.then, label %if.elseif.then:                                          ; preds = %entrystore i32 0, i32* %retval, align 4br label %returnif.else:                                          ; preds = %entrystore i32 1, i32* %retval, align 4br label %returnreturn:                                           ; preds = %if.else, %if.then%1 = load i32, i32* %retval, align 4ret i32 %1
}

上面代码出现的新的指令主要有三个,icmp、br、label。srem不算新的指令,上一个例子中已经说过了,取余运算。

  %cmp = icmp eq i32 %rem, 0

icmp指令,根据比较规则,比较两个操作数,将比较的结果以布尔值或者布尔值向量(vector of boolean values,暂且就这么叫吧,也不知对不对)返回,且对于操作数的限定是操作数为整数或整数值向量、指针或指针向量。在这里,eq是比较规则,%rem和0是操作数,i32是操作数类型,比较%rem与0的值是否相等,将比较的结果存放到%cmp中。

  br i1 %cmp, label %if.then, label %if.else

br指令有两种形式,分别对应于条件分支和无条件分支。该指令的条件分支在形式上接受一个“i1”值和两个“label”值,用于将控制流传输到当前函数中的不同基本块,上面这条指令是条件分支,有点像c中的三目条件运算符< expression ?Statement :statement>;无条件分支的话就是不用判断,直接跳转到指定的分支,有点像c中goto,比如说这个就是无条件分支br label %return。上面指令的意思是,i1类型的变量%cmp的值如果为真,执行if.then,否则执行if.else。
官网对label的划分与解释是,label是类型系统(type system)中的第一类类型(first class type),所以label并不是一条指令,First class type应该是IR中最重要的类型了,所有的指令产生的值都是first class type值。我这里把label理解成一个代码标签,作为label %if.then这条分支的入口。

总结一下if条件语句:

  • 求出if语句表达式的两个操作数的值(也有可能是一个eg:a > 0);
  • icmp指令开始比较,会产生一个布尔结果值;
  • br指令的条件分支利用上一步产生的值,跳转到相对应的分支入口;
  • 分支执行完再用br指令的无条件分支跳到if的结束分支。

3.While语句介绍

c程序代码:

#include <stdio.h>int main()
{int a = 0, b = 1;while(a < 5){a++;b *= a;}return b;
}

LLVM IR代码:

define i32 @main() #0 {entry:%retval = alloca i32, align 4%a = alloca i32, align 4%b = alloca i32, align 4store i32 0, i32* %retval, align 4store i32 0, i32* %a, align 4store i32 1, i32* %b, align 4br label %while.condwhile.cond:                                       ; preds = %while.body, %entry%0 = load i32, i32* %a, align 4%cmp = icmp slt i32 %0, 5br i1 %cmp, label %while.body, label %while.endwhile.body:                                       ; preds = %while.cond%1 = load i32, i32* %a, align 4%inc = add nsw i32 %1, 1store i32 %inc, i32* %a, align 4%2 = load i32, i32* %a, align 4%3 = load i32, i32* %b, align 4%mul = mul nsw i32 %3, %2store i32 %mul, i32* %b, align 4br label %while.condwhile.end:                                        ; preds = %while.cond%4 = load i32, i32* %b, align 4ret i32 %4
}

对比if语句可以发现,while中几乎没有新的指令出现,所以说所谓的while循环,也就是“跳转+分支”这一结构。
While的运行流程是:首先跳到while.cond: 相关变量得到初始值后判断是否满足继续循环条件,若满足,就转到while.body: 进行循环实际操作,一次实际操作运行完后再次跳到while.cond:进行条件判断,如此循环~;若否,则直接跳到 while.end: 终止循环;

4.switch语句

对应C的伪代码:

int main (){char grade = 'B';int score;switch(grade){case 'A' :score = 4;break;case 'B' :score = 3;break;case 'C' :score = 2;break;case 'D' :score = 1;break;default :score = 0;}printf("your score: %d\n", score );return 0;
}

IR代码:

@.str = private unnamed_addr constant [16 x i8] c"your score: %d\0A\00", align 1define i32 @main(){entry:%grade = alloca i8, align 1%score = alloca i32, align 4store i8 66, i8* %grade, align 1%0 = load i8, i8* %grade, align 1%conv = sext i8 %0 to i32switch i32 %conv, label %sw.default [i32 65, label %sw.ai32 66, label %sw.bi32 67, label %sw.ci32 68, label %sw.d]sw.a: store i32 4, i32* %score, align 4br label %sw.endsw.b: store i32 3, i32* %score, align 4br label %sw.endsw.c: store i32 2, i32* %score, align 4br label %sw.endsw.d:store i32 1, i32* %score, align 4br label %sw.endsw.default: store i32 0, i32* %score, align 4br label %sw.endsw.end:%1 = load i32, i32* %score, align 4%call = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([16 x i8], [16 x i8]* @.str, i32 0, i32 0), i32 %1)ret i32 0
}declare i32 @printf(i8*, ...)

这里新出来了switch 指令,switch指令是一个终端指令,同br、ret一样,用在一个基本快的结束。也没啥难度,就是选一个与%conv值相等的基本快标签跳进去。

5.对指针的操作

对应C的伪代码:

int main(){int i = 10;int* pi = &i;printf("i的值为:%d",i);printf("*pi的值为:%d",*pi);printf("&i的地址值为:",%d);printf("pi的地址值为:",%d);
}

IR代码:

@.str = private unnamed_addr constant [16 x i8] c"i\E7\9A\84\E5\80\BC\E4\B8\BA\EF\BC\9A%d\00", align 1
@.str.1 = private unnamed_addr constant [18 x i8] c"*pi\E7\9A\84\E5\80\BC\E4\B8\BA\EF\BC\9A%d\00", align 1
@.str.2 = private unnamed_addr constant [23 x i8] c"&i\E7\9A\84\E5\9C\B0\E5\9D\80\E5\80\BC\E4\B8\BA\EF\BC\9A%p\00", align 1
@.str.3 = private unnamed_addr constant [23 x i8] c"pi\E7\9A\84\E5\9C\B0\E5\9D\80\E5\80\BC\E4\B8\BA\EF\BC\9A%p\00", align 1define i32 @main(){entry:%i = alloca i32, align 4%pi = alloca i32*, align 8store i32 10, i32* %i, align 4store i32* %i, i32** %pi, align 8%0 = load i32, i32* %i, align 4%call = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([16 x i8], [16 x i8]* @.str, i32 0, i32 0), i32 %0)%1 = load i32, i32* %i, align 4%call1 = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([18 x i8], [18 x i8]* @.str.1, i32 0, i32 0), i32 %1)%call2 = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([23 x i8], [23 x i8]* @.str.2, i32 0, i32 0), i32* %i)%2 = load i32*, i32** %pi, align 8%call3 = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([23 x i8], [23 x i8]* @.str.3, i32 0, i32 0), i32* %2)ret i32 0
}declare i32 @printf(i8*, ...)

对指针的操作就是指针的指针,开辟一块指针类型的内存,里面放个指针%pi = alloca i32*, align 8,如果你是从头看下来的,相信这里不会有啥问题。需要注意的是alloca 指令产生的是一个指针,所以才有上一句所说的指针的指针。这些内容属于C的知识,如果不理解的话百度一下,很简单的。

6.对数组的操作

对应C的伪代码:

int main(){char str[30];char c;int i;for(c=65,i=0; c<=90; c++,i++){str[i] = c;}printf("%s\n", str);return 0;
}

IR代码:

@.str = private unnamed_addr constant [4 x i8] c"%s\0A\00", align 1define i32 @main() {entry:%str = alloca [30 x i8], align 16%c = alloca i8, align 1%i = alloca i32, align 4store i8 65, i8* %c, align 1store i32 0, i32* %i, align 4br label %for.condfor.cond:%0 = load i8, i8* %c, align 1%sext = sext i8 %0 to i32%cmp = icmp sle i32 %sext, 90br i1 %cmp, label %for.body, label %for.endfor.body:%1 = load i8, i8* %c, align 1%2 = load i32, i32* %i, align 4%array = getelementptr inbounds [30 x i8], [30 x i8]* %str, i32 0, i32 %2store i8 %1, i8* %array, align 1%add = add i8 %1, 1store i8 %add, i8* %c, align 1%3 = load i32, i32* %i, align 4%add2 = add nsw i32 %3, 1store i32 %add2, i32* %i, align 4br label %for.condfor.end:%arraydecay = getelementptr inbounds [30 x i8], [30 x i8]* %str, i32 0, i32 0%call = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([4 x i8], [4 x i8]* @.str, i32 0, i32 0), i8* %arraydecay)ret i32 0
}declare i32 @printf(i8*, ...)

数组新增了两个不同的地方:%str = alloca [30 x i8], align 16%arraydecay = getelementptr inbounds [30 x i8], [30 x i8]* %str, i32 0, i32 0
第一条分配内存的指令alloca [30 x i8]同first class类型一样理解,就是分配了30个i8类型的连续空间。
第二条是一个很重要的指令,首次看可能不是特别好理解,直接看指令介绍,的“内存访问和寻址”的“getelementptr指令”介绍。

7.对结构体的操作

对应C的伪代码:

struct Grade{int number;
}struct Stu{char *name;int age;char group;float score;struct Grade grade1;
};int main(){struct Stu stu1;stu1.name = "Tom";stu1.age = 18;stu1.group = 'A';stu1.score = 136.5;stu1.grade1.number = 4;printf("%s,%d,%c,%.1f, %d\n", stu1.name,stu1.age, stu1.group, stu1.score, stu1.grade1.number);return 0;
}

IR代码:

%struct.grade = type{ i32 }
%struct.Stu = type { i8*, i32, i8, float, %struct.grade}@str.name = private unnamed_addr constant [4 x i8] c"Tom\00", align 1
@str.print = private unnamed_addr constant [25 x i8] c"%s\EF\BC\8C%d\EF\BC\8C%c\EF\BC\8C%.1f, %d\0A\00", align 1define i32 @main(){entry:%stu1 = alloca %struct.Stu, align 8%name = getelementptr inbounds %struct.Stu, %struct.Stu* %stu1, i32 0, i32 0store i8* getelementptr inbounds ([4 x i8], [4 x i8]* @str.name, i32 0, i32 0), i8** %name, align 8%age = getelementptr inbounds %struct.Stu, %struct.Stu* %stu1, i32 0, i32 1store i32 18, i32* %age, align 8%group = getelementptr inbounds %struct.Stu, %struct.Stu* %stu1, i32 0, i32 2store i8 65, i8* %group, align 4%score = getelementptr inbounds %struct.Stu, %struct.Stu* %stu1, i32 0, i32 3store float 136.5, float* %score, align 8%grade = getelementptr inbounds %struct.Stu, %struct.Stu* %stu1, i32 0, i32 4, i32 0store i32 4, i32* %grade, align 4%0 = load i8*, i8** %name, align 8%1 = load i32, i32* %age, align 8%2 = load i8, i8* %group, align 4%sext = sext i8 %2 to i32%3 = load float, float* %score, align 8%4 = load i32, i32* %grade, align 4%fpext = fpext float %3 to double%call = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([25 x i8], [25 x i8]* @str.print, i32 0, i32 0), i8* %0, i32 %1, i32 %sext, double %fpext, i32 %4)ret i32 0
}declare i32 @printf(i8*, ...)

IR中对于结构体的使用同C中很相似,都是先声明一个类型,然后在使用这个类型利用alloca指令开辟内存。第1节基本语法介绍中也说过,%代表局部标识符(寄存器名称也就是局部变量,类型),其中类型就是这里的结构体。

%struct.grade = type{ i32 }
%struct.Stu = type { i8*, i32, i8, float, %struct.grade}

结构体这一块需要注意的是这一条指令,总共有三个索引,%grade = getelementptr inbounds %struct.Stu, %struct.Stu* %stu1, i32 0, i32 4, i32 0,还是直接看指令介绍,的“内存访问和寻址”的“getelementptr指令”介绍,里面都有。

8.引用内置函数

对应C的伪代码:

int main (){printf("值 8.0 ^ 3 = %lf\n", pow(8.0, 3));printf("值 3.05 ^ 1.98 = %lf", llvm.pow.f32(3.05, 1.98));return 0;
}

IR代码:

@.str = private unnamed_addr constant [19 x i8] c"\E5\80\BC 8.0 ^ 3 = %lf\0A\00", align 1
@.str.1 = private unnamed_addr constant [22 x i8] c"\E5\80\BC 3.05 ^ 1.98 = %lf\00", align 1declare i32 @printf(i8*, ...)
declare double @pow(double, double)
declare double  @llvm.pow.f32(double, double)define i32 @main(){entry:%pow1 = call double @pow(double 8.0, double 3.0)call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([19 x i8], [19 x i8]* @.str, i32 0, i32 0), double %pow1)%pow2 = call double @llvm.pow.f32(double 3.05, double 1.98)call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([22 x i8], [22 x i8]* @.str.1, i32 0, i32 0), double %pow2)ret i32 0
}

9.引用外部函数

对应C的伪代码:

int func(int a) {a = a*2;return a;
}----------------------------------------#include<stdio.h>extern int func(int a);int main() {int num = 5;num = func(num);printf("number is %d\n", num);return num;
}

IR代码:

define i32 @func(i32 %a){entry:%a.addr = alloca i32, align 4store i32 %a, i32* %a.addr, align 4%0 = load i32, i32* %a.addr, align 4%mul = mul nsw i32 %0, 2store i32 %mul, i32* %a.addr, align 4%1 = load i32, i32* %a.addr, align 4ret i32 %1
}-----------------------------------------------------@.str = private unnamed_addr constant [14 x i8] c"number is %d\0A\00", align 1; Function Attrs: noinline nounwind optnone uwtable
define i32 @main() #0 {entry:%retval = alloca i32, align 4%num = alloca i32, align 4store i32 0, i32* %retval, align 4store i32 5, i32* %num, align 4%0 = load i32, i32* %num, align 4%call = call i32 @func(i32 %0)store i32 %call, i32* %num, align 4%1 = load i32, i32* %num, align 4%call1 = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([14 x i8], [14 x i8]* @.str, i32 0, i32 0), i32 %1)%2 = load i32, i32* %num, align 4ret i32 %2
}declare i32 @func(i32)declare i32 @printf(i8*, ...)

链接后的IR:

@.str = private unnamed_addr constant [14 x i8] c"number is %d\0A\00", align 1define i32 @func(i32 %a){entry:%a.addr = alloca i32, align 4store i32 %a, i32* %a.addr, align 4%0 = load i32, i32* %a.addr, align 4%mul = mul nsw i32 %0, 2store i32 %mul, i32* %a.addr, align 4%1 = load i32, i32* %a.addr, align 4ret i32 %1
}define i32 @main(){entry:%num = alloca i32, align 4store i32 5, i32* %num, align 4%0 = load i32, i32* %num, align 4%call = call i32 @func(i32 %0)store i32 %call, i32* %num, align 4%1 = load i32, i32* %num, align 4%call1 = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([14 x i8], [14 x i8]* @.str, i32 0, i32 0), i32 %1)%2 = load i32, i32* %num, align 4ret i32 %2
}declare i32 @printf(i8*, ...)

简单了解LLVM IR基本语法相关推荐

  1. LLVM IR入门指南(4)——类型系统

    我们知道,汇编语言是弱类型的,我们操作汇编语言的时候,实际上考虑的是一些二进制串.但是,LLVM IR却是强类型的,在LLVM IR中所有变量都必须有类型.这是因为,我们在使用高级语言编程的时候,往往 ...

  2. LLVM IR 语法

    译者序     目前几乎没有关于LLVM IR(中间语言)的中文资料,因此本人在看英文手册的同时尝试翻译.限于水平和时间,本文只翻译了一小部分英文手册,如果发现理解有冲突之处,请以原文为准.     ...

  3. 手写token解析器、语法解析器、LLVM IR生成器(GO语言)

    最近开始尝试用go写点东西,正好在看LLVM的资料,就写了点相关的内容 - 前端解析器+中间代码生成(本地代码的汇编.执行则靠LLVM工具链完成) https://github.com/daibinh ...

  4. LLVM一些编程语法语义特性

    LLVM一些编程语法语义特性 High Level Structure Module Structure LLVM 程序由Module's组成,每个 's 是输入程序的一个翻译单元.每个模块由函数.全 ...

  5. LLVM IR 理解

    LLVM IR 理解 LLVM IR表示 LLVM IR有三个不同的形式: 内存中编译中间语言(IR) 保存在硬盘上的 bitcode(.bc 文件,适合快速被一个 JIT 编译器加载) 一个可读性的 ...

  6. LLVM极简教程: 第三章 LLVM IR代码生成

    第三章 LLVM IR代码生成 原文: Code generation to LLVM IR 本章简介 欢迎进入"用LLVM开发新语言"教程的第三章.本章将介绍如何把第二章中构造的 ...

  7. LLVM IR格式的基本介绍

    LLVM IR以两种格式存储在磁盘上: 1.位码(.bc文件) 2.汇编文本(.ll文件) 以sum.c源代码为例 int sum(int a, int b){return a+b; } 使用Clan ...

  8. 修改的LLVM IR基本指令

    SysY2022语言定义中不包含无符号整数.结构体.移位操作,整数和浮点数均为32位,比赛测试样例不包含错误.鉴于SysY2022语言的特点,为了IR的简洁,对LLVM IR进行筛选和修改得到如下指令 ...

  9. LLVM框架/LLVM编译流程/Clang前端/LLVM IR/LLVM应用与实践-李明杰-专题视频课程

    LLVM框架/LLVM编译流程/Clang前端/LLVM IR/LLVM应用与实践-3人已学习 课程介绍         LLVM并非仅仅是一款编译器这么简单.利用LLVM,我们可以进行各种疯狂的操作 ...

最新文章

  1. 天命剑之天命的含义--天行有悖,乃命羲和。
  2. redis界面管理工具phpRedisAdmin 安装
  3. MVC 学习日志1(上)
  4. lower_bound upper_bound
  5. 如何设置Matlab输出到Word中图片的大小
  6. 雷林鹏分享:C# 匿名方法
  7. PyTorch 1.0 中文文档:多进程包 - torch.multiprocessing
  8. 不采用服务器虚拟化的优缺点,为什么要进行虚拟化部署?虚拟化的缺点是什么?...
  9. 别点进来! Linux 与 Mac 下有趣但毫无用处的命令(转载)
  10. IDEA常用快捷键总结(附导入其他IDE快捷键)
  11. 中兴b860修改mac_【原创】猫盘群晖超级简单修改【SN MAC】 工具
  12. Hadoop生态圈:19个让大象飞起来的工具!
  13. MYSQL数据库导出和备份----mysqldump
  14. webpack配置完全指南
  15. yy神曲url解析php_歪歪神曲解析源码(参考)
  16. 全球KYC服务商ADVANCE.AI顺利加入深跨协 推动跨境电商行业有序发展
  17. Redis跳跃表(SkipList)
  18. 谈谈我的单片机编程思路
  19. 大规模中文文本处理中的自动切词和标注技术
  20. 基于LabVIEW编程的气象监测系统

热门文章

  1. (一)操作系统的基本概念
  2. 一杯茶的时间,上手 Docker
  3. .NET程序中加入Autodesk Design Review 2013 ,打包完成后安装提示未注册XXX.dll解决办法
  4. air 新浪开放平台 登录部分接口案例
  5. IOS开发协议使用之──非正式协议和正式协议
  6. PTA乙级1028 人口普查
  7. Tryhackme-Starters
  8. 可供参考的互联网电商订单中心设计
  9. “排队” 用英语怎么说
  10. 中国健身、俱乐部和健身房管理软件系统行业市场供需与战略研究报告