作为一门面向过程、抽象化的通用程序设计语言,C 常被用于系统开发、游戏引擎等场景中。不过,近日 Rust、Swift 资深专家 Aria Beingessner 发布了一篇文章《C 不再是一种编程语言》的文章,在 Hacker News 上引起了开发者激烈的讨论。

Aria 和其朋友 Phantomderp 都一致认为,C 语言的 ABI 接口非常令人失望,并试图进行修复。尽管如此,但二人对 C 语言失望的点又各自不同,那具体产生了哪些分歧?笔者将对原文进行编译,一探究竟。

原文链接:https://gankra.github.io/blah/c-isnt-a-language/

HN:https://news.ycombinator.com/item?id=30704642

整理 | 于轩

出品 | 程序人生 (ID:coder _life)

在文章伊始,Aria表示其正在尝试从实质上改善使用C以外的任何语言的条件,而Phantomderp试图从本质上改善使用C本身作为编程语言的条件。

也许大家会非常好奇,以上和C语言到底有什么关系?

Aria认为,如果C真的是一种编程语言,那就和它无关。不幸的是,它并不再是一门编程语言了。这似乎与C语言由数十亿种实现方式和失败的层次结构,导致它的定义方式非常糟糕的事实有关。C已经被提升到一个具有威望和权力的角色,它的统治是绝对和永恒的,以至于它扭曲了开发者与该语言之间的对话方式。当下,C是编程的通用语言,我们都必须学C,这也导致C不再只是一种编程语言,它成了每一种通用编程语言都需要遵守的协议

这实际有点像是关于整个“C是一个不可捉摸的实现定义混乱” 。但仅因为它让我们不得不使用这个协议,这就变成了一个更大的噩梦。

1.外部功能接口

首先我们从技术层面来聊一下。假如你已经完成了新语言Bappyscript设计,对Bappy Paws/Hooves/Fins有一流的支持。这是一种神奇的语言,将彻底改变人们的编程方式。

但现在需要让它真正做一些有用的事情。比如接受用户输入、输出,或者字面上的任何可观察之类的东西。如果你想让该语言编写的程序与主流操作系统兼容,那就需要与操作系统的界面进行交互。听说Linux上的一切都“只是一个文件”,所以一起在Linux上打开一个文件吧!

OPEN(2)
NAMEopen, openat, creat - open and possibly create a file
SYNOPSIS#include <fcntl.h>int open(const char *pathname, int flags);int open(const char *pathname, int flags, mode_t mode);int creat(const char *pathname, mode_t mode);int openat(int dirfd, const char *pathname, int flags);int openat(int dirfd, const char *pathname, int flags, mode_t mode);/* Documented separately, in openat2(2): */int openat2(int dirfd, const char *pathname,const struct open_how *how, size_t size);Feature Test Macro Requirements for glibc (seefeature_test_macros(7)):openat():Since glibc 2.10:_POSIX_C_SOURCE >= 200809LBefore glibc 2.10:_ATFILE_SOURCE

这是Bappyscript,不是C,那Linux的Bappyscript接口在哪里?

你说Linux中没有Bappyscript接口是什么意思?好吧,当然是因为这是一种全新的语言,但你会添加一个,对吗?那这时你就会发现,你好像必须使用他们给的东西。

你将需要某种接口,让语言能够调用外部函数,就像外部函数接口FFI。然后你发现Rust也有C FFI,Swift也有,甚至Python也有。

最后你会发现,每个人都必须学会C才能与主流的操作系统进行交互,然后当需要相互对话时,大家突然都用起了C。既然如此,为什么不直接用C来进行交互呢?

现在C就变成了一种编程通用语言,不仅是一种编程语言,它还是一种协议了。

2.与C进行交互涉及哪些内容?

很明显,基本上每种语言都必须学会与C进行交互,而且这种语言绝对是非常明确的。

"Talking"C是什么意思?它意味着以C头文件的形式获得接口类型和功能的描述,并以某种方式:

  • 匹配这些类型的布局

  • 用链接器做一些事情,将函数的符号解析为指针

  • 用适当的ABI来调用这些函数(比如把args放在正确的寄存器中)

那么,这里就有几个问题:

  • 你实际上不能写一个C解析器

  • C实际上没有ABI,甚至没有定义的类型布局

3.实际上无法解析一个C头文件

Aria曾断言解析C基本上是不可能的,但有人说其实有很多工具可以读取C头文件,比如rust-bindgen。事实果真如此吗?其实不然。

bindgen使用libclang来解析C和C++头文件。要修改bindgen搜索libclang的方式,请参阅clang-sys文档。关于bindgen如何使用libclang的更多细节,请参阅bindgen用户指南。

任何花费大量时间试图快速解析C(++)头文件的人都会很快放弃,然后让一个C(++)编译器来做这件事。请记住,有意义地解析C头文件不仅仅是解析:你还需要解决#includes、typedefs和macros的问题!所以现在不仅要实现所有相关功能,还要实现所有平台的头文件解析逻辑,并且还需要想方设法找到DEFINED!

就拿Swift来说,它在与C进行互操作和资源方面拥有绝对优势,它是由苹果开发的一门编程语言,有效取代了Objective-C,成为在其平台上定义和使用系统API的主要语言。在这样做的过程中,它比其他任何人都更想进一步实现ABI稳定和概念设计。

它也是Aria见过的最支持FFI的语言之一。它可以本地导入(Objective-)C(++)头文件,并产生一个漂亮的本地Swift接口,其类型在边界自动 "桥接 "到它们的Swift对等项(由于类型具有相同的ABI,所以通常是透明的)。

Swift的许多开发者同时也是构建和维护Clang和LLVM的开发人员。这些人都是C及其衍生品方面的世界顶级专家。Doug Gregor就是其中之一,他曾表达了对C FFI的看法:

可以看出,即使是Swift也不想花时间解析C(++)头文件。那么,如果你绝对不想让C编译器在编译时解析和解决头文件,你该怎么做呢?

你需要手工翻译!int64_t? 还是写i64. long…?什么是long?

4.C实际上没有ABI

好吧,这没有什么好惊讶的:C语言中的整数类型,为了 “可移植性”而被设计成大小不固定。我们可以认为CHAR_BIT很奇怪,但这也不能帮助我们了解long的大小和对齐方式。

有人说每个平台都有标准化的调用约定和ABI,确实有,而且它们通常定义了C中关键原语的布局(并且有些不只是用C类型来定义调用约定,参考AMD64 SysV)。

还有一个棘手的问题:架构并没有定义ABI,操作系统也没有。我们必须在一个特定的目标三元组上全力以赴,比如 “x86_64-pc-windows-gnu”(不要和 "x86_64-pc-windows-msvc "混淆)。经过测试,一共有176个三元组。


> rustc --print target-listaarch64-apple-darwin
aarch64-apple-ios
aarch64-apple-ios-macabi
aarch64-apple-ios-sim
aarch64-apple-tvos...
armv7-unknown-linux-musleabi
armv7-unknown-linux-musleabihf
armv7-unknown-linux-uclibceabihf
...
x86_64-uwp-windows-gnu
x86_64-uwp-windows-msvc
x86_64-wrs-vxworks
>_

ABI实在是太多了,因为测试中甚至没有用到所有不同的调用约定,如stdcall vs fastcall或aapcs vs aapcs-vfp。

但至少所有这些ABI和调用约定之类都可以用一种方便使用的机器可读格式获得。至少主流的C编译器在特定目标三元组的ABI上达成了一致! 当然有一些奇怪的jank C编译器,但Clang和GCC不是:


> abi-checker --tests ui128 --pairs clang_calls_gcc gcc_calls_clang...Test ui128::c::clang_calls_gcc::i128_val_in_0_perturbed_small        passed
Test ui128::c::clang_calls_gcc::i128_val_in_1_perturbed_small        passed
Test ui128::c::clang_calls_gcc::i128_val_in_2_perturbed_small        passed
Test ui128::c::clang_calls_gcc::i128_val_in_3_perturbed_small        passed
Test ui128::c::clang_calls_gcc::i128_val_in_0_perturbed_big          failed!
test 57 arg3 field 0 mismatch
caller: [30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 3A, 3B, 3C, 3D, 3E, 3F]
callee: [38, 39, 3A, 3B, 3C, 3D, 3E, 3F, 40, 41, 42, 43, 44, 45, 46, 47]
Test ui128::c::clang_calls_gcc::i128_val_in_1_perturbed_big          failed!
test 58 arg3 field 0 mismatch
caller: [30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 3A, 3B, 3C, 3D, 3E, 3F]
callee: [38, 39, 3A, 3B, 3C, 3D, 3E, 3F, 40, 41, 42, 43, 44, 45, 46, 47]...392 passed, 60 failed, 0 completely failed, 8 skipped

上面是Aria在Ubuntu 20.04 x64上运行的FFI abi-checker,她在这个相当重要的、表现良好的平台上测试了一些非常无聊的Demo。结果发现,一些整数参数在两个由Clang和GCC编译的静态库之间按值传递失败了!

Aria发现,Clang和GCC甚至不能就Linux x64上_int128的ABI达成一致。

Aria本来是为了检查rustc中的错误,没想到会在一个重要的、常用的ABI上发现两大主流C编译器的不一致。

5.试图驯服C

Aria认为,可怕的是对C头文件进行语义解析,只能由该平台的C编译器来完成。即使C编译器告诉了你类型和如何理解注释,但实际上你仍然不知道所有内容的大小/对齐/惯例。那如何与这些乱七八糟的东西进行互操作呢?Aria提供了两种选择。

第一个选择是完全放弃,将你的语言与C进行灵魂绑定,这可以是以下任何一种:

  • 用C(++)编写编译器/运行时,这样它就可以用C了

  • 让你的 "codegen "直接生成C(++),这样用户无论如何都需要一个C编译器

  • 将编译器建立在一个成熟的主要C编译器(Clang或GCC)之上

即使做到上面这些,也不会让你的编译器走多远,除非你的语言真的暴露了unsigned long long,否则你将继承C的巨大可移植性混乱。

这就让我们想到了第二个选择:撒谎、欺骗和偷窃。

如果这一切是无论如何都无法避免的灾难,你还不如开始手工翻译类型和接口定义,基本上就是我们每天在Rust中所做的事情。比如,人们使用rust-bindgen和friends自动化处理一些事,但很多时候,定义会被检查或手工调整。因为人们不想浪费时间去尝试Phantomderp的定制C构建系统可移植地工作。

在Rust中,Linux x64上的intmax_t是什么?

pub type intmax_t = i64;

在Nim中,Linux x64上的long long是什么?

clonglong {.importc: "long long", nodecl.} = int64

很多代码已经完全放弃将C保持在循环中,开始对核心类型的定义进行硬编码。毕竟,它们显然只是平台ABI的一部分!他们要改变intmax_t的大小吗?这显然是一个破坏ABI的变化!

那phantomderp正在研究的又是什么?

我们讨论过为何intmax_t不能被改变,因为如果我们从long long(64位整数)改为_int128_t(128位整数),某个地方的二进制会失控使用错误的调用约定/返回约定。但有没有一种方法,如果代码选择了它或其他东西,我们可以为较新的应用程序升级函数调用,而让旧应用程序保持不变?让我们编写一些代码,测试一下透明别名可以帮助ABI的想法。

Aria提出了她的疑问:编程语言如何处理这种变化?如何指定与哪个版本的 intmax_t交互?如果你有一些C头文件提到intmax_t,它使用的是哪个定义?

在此讨论具有不同ABI的平台的主要机制是目标三元组。你知道什么是目标三元组吗?你知道基本上涵盖了过去20年里所有主流桌面/服务器Linux发行版的 x86_64-unknown-linux-gnu包括什么吗?现在,虽然表面上可以针对这个目标进行编译,并得到一个在所有这些平台上都能“正常工作”的二进制文件,但Aria不相信有些程序会被编译成intmax_t大于int64_t

任何试图做出这种改变的平台都会成为一个新的x86_64-unknown-linux-gnu2目标三元组吗?如果任何针对x86_64-unknown-linux-gnu编译的东西都被允许在上面运行,这难道还不够吗?

6.在不破坏ABI的情况下更改签名

"那又怎样,C永远不会再有进步吗?"不!但也是!因为他们提供了糟糕的设计。

老实说,进行ABI兼容的修改是一种艺术形式。这种艺术的一部分就是准备工作。具体来说,如果你准备好了,做出不破坏ABI的修改就会容易得多。

正如phantomderp的文章所指出的,像glibc(gx86_64-unknown-linux-gnu中的gnu)早就明白了这一点,并使用符号版本化这样的机制来更新签名和API,同时为任何针对旧版本编译的人保留旧版本。

因此,如果你有int32_t my_rad_symbol(int32_t),你告诉编译器将其导出为my_rad_symbol_v1,那么任何根据这个头文件进行编译的人,都会在他们的代码中写上my_rad_symbol,但针对my_rad_symbol_v1链接。

然后当你决定实际上应该使用int64_t时,你可以把int64_t my_rad_symbol(int64_t)作为my_rad_symbol_v2 ,但保留旧的定义作为 my_rad_symbol_v1。任何针对较新版本头文件进行编译的人都会高兴地使用v2符号,而针对旧版本进行编译的人则继续使用v1!

但是你仍然有一个兼容性的问题:任何用新头文件编译的人都不能与库的旧版本进行链接,库的V1版本根本没有V2符号!因此,如果你想获得热门的新功能,你就要接受与旧系统的不兼容。

不过这并不是什么大问题,它只是让平台供应商感到难过,因为没有人能够立即使用他们花了这么多时间做的东西。你不得不推出一个闪亮的新功能,然后让大家等待它变得足够普遍和成熟。但为了人们愿意依赖它并中断对旧平台的支持(或者愿意为它实施动态检查和回退)时,你必须坐等几年。

如果你真的想让人们立即升级,那就要谈论向前兼容的问题。这让旧版本的东西以某种方式与他们没有概念的新功能一起工作。

7.在不破坏ABI的情况下更改类型

那除了可以改变一个函数的签名,还可以改变类型布局吗?Aria表示,这取决于你是如何暴露类型的。

C真正奇妙的一个特点是,它可以让你区分一个已知布局的类型和一个未知布局的类型。如果你只在C头文件中前向声明一个类型,那么任何与之交互的用户代码都不被“允许”知道该类型的布局,并且必须一直在指针后面不透明地处理它。

所以你可以做一个像MyRadType* make_val()use_val(MyRadType*)的API,然后使用同样的符号版本技巧来暴露make_val_v1use_val_v1符号,任何时候你想改变这个布局,你就在所有与该类型交互的东西上增加版本。类似地,你在MyRadTypeV1MyRadTypeV2和一些类型定义中保留了一些,以确保人们使用“正确”的类型。这样就可以在不同的版本之间改变类型的布局。

如果多个东西建立在你的库之上,然后开始用不透明类型相互交谈,坏事就会发生:

  • lib1: 制作一个API,接受MyRadType*并调用 use_val

  • lib2:调用 make_val并将结果传递给lib1

如果lib1和lib2针对库的不同版本进行了编译,那么make_val_v1就会被输入到use_val_v2中!你有两个选择来处理这个问题:

1.说这是被禁止的,责备那些无论如何都要这么做的人,然后伤心

2.以一种向前兼容的方式设计MyRadType,这样混合就可以了

常见的前向兼容技巧包括:

  • 保留未使用的字段供未来版本使用

  • MyRadType的所有版本都有一个共同的前缀,可以让你“检查”你所使用的版本

  • 拥有自定大小的字段,以便旧版本可以“跳过”新的部分

8.案例研究:MINIDUMP_HANDLE_DATA

微软是这种向前兼容的大师,甚至可以实现在架构之间保持布局兼容。Aria最近正在处理的一个例子是Minidumpapiset.h中的MINIDUMP_HANDLE_DATA_STREAM。

这个API描述了一个有版本的值列表。该列表以这种类型开始:


typedef struct _MINIDUMP_HANDLE_DATA_STREAM {ULONG32 SizeOfHeader;ULONG32 SizeOfDescriptor;ULONG32 NumberOfDescriptors;ULONG32 Reserved;
} MINIDUMP_HANDLE_DATA_STREAM, *PMINIDUMP_HANDLE_DATA_STREAM;

其中:

  • SizeOfHeader 是MINIDUMP_HANDLE_DATA_STREAM本身大小。如果他们需要在最后增加更多的字段,那也没关系,因为旧版本可以使用这个值来检测头的“版本”,也可以跳过任何他们不知道的字段。

  • SizeOfDescriptor是数组中每个元素的大小。这让你知道你有什么 "版本 "的元素,并跳过任何你不知道的字段。

  • NumberOfDescriptors 是数组长度

  • Reserved是一些额外的内存,无论如何他们决定保留在头文件中(Minidumpapiset.h非常谨慎,从不在任何地方进行填充,因为填充字节有未指定的值,而且它是一种序列化的二进制文件格式。我希望他们添加这个字段是为了使结构的大小是8的倍数,这样就不会有任何关于数组元素在标题之后是否需要填充的问题。这是在认真对待兼容性!)

而事实上,微软实际上有理由使用这种版本方案,并定义了两个版本的数组元素:

typedef struct _MINIDUMP_HANDLE_DESCRIPTOR {ULONG64 Handle;RVA TypeNameRva;RVA ObjectNameRva;ULONG32 Attributes;ULONG32 GrantedAccess;ULONG32 HandleCount;ULONG32 PointerCount;
} MINIDUMP_HANDLE_DESCRIPTOR, *PMINIDUMP_HANDLE_DESCRIPTOR;
typedef struct _MINIDUMP_HANDLE_DESCRIPTOR_2 {ULONG64 Handle;RVA TypeNameRva;RVA ObjectNameRva;ULONG32 Attributes;ULONG32 GrantedAccess;ULONG32 HandleCount;ULONG32 PointerCount;RVA ObjectInfoRva;ULONG32 Reserved0;
} MINIDUMP_HANDLE_DESCRIPTOR_2, *PMINIDUMP_HANDLE_DESCRIPTOR_2;// The latest MINIDUMP_HANDLE_DESCRIPTOR definition.
typedef MINIDUMP_HANDLE_DESCRIPTOR_2 MINIDUMP_HANDLE_DESCRIPTOR_N;
typedef MINIDUMP_HANDLE_DESCRIPTOR_N *PMINIDUMP_HANDLE_DESCRIPTOR_N;

这些结构的实际细节不是很有趣,除了:

  • 他们只是通过在末尾添加字段来改变它

  • 有一个“最新版本”的类型定义

  • 保留了一些也许再次Padding(填充)(RVA是一个ULONG32)

这是一个坚不可摧的向前兼容的庞然大物。它们对填充非常小心,它甚至在32位和64位之间有相同的布局 (这实际上是非常重要的,因为你希望一个架构上的minidump处理器能够处理来自每个架构的minidump)。

9.案例研究:jmp_buf

Aria对这种情况不是很熟悉,但在研究历史上的glibc中断时,她在LWN上看到了一篇很棒的文章:《glibc s390 ABI中断》,她假设它是准确的。

事实证明,glibc曾经破解过类型的ABI,至少在s390上。根据这篇文章的描述,它是混乱的。

特别是他们改变了setjmp/longjmp使用的保存状态类型的布局,即jmp_buf 。现在,他们知道这是一个破坏ABI的变化,所以他们做了负责任的符号版本化的事情。

jmp_buf并不是一个不透明的类型,其他东西都在内联地存储这个类型的实例,比如Perl的运行时间。不用说,这个相对晦涩的类型已经渗透到许多二进制文件中去了,最终的结论是,Debian的所有东西都需要重新编译!

这篇文章甚至讨论了将libc版本升级以应对这种情况的可能性:

在像debian这样的混合ABI环境中,SO名称碰撞导致两个libc被加载并争夺相同的符号命名空间,而解析(以及因此选择ABI)则由ELF插值和范围规则决定。这真是一场噩梦。这可能是一个比告诉大家重建并继续生活更糟糕的解决方案。

10.真的能改变intmax_t吗?

在Aria看来,不完全是。就像jmp_buf一样,它不是一个不透明的类型,这意味着它被内联到大量的随机结构中,被认为具有大量其他语言和编译器的特定表示,并且可能是大量公共接口的一部分。而这些接口并不在libc、Linux,甚至不在发行版维护者的控制之下。

当然,libc可以适当地使用符号版本技巧来使其API与新的定义兼容,但改变像 intmax_t这样的基本数据类型的大小,是在一个平台的大生态系统中寻求混乱。

Aria希望被证明自己是错误的,但据她所知,做出这样的改变需要一个新的目标三元组,并且不允许任何为旧ABI构建的二进制/库在这个新三元组上运行。当然有人可以做这些工作,但Aria并不羡慕任何这样做的发行版。

即使如此,面临的还有x64的int问题:这是一个非常基本的类型,而且长期以来一直是这种大小,无数的应用程序可能对它有奇怪的无法察觉的假设。这就是为什么int在x64上是32位的,尽管它应该是64位的:int是32位的时间太长了,以至于完全无望将软件更新到新的大小,尽管它是一个全新的架构和目标三元组。

Aria再次希望自己是错的,但是人们有时犯的错误如此严重,以至于根本无法挽回。如果C语言是一种独立的编程语言?当然可以去做。但它不是,它是一个协议,还是我们必须使用的糟糕的协议。

就算C征服了世界,但也许它再也得不到好东西了。

“C 不再是一种编程语言”相关推荐

  1. C 不再是一种编程语言

    近日,Rust和Swift资深专家Aria Beingessner发布的一篇文章<C 不再是一种编程语言>在Hacker News上引起了热烈讨论. 原文链接:C Isn't A Prog ...

  2. “C不再是一种编程语言”

    本文标题里的观点很"刺激",它来自国外一位 Swift 和 Rust 专家 Aria Beingessner,他近日撰写了一篇文章<C 不再是一种编程语言>,在技术社区 ...

  3. python语言开发的软件有哪些-最适合人工智能开发的5种编程语言,你知道几种?...

    原标题:最适合人工智能开发的5种编程语言,你知道几种? 私信我或关注微信号:猿来如此呀,回复:学习,获取免费学习资源包. 人工智能技术的提升不仅为企业的运营带来了效率,而且为人民的生活带来了便利. 迄 ...

  4. python与人工智能编程-最适合人工智能开发的5种编程语言,Python排第一

    原标题:最适合人工智能开发的5种编程语言,Python排第一 文章转载自超图集团,版权归原作者和刊载媒体所有. 近年来,人工智能(AI)正在不断释放科技革命和产业变革积蓄的巨大能量,深刻改变着人类生产 ...

  5. python与人工智能编程-最适合人工智能开发的5种编程语言,你知道几种?

    原标题:最适合人工智能开发的5种编程语言,你知道几种? 私信我或关注微信号:猿来如此呀,回复:学习,获取免费学习资源包. 人工智能技术的提升不仅为企业的运营带来了效率,而且为人民的生活带来了便利. 迄 ...

  6. 老而不死的三种编程语言!

    在软件世界中,铁打的二进制,流水的语言.从计算机诞生至今,不知诞生了多少门编程语言.译者查了一下 Wikipedia,好家伙,名单上足足有几百种!但并不是所有的语言都能长期占据 Top 10.今天我们 ...

  7. kotlin和java语言_Kotlin VS Java – 2020年您应该学习哪种编程语言?

    kotlin和java语言 It has been several years since Kotlin came out, and it has been doing well. Since it ...

  8. 十年后可能消失的五种编程语言

    本文作者从自己的观点出发,介绍了未来 20 年内可能消失的五种编程语言,并给出了具体的原因.最后对想要学习编程的初学者给出了学习建议. >>>> 随着时间的流逝,程序员们发现了 ...

  9. 为啥有人说每个程序员都应该这学习5种编程语言?

    1.Java Java是过去20年中最流行的语言之一,它控制着服务器端应用程序开发的世界.它还在使用Android的移动游戏和应用程序开发以及企业Web开发领域占有相当大的市场份额. Java是从WO ...

最新文章

  1. Python3 django2.0 字段加密 解密 AES
  2. PIC模拟从入门到熟练系列之组会PPT20210913《Note of PIC》
  3. 方法:查询MongoDB数据库中最新一条数据(JAVA)
  4. 移动端页面(响应式)
  5. 面向对象chapter10
  6. python多线程实现同时下载_Python实现多线程下载
  7. ppt设置外观样式_ppt设置图片样式_PPT如何快速修改图像样式图文教
  8. SEO原创助手-SEO免费原创助手工具自动分析网站排名
  9. python中减号怎么打_python减号
  10. 电脑开机自动弹出广告如何解决
  11. 关于数学计算机手抄报简单的,数学手抄报简单又漂亮图片
  12. 可扩展标记语言XML(淅淅沥沥的小雨)
  13. vue导致页面白屏几种情况
  14. ActiveMQ的简单使用
  15. 风雨成长路——2015上半年年总
  16. 计算机发明者的电影,电脑发明的奠基人之一,你了解他的生平吗?这部电影带你走近天才...
  17. canal工作原理及简单案例演示
  18. 使用Xshell实现SSH隧道穿透
  19. 双击jar包运行,出现使用压缩软件打开的情况
  20. 信息安全意识培训详解

热门文章

  1. Java获取函数参数名称
  2. 深度学习之Loss不下降原因分析篇
  3. AcWing 1208. 翻硬币
  4. Socket 连接建立过程
  5. 面试—每日一题(3)
  6. SQL SERVER数据库设计与现实
  7. [Swift]LeetCode1044. 最长重复子串 | Longest Duplicate Substring
  8. T-SQL的进阶:超越基本级别3:构建相关子查询——701小组
  9. C#刨根究底:《你必须知道的.NET》读书笔记系列
  10. 迟到的,2016年终总结