2017年8月

不要让 git 命令中的错误抹去你数天的工作

 title=

今天我的同事几乎失去了他在四天工作中所做的一切。由于不正确的 git 命令,他把保存在 stash 中的更改删除了。在这悲伤的情节之后,我们试图寻找一种恢复他所做工作的方法,而且我们做到了!

首先警告一下:当你在实现一个大功能时,请将它分成小块并定期提交。长时间工作而不做提交并不是一个好主意。

现在我们已经搞定了那个错误,下面就演示一下怎样从 stash 中恢复误删的更改。

我用作示例的仓库中,只有一个源文件 “main.c”,如下所示:

 title=

它只有一次提交,即 “Initial commit”:

 title=

该文件的第一个版本是:

 title=

我将在文件中写一些代码。对于这个例子,我并不需要做什么大的改动,只需要有什么东西放进 stash 中即可,所以我们仅仅增加一行。“git diff” 的输出如下:

 title=

现在,假设我想从远程仓库中拉取一些新的更改,当时还不打算提交我自己的更改。于是,我决定先 stash 它,等拉取远程仓库中的更改后,再把我的更改恢复应用到主分支上。我执行下面的命令将我的更改移动到 stash 中:

git stash

使用命令 git stash list 查看 stash,在这里能看到我的更改:

 title=

我的代码已经在一个安全的地方,而且主分支目前是干净的(使用命令 git status 检查)。现在我只需要拉取远程仓库的更改,然后把我的更改恢复应用到主分支上,而且我也应该是这么做的。

但是我错误地执行了命令:

git stash drop

它删除了 stash,而不是执行了下面的命令:

git stash pop

这条命令会在从栈中删除 stash 之前应用它。如果我再次执行命令 git stash list,就能看到在没有从栈中将更改恢复到主分支的之前,我就删除了它。OMG!接下来怎么办?

好消息是:git 并没有删除包含了我的更改的对象,它只是移除了对它的引用。为了证明这一点,我使用命令 git fsck,它会验证数据库中对象的连接和有效性。这是我对该仓库执行了 git fsck 之后的输出:

 title=

由于使用了参数 --unreachable,我让 git-fsck 显示出所有不可访问的对象。正如你看到的,它显示并没有不可访问的对象。而当我从 stash 中删除了我的更改之后,再次执行相同的指令,得到了一个不一样的输出:

 title=

现在有三个不可访问对象。那么哪一个才是我的更改呢?实际上,我不知道。我需要通过执行命令 git show 来搜索每一个对象。

 title=

就是它!ID 号 95ccbd927ad4cd413ee2a28014c81454f4ede82c 对应了我的更改。现在我已经找到了丢失的更改,我可以恢复它。其中一种方法是将此 ID 取出来放进一个新的分支,或者直接提交它。如果你得到了你的更改对象的 ID 号,就可以决定以最好的方式,将更改再次恢复应用到主分支上。对于这个例子,我使用 git stash 将更改恢复到我的主分支上。

git stash apply 95ccbd927ad4cd413ee2a28014c81454f4ede82c

另外需要重点记住的是 git 会周期性地执行它的垃圾回收程序(gc),它执行之后,使用 git fsck 就不能再看到不可访问对象了。

本文最初发表于作者的博客,并得到了转载授权。

(题图:opensource.com,附图:José Guilherme Vanz, CC BY


via: https://opensource.com/article/17/8/recover-dropped-data-stash

作者:Jose Guilherme Vanz 译者:firmianay 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

非常有用的 csplit 命令可以将单个文件分割成多个文件。Carla Schroder 解释说。

Linux 有几个用于分割文件的工具程序。那么你为什么要分割文件呢?一个用例是将大文件分割成更小的尺寸,以便它适用于比较小的存储介质,比如 U 盘。当您遇到 FAT32(最大文件大小为 4GB),且您的文件大于此时,通过 U 盘传输文件也是一个很好的技巧。另一个用例是加速网络文件传输,因为小文件的并行传输通常更快。

我们将学习如何使用 csplitsplitcat 来重新整理文件,然后再将文件合并在一起。这些操作在任何文件类型下都有用:文本、图片、音频文件、ISO 镜像文件等。

使用 csplit 分割文件

csplit 是这些有趣的小命令中的一个,它永远伴你左右,一旦开始用它就离不开了。csplit 将单个文件分割成多个文件。这个示例演示了最简单的使用方法,它将文件 foo.txt 分为三个文件,以行号 17 和 33 作为分割点:

$ csplit foo.txt 17 33
2591
3889
2359

csplit 在当前目录下创建了三个新文件,并以字节为单位打印出新文件的大小。默认情况下,每个新文件名为 xx_nn

$ ls
xx00
xx01
xx02

您可以使用 head 命令查看每个新文件的前十行:

$ head xx*

==> xx00 <==
Foo File
by Carla Schroder

Foo text

Foo subheading

More foo text

==> xx01 <==
Foo text

Foo subheading

More foo text

==> xx02 <==
Foo text

Foo subheading

More foo text

如果要将文件分割成包含相同行数的多个文件怎么办?可以指定行数,然后将重复次数放在在花括号中。此示例重复分割 4 次,并将剩下的转储到最后一个文件中:

$ csplit foo.txt 5 {4}
57
1488
249
1866
3798

您可以使用星号通配符来告诉 csplit 尽可能多地重复分割。这听起来很酷,但是如果文件不能等分,则可能会失败(LCTT 译注:低版本的 csplit 不支持此参数):

$ csplit foo.txt 10 {*}
1545
2115
1848
1901
csplit: '10': line number out of range on repetition 4
1430

默认的行为是删除发生错误时的输出文件。你可以用 -k 选项来解决这个问题,当有错误时,它就不会删除输出文件。另一个行为是每次运行 csplit 时,它将覆盖之前创建的文件,所以你需要使用新的文件名来分别保存它们。使用 --prefix= _prefix_ 来设置一个不同的文件前缀:

$ csplit -k --prefix=mine foo.txt 5 {*}  
57
1488
249
1866
993
csplit: '5': line number out of range on repetition 9
437

$ ls
mine00
mine01
mine02
mine03 
mine04
mine05

选项 -n 可用于改变对文件进行编号的数字位数(默认是 2 位):

$ csplit -n 3 --prefix=mine foo.txt 5 {4}
57
1488
249
1866
1381
3798

$ ls
mine000
mine001
mine002
mine003
mine004
mine005

csplit 中的 “c” 是上下文(context)的意思。这意味着你可以根据任意匹配的方式或者巧妙的正则表达式来分割文件。下面的例子将文件分为两部分。第一个文件在包含第一次出现 “fie” 的前一行处结束,第二个文件则以包含 “fie” 的行开头。

$ csplit foo.txt /fie/ 

在每次出现 “fie” 时分割文件:

$ csplit foo.txt /fie/ {*}

在 “fie” 前五次出现的地方分割文件:

$ csplit foo.txt /fie/ {5}

仅当内容以包含 “fie” 的行开始时才复制,并且省略前面的所有内容:

$ csplit myfile %fie% 

将文件分割成不同大小

splitcsplit 类似。它将文件分割成特定的大小,当您将大文件分割成小的多媒体文件或者使用网络传送时,这就非常棒了。默认的大小为 1000 行:

$ split foo.mv
$ ls -hl
266K Aug 21 16:58 xaa
267K Aug 21 16:58 xab
315K Aug 21 16:58 xac
[...]

它们分割出来的大小相似,但你可以指定任何你想要的大小。这个例子中是 20M 字节:

$ split -b 20M foo.mv

尺寸单位缩写为 K,M,G,T,P,E,Z,Y(1024 的幂)或者 KB,MB,GB 等等(1000 的幂)。

为文件名选择你自己的前缀和后缀:

$ split -a 3 --numeric-suffixes=9 --additional-suffix=mine foo.mv SB
240K Aug 21 17:44 SB009mine
214K Aug 21 17:44 SB010mine
220K Aug 21 17:44 SB011mine

-a 选项控制编号的数字位置。--numeric-suffixes 设置编号的开始值。默认前缀为 x,你也可以通过在文件名后输入它来设置一个不同的前缀。

将分割后的文件合并

你可能想在某个时候重组你的文件。常用的 cat 命令就用在这里:

$ cat SB0* > foo2.txt

示例中的星号通配符将匹配到所有以 SB0 开头的文件,这可能不会得到您想要的结果。您可以使用问号通配符进行更精确的匹配,每个字符使用一个问号:

$ cat SB0?????? > foo2.txt

和往常一样,请查阅相关的手册和信息页面以获取完整的命令选项。


via: https://www.linux.com/learn/intro-to-linux/2017/8/splitting-and-re-assembling-files-linux

作者:CARLA SCHRODER 译者:firmianay 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

关于应用密码学最令人扼腕也最引人入胜的一件事就是我们在现实中实际使用的密码学是多么的少。这并不是指密码学在业界没有被广泛的应用————事实上它的应用很广泛。我想指出的是,迄今为止密码学研究人员开发了如此多实用的技术,但工业界平常使用的却少之又少。实际上,除了少数个别情况,我们现今使用的绝大部分密码学技术是在 21 世纪初 (注1) 就已经存在的技术。

大多数人并不在意这点,但作为一个工作在研究与应用交汇领域的密码学家,这让我感到不开心。我不能完全解决这个问题,我做的,就是谈论一部分这些新的技术。在这个夏天里,这就是我想要做的:谈论。具体来说,在接下来的几个星期里,我将会写一系列讲述这些没有被看到广泛使用的前沿密码学技术的文章。

今天我要从一个非常简单的问题开始:在公钥加密之外还有什么(可用的加密技术)?具体地说,我将讨论几个过去 20 年里开发出的技术,它们可以让我们走出传统的公钥加密的概念的局限。

这是一篇专业的技术文章,但是不会有太困难的数学内容。对于涉及方案的实际定义,我会提供一些原论文的链接,以及一些背景知识的参考资料。在这里,我们的关注点是解释这些方案在做什么————以及它们在现实中可以怎样被应用。

基于身份的加密

在 20 世纪 80 年代中期,一位名叫 阿迪·萨莫尔 Adi Shamir 的密码学家提出了一个 全新的想法 。这个想法,简单来说,就是摒弃公钥

为了理解 萨莫尔 的想法从何而来,我们最好先了解一些关于公钥加密的东西。在公钥加密的发明之前,所有的加密技术都牵涉到密钥。处理这样的密钥是相当累赘的工作。在你可以安全地通信之前,你需要和你的伙伴交换密钥。这一过程非常的困难,而且当通信规模增大时不能很好地运作。

公钥加密(由 Diffie-Hellman 和萨莫尔的 RSA) 密码系统发展而来的)通过极大地简化密钥分配的过程给密码学带来了革命性的改变。比起分享密钥,用户现在只要将他们的公共密钥发送给其他使用者。有了公钥,公钥的接收者可以加密给你的信息(或者验证你的数字签名),但是又不能用该公钥来进行解密(或者产生数字签名)。这一部分要通过你自己保存的私有密钥来完成。

尽管公钥的使用改进了密码学应用的许多方面,它也带来了一系列新的挑战。从实践中的情况来看,拥有公钥往往只是成功的一半————人们通常还需要安全地分发这些公钥。

举一个例子,想象一下我想要给你发送一封 PGP 加密的电子邮件。在我可以这么做之前,我需要获得一份你的公钥的拷贝。我要怎么获得呢?显然我们可以亲自会面,然后当面交换这个密钥————但(由于面基的麻烦)没有人会愿意这样做。通过电子的方式获得你的公钥会更理想。在现实中,这意味着要么(1)我们必须通过电子邮件交换公钥, 要么(2)我必须通过某个第三方基础设施,比如一个 网站 或者 密钥服务器 ,来获得你的密钥。现在我们面临这样的问题:如果电子邮件或密钥服务器是不值得信赖的(或者简单的来说允许任何人以 你的名义 上传密钥 ),我就可能会意外下载到恶意用户的密钥。当我给“你”发送一条消息的时候,也许我实际上正在将消息加密发送给 Mallory.

Mallory

解决这个问题——关于交换公钥和验证它们的来源的问题——激励了大量的实践密码工程,包括整个 web PKI (网络公钥基础设施)。在大部分情况下,这些系统非常奏效。但是萨莫尔并不满意。如果,他这样问道,我们能做得更好吗?更具体地说,他这样思考:我们是否可以用一些更好的技术去替换那些麻烦的公钥?

萨莫尔的想法非常令人激动。他提出的是一个新的公钥加密形式,在这个方案中用户的“公钥”可以就是他们的身份。这个身份可以是一个名字(比如 “Matt Green”)或者某些诸如电子邮箱地址这样更准确的信息。事实上,“身份”是什么并不重要。重要的是这个公钥可以是一个任意的字符串————而不是一大串诸如“ 7cN5K4pspQy3ExZV43F6pQ6nEKiQVg6sBkYPg1FG56Not ”这样无意义的字符组合。

当然,使用任意字符串作为公钥会造成一个严重的问题。有意义的身份听起来很棒————但我们无法拥有它们。如果我的公钥是 “Matt Green” ,我要怎么得到的对应的私钥?如果能获得那个私钥,又有谁来阻止其他的某些 Matt Green 获得同样的私钥,进而读取我的消息。进而考虑一下这个,谁来阻止任意的某个不是名为 Matt Green 的人来获得它。啊,我们现在陷入了 Zooko 三难困境

萨莫尔的想法因此要求稍微更多一点的手段。相比期望身份可以全世界范围使用,他提出了一个名为“ 密钥生成机构 key generation authority ”的特殊服务器,负责产生私钥。在设立初期,这个机构会产生一个 最高公共密钥 master public key (MPK),这个公钥将会向全世界公布。如果你想要加密一条消息给“Matt Green”(或者验证我的签名),你可以用我的身份和我们达成一致使用的权威机构的唯一 MPK 来加密。要解密这则消息(或者制作签名),我需要访问同一个密钥机构,然后请求一份我的密钥的拷贝。密钥机构将会基于一个秘密保存的 最高私有密钥 master secret key (MSK)来计算我的密钥。

加上上述所有的算法和参与者,整个系统看起来是这样的:

一个 基于身份加密 Identity-Based Encryption (IBE)系统的概览。 密钥生成机构 Key Generation Authority 的 Setup 算法产生最高公共密钥(MPK)和最高私有密钥(MSK)。该机构可以使用 Extract 算法来根据指定的 ID 生成对应的私钥。加密器(左)仅使用身份和 MPK 来加密。消息的接受者请求对应她身份的私钥,然后用这个私钥解密。(图标由 Eugen Belyakoff 制作)

这个设计有一些重要的优点————并且胜过少数明显的缺点。在好的方面,它完全摆脱了任何和你发送消息的对象进行密钥交换的必要。一旦你选择了一个主密钥机构(然后下载了它的 MPK),你就可以加密给整个世界上任何一个人的消息。甚至更酷炫地,在你加密的时候,你的通讯对象甚至还不需要联系密钥机构。她可以在你给她发送消息之后再取得她的私钥。

当然,这个“特性”也同时是一个漏洞。因为密钥机构产生所有的私钥,它拥有相当大权力。一个不诚实的机构可以轻易生成你的私钥然后解密你的消息。用更得体的方式来说就是标准的 IBE 系统有效地“包含” 密钥托管机制 (注2)

基于身份加密(IBE)中的“加密(E)”

所有这些想法和额外的思考都是萨莫尔在他 1984 年的论文中提出来的。其中有一个小问题:萨莫尔只能解决问题的一半。

具体地说,萨莫尔提出了一个 基于身份签名 identity-based signature (IBS)的方案—— 一个公共验证密钥是身份、而签名密钥由密钥机构生成的签名方案。他尽力尝试了,但仍然不能找到一个建立基于身份加密的解决方案。这成为了一个悬而未决的问题。 (注3)

到有人能解决萨莫尔的难题等了 16 年。令人惊讶的是,当解答出现的时候,它出现了不只一次,而是三次

第一个,或许也是最负盛名的 IBE 的实现,是由 丹·博奈 Dan Boneh 马太·富兰克林Matthew Franklin在多年以后开发的。博奈和富兰克林的发现的时机十分有意义。 博奈富兰克林方案 Boneh-Franklin scheme 根本上依赖于能支持有效的 “ 双线性映射 bilinear map ” (或者“ 配对 pairing ”) (注4) 的椭圆曲线。需要计算这些配对的该类 算法 在萨莫尔撰写他的那篇论文是还不被人知晓,因此没有被建设性地使用——即被作为比起 一种攻击 更有用的东西使用——直至 2000年

(关于博奈富兰克林 IBE 方案的简短教学,请查看 这个页面

第二个被称为 Sakai-Kasahara 的方案的情况也大抵类似,这个方案将在与第一个大约同一时间被另外一组学者独立发现。

第三个 IBE 的实现并不如前二者有效,但却更令人吃惊得多。这个方案 克利福德·柯克斯 Clifford Cocks ,一位英国国家通信总局的资深密码学家开发。它因为两个原因而引人注目。第一,柯克斯的 IBE 方案完全不需要用到双线性映射——都是建立在以往的 RSA 的基础上的,这意味着原则上这个算法这么多年来仅仅是没有被人们发现(而非在等待相应的理论基础)而已。第二,柯克斯本人近期因为一些甚至更令人惊奇的东西而闻名:在 RSA 算法被提出之前将近 5 年 发现 RSA 加密系统(LCTT 译注:即公钥加密算法)。用再一个在公钥加密领域的重要成就来结束这一成就,实在堪称令人印象深刻的创举。

自 2001 年起,许多另外的 IBE 构造涌现出来,用到了各种各样的密码学背景知识。尽管如此,博奈和富兰克林早期的实现仍然是这些算法之中最为简单和有效的。

即使你并不因为 IBE 自身而对它感兴趣,事实证明它的基本元素对密码学家来说在许许多多单纯地加密之外的领域都十分有用。事实上,如果我们把 IBE 看作是一种由单一的主公/私钥对来产生数以亿计的相关联的密钥对的方式,它将会显得意义非凡。这让 IBE 对于诸如 选择密文攻击 chosen ciphertext attacks 前向安全的公钥加密 forward-secure public key encryption 短签名方案 short signature schemes 这样各种各样的应用来说非常有用。

基于特征加密

当然,如果你给密码学家以一个类似 IBE 的工具,那么首先他们要做的将是找到一种让事情更复杂改进它的方法。

最大的改进之一要归功于 阿密特·萨海 Amit Sahai 布伦特·沃特世 Brent Waters 。我们称之为 基于特征加密 Attribute-Based Encryption ,或者 ABE。

这个想法最初并不是为了用特征来加密。相反,萨海和沃特世试图开发一种使用生物辨识特征来加密的基于身份的加密方案。为了理解这个问题,想象一下我决定使用某种生物辨识特征,比如你的 虹膜扫描影像,来作为你的“身份”来加密一则给你的密文。然后你将向权威机构请求一个对应你的虹膜的解密密钥————如果一切都匹配得上,你就可以解密信息了。

问题就在于这几乎不能奏效。

告诉我这不会给你带来噩梦

因为生物辨识特征的读取(比如虹膜扫描或者指纹模板)本来就是易出错的。这意味着每一次的读取通常都是十分接近的,但却总是会几个对不上的比特。在标准的 IBE 系统中这是灾难性的:如果加密使用的身份和你的密钥身份有哪怕是一个比特的不同,解密都会失效。你就不走运了。

萨海和沃特世决定通过开发一种包含“阈值门”的 IBE 形式来解决这个问题。在这个背景下,一个身份的每一个字节都被表示为一个不同的“特征”。把每一个这种特征看作是你用于加密的一个元件——譬如“你的虹膜扫描的 5 号字节是 1”和“你的虹膜扫描的 23 号字节是 0”。加密的一方罗列出所有这些字节,然后将它们中的每一个都用于加密中。权威机构生成的解密密钥也嵌入了一连串相似的字节值。根据这个方案的定义,当且仅当(你的身份密钥与密文解密密钥之间)配对的特征数量超过某个预先定义的阈值时,才能顺利解密:比如为了能解密,2048 个字节中的(至少) 2024 个要是对应相同的。

这个想法的优美之处不在于模糊 IBE,而在于一旦你有了一个阈值门和一个“特征”的概念,你就能做更有趣的事情。主要的观察结论 是阈值门可以拥有实现布尔逻辑的 AND 门和 OR 门(LCTT 译注:译者认为此处应为用 AND 门和 OR 门实现, 原文: a threshold gate can be used to implement the boolean AND and OR gates),就像这样:

甚至你还可以将这些逻辑闸门堆叠起来,一些在另一些之上,来表示一些相当复杂的布尔表达式——这些表达式本身就用于判定在什么情况下你的密文可以被解密。举个例子,考虑一组更为现实的特征,你可以这样加密一份医学记录,使医院的儿科医生或者保险理算员都可以阅读它。你所需要做的只不过是保证人们可以得到正确描述他们的特征的密钥(就是一些任意的字符串,如同身份那样)。

一个简单的“密文规定”。在这个规定中当且仅当一个密钥与一组特定的特征匹配时,密文才能被解密。在这个案例中,密钥满足该公式的条件,因此密文将被解密。其余用不到的特征在这里忽略掉。

其他的条件判断也能实现。通过一长串特征,比如文件创建时间、文件名,甚至指示文件创建位置的 GPS 坐标, 来加密密文也是有可能的。于是你可以让权威机构分发一部分对应你的数据集的非常精确的密钥————比如说,“该密钥用于解密所有在 11 月 3 号和 12 月 12 号之间在芝加哥被加密的包含‘小儿科’或者‘肿瘤科’标记的放射科文件”。

函数式加密

一旦拥有一个相关的基础工具,像 IBE 和 ABE,研究人员的本能是去扩充和一般化它。为什么要止步于简单的布尔表达式?我们能不能制作嵌入了任意的计算机程序 密钥 key (或者 密文 ciphertext )?答案被证明是肯定的——尽管不是非常高效。一组 近几年的 研究 显示可以根据各种各样的 基于格 lattice-based 的密码假设,构建在 任意多项式大小线路 arbitrary polynomial-size circuits 运作的 ABE。所以这一方向毫无疑问非常有发展潜力。

这一潜力启发了研究人员将所有以上的想法一般化成为单独一类被称作 “函数式加密” functional encryption 的加密方式。函数式加密更多是一种抽象的概念而没有具体所指——它不过是一种将所有这些系统看作是一个特定的类的实例的方式。它基本的想法是,用一种依赖于(1)明文,和(2)嵌入在密钥中的数据 的任意函数 F 的算法来代表解密过程。

(LCTT 译注:上面函数 F 的 (1) 原文是“the plaintext inside of a ciphertext”,但译者认为应该是密文,其下的公式同。)

这个函数大概是这样的:

输出 = F(密钥数据,密文数据)

在这一模型中,IBE 可以表达为有一个加密算法 加密(身份,明文)并定义了一个这样的函数 F:如果“密钥输入 == 身份”,则输出对应明文,否则输出空字符串的系统。相似地,ABE 可以表达为一个稍微更复杂的函数。依照这一范式,我们可以展望在将来,各类有趣的功能都可以由计算不同的函数得到,并在未来的方案中被实现。

但这些都必须要等到以后了。今天我们谈的已经足够多了。

所以这一切的重点是什么?

对于我来说,重点不过是证明密码学可以做到一些十分优美惊人的事。当谈及工业与“应用”密码学时,我们鲜有见到这些出现在日常应用中,但我们都可以等待着它们被广泛使用的一天的到来。

也许完美的应用就在某个地方,也许有一天我们会发现它。

注:

  • 注 1:最初在这片博文里我写的是 “20 世纪 90 年代中期”。在文章的评论里,Tom Ristenpart 提出了异议,并且非常好地论证了很多重要的发展都是在这个时间之后发生的。所以我把时间再推进了大约 5 年,而我也在考虑怎样将这表达得更好一些。
  • 注 2:我们知道有一种叫作 “无证书加密” 的加密的中间形式。这个想法由 Al-Riyami 和 Paterson 提出,并且使用到标准公钥加密和 IBE 的结合。基本的思路是用一个(消息接受者生成的)传统密钥和一个 IBE 身份共同加密每则消息。然后接受者必须从 IBE 权威机构处获得一份私钥的拷贝来解密。这种方案的优点是两方面的:(1)IBE 密钥机构不能独自解密消息,因为它没有对应的(接受者)私钥,这就解决了“托管”问题(即权威机构完全具备解密消息的能力);(2)发送者不必验证公钥的确属于接收者(LCTT 译注:原文为 sender,但译者认为应该是笔误,应为 recipient),因为 IBE 方面会防止伪装者解密这则消息。但不幸的是,这个系统更像是传统的公钥加密系统,而缺少 IBE 简洁的实用特性。
  • 注 3:开发 IBE 的一部分挑战在于构建一个面临不同密钥持有者的“勾结”安全的系统。譬如说,想象一个非常简单的只有 2 比特的身份鉴定系统。这个系统只提供四个可能的身份:“00”,“01”,“10”,“11”。如果我分配给你对应 “01” 身份的密钥,分配给 Bob 对应 “10” 的密钥,我需要保证你们不能合谋生成对应 “00” 和 “11” 身份的密钥。一些早期提出的解决方法尝试通过用不同方式将标准公共加密密钥拼接到一起来解决这个问题(比如,为身份的每一个字节保留一个独立的公钥,然后将对应的多个私钥合并成一个分发)。但是,当仅仅只有少量用户合谋(或者他们的密钥被盗)时,这些系统就往往会出现灾难性的失败。因而基本上这个问题的解决就是真正的 IBE 与它的仿造近亲之间的区别。
  • 注 4: 博奈和富兰克林方案的完整描述可以在 这里 看到,或者在他们的 原版论文 中。这里这里这里 有一部分代码。除了指出这个方案十分高效之外,我不希望在这上面花太多的篇幅。它由 Voltage Security(现属于惠普) 实现并占有专利。

via: https://blog.cryptographyengineering.com/2017/07/02/beyond-public-key-encryption/

作者:Matthew Green 译者:Janzen\_Liu 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

在前几篇博文中我们学习了 DWARF 信息以及它如何使我们将机器码和上层源码联系起来。这一次我们通过为我们的调试器添加源码级逐步调试将该知识应用于实际。

系列文章索引

随着后面文章的发布,这些链接会逐渐生效。

  1. 准备环境
  2. 断点
  3. 寄存器和内存
  4. Elves 和 dwarves
  5. 源码和信号
  6. 源码级逐步执行
  7. 源码级断点
  8. 调用栈展开
  9. 读取变量
  10. 下一步

揭秘指令级逐步执行

我们正在超越了自我。首先让我们通过用户接口揭秘指令级单步执行。我决定将它切分为能被其它部分代码利用的 single_step_instruction 和确保是否启用了某个断点的 single_step_instruction_with_breakpoint_check 两个函数。

void debugger::single_step_instruction() {
    ptrace(PTRACE_SINGLESTEP, m_pid, nullptr, nullptr);
    wait_for_signal();
}

void debugger::single_step_instruction_with_breakpoint_check() {
    //首先,检查我们是否需要停用或者启用某个断点
    if (m_breakpoints.count(get_pc())) {
        step_over_breakpoint();
    }
    else {
        single_step_instruction();
    }
}

正如以往,另一个命令被集成到我们的 handle_command 函数:

else if(is_prefix(command, "stepi")) {
    single_step_instruction_with_breakpoint_check();
    auto line_entry = get_line_entry_from_pc(get_pc());
    print_source(line_entry->file->path, line_entry->line);
 }

利用新增的这些函数我们可以开始实现我们的源码级逐步执行函数。

实现逐步执行

我们打算编写这些函数非常简单的版本,但真正的调试器有 thread plan 的概念,它封装了所有的单步信息。例如,调试器可能有一些复杂的逻辑去决定断点的位置,然后有一些回调函数用于判断单步操作是否完成。这其中有非常多的基础设施,我们只采用一种朴素的方法。我们可能会意外地跳过断点,但如果你愿意的话,你可以花一些时间把所有的细节都处理好。

对于跳出 step_out,我们只是在函数的返回地址处设一个断点然后继续执行。我暂时还不想考虑调用栈展开的细节 - 这些都会在后面的部分介绍 - 但可以说返回地址就保存在栈帧开始的后 8 个字节中。因此我们会读取栈指针然后在内存相对应的地址读取值:

void debugger::step_out() {
    auto frame_pointer = get_register_value(m_pid, reg::rbp);
    auto return_address = read_memory(frame_pointer+8);

    bool should_remove_breakpoint = false;
    if (!m_breakpoints.count(return_address)) {
        set_breakpoint_at_address(return_address);
        should_remove_breakpoint = true;
    }

    continue_execution();

    if (should_remove_breakpoint) {
        remove_breakpoint(return_address);
    }
}

remove_breakpoint 是一个小的帮助函数:

void debugger::remove_breakpoint(std::intptr_t addr) {
    if (m_breakpoints.at(addr).is_enabled()) {
        m_breakpoints.at(addr).disable();
    }
    m_breakpoints.erase(addr);
}

接下来是跳入 step_in。一个简单的算法是继续逐步执行指令直到新的一行。

void debugger::step_in() {
   auto line = get_line_entry_from_pc(get_pc())->line;

    while (get_line_entry_from_pc(get_pc())->line == line) {
        single_step_instruction_with_breakpoint_check();
    }

    auto line_entry = get_line_entry_from_pc(get_pc());
    print_source(line_entry->file->path, line_entry->line);
}

跳过 step_over 对于我们来说是三个中最难的。理论上,解决方法就是在下一行源码中设置一个断点,但下一行源码是什么呢?它可能不是当前行后续的那一行,因为我们可能处于一个循环、或者某种条件结构之中。真正的调试器一般会检查当前正在执行什么指令然后计算出所有可能的分支目标,然后在所有分支目标中设置断点。对于一个小的项目,我不打算实现或者集成一个 x86 指令模拟器,因此我们要想一个更简单的解决办法。有几个可怕的选择,一个是一直逐步执行直到当前函数新的一行,或者在当前函数的每一行都设置一个断点。如果我们是要跳过一个函数调用,前者将会相当的低效,因为我们需要逐步执行那个调用图中的每个指令,因此我会采用第二种方法。

void debugger::step_over() {
    auto func = get_function_from_pc(get_pc());
    auto func_entry = at_low_pc(func);
    auto func_end = at_high_pc(func);

    auto line = get_line_entry_from_pc(func_entry);
    auto start_line = get_line_entry_from_pc(get_pc());

    std::vector<std::intptr_t> to_delete{};

    while (line->address < func_end) {
        if (line->address != start_line->address && !m_breakpoints.count(line->address)) {
            set_breakpoint_at_address(line->address);
            to_delete.push_back(line->address);
        }
        ++line;
    }

    auto frame_pointer = get_register_value(m_pid, reg::rbp);
    auto return_address = read_memory(frame_pointer+8);
    if (!m_breakpoints.count(return_address)) {
        set_breakpoint_at_address(return_address);
        to_delete.push_back(return_address);
    }

    continue_execution();

    for (auto addr : to_delete) {
        remove_breakpoint(addr);
    }
}

这个函数有一点复杂,我们将它拆开来看。

    auto func = get_function_from_pc(get_pc());
    auto func_entry = at_low_pc(func);
    auto func_end = at_high_pc(func);

at_low_pcat_high_pclibelfin 中的函数,它们能给我们指定函数 DWARF 信息条目的最小和最大程序计数器值。

    auto line = get_line_entry_from_pc(func_entry);
    auto start_line = get_line_entry_from_pc(get_pc());

    std::vector<std::intptr_t> breakpoints_to_remove{};

    while (line->address < func_end) {
        if (line->address != start_line->address && !m_breakpoints.count(line->address)) {
            set_breakpoint_at_address(line->address);
            breakpoints_to_remove.push_back(line->address);
        }
        ++line;
    }

我们需要移除我们设置的所有断点,以便不会泄露出我们的逐步执行函数,为此我们把它们保存到一个 std::vector 中。为了设置所有断点,我们循环遍历行表条目直到找到一个不在我们函数范围内的。对于每一个,我们都要确保它不是我们当前所在的行,而且在这个位置还没有设置任何断点。

    auto frame_pointer = get_register_value(m_pid, reg::rbp);
    auto return_address = read_memory(frame_pointer+8);
    if (!m_breakpoints.count(return_address)) {
        set_breakpoint_at_address(return_address);
        to_delete.push_back(return_address);
    }

这里我们在函数的返回地址处设置一个断点,正如跳出 step_out

    continue_execution();

    for (auto addr : to_delete) {
        remove_breakpoint(addr);
    }

最后,我们继续执行直到命中它们中的其中一个断点,然后移除所有我们设置的临时断点。

它并不美观,但暂时先这样吧。

当然,我们还需要将这个新功能添加到用户界面:

    else if(is_prefix(command, "step")) {
        step_in();
    }
    else if(is_prefix(command, "next")) {
        step_over();
    }
    else if(is_prefix(command, "finish")) {
        step_out();
    }

测试

我通过实现一个调用一系列不同函数的简单函数来进行测试:

void a() {
    int foo = 1;
}

void b() {
    int foo = 2;
    a();
}

void c() {
    int foo = 3;
    b();
}

void d() {
    int foo = 4;
    c();
}

void e() {
    int foo = 5;
    d();
}

void f() {
    int foo = 6;
    e();
}

int main() {
    f();
}

你应该可以在 main 地址处设置一个断点,然后在整个程序中跳入、跳过、跳出函数。如果你尝试跳出 main 函数或者跳入任何动态链接库,就会出现意料之外的事情。

你可以在这里找到这篇博文的相关代码。下次我们会利用我们新的 DWARF 技巧来实现源码级断点。


via: https://blog.tartanllama.xyz/c++/2017/05/06/writing-a-linux-debugger-dwarf-step/

作者:Simon Brand 译者:ictlyh 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

OpenStack 非常复杂,许多社区成员都在努力使 OpenStack 的部署和操作更加容易。其中大部分时间都用来改善相关工具,如:Ansible、Puppet、Kolla、Juju、Triple-O 和 Chef (仅举几例)。但是,如果我们降低一下标准,并且还能使包的体验更加简单,将会怎样呢?

我们正在努力通过 snap 包来实现这一点。snap 包是一种新兴的软件分发方式,这段来自 snapcraft.io 的介绍很好的总结了它的主要优点:snap 包可以快速安装、易于创建、安全运行而且能自动地事务化更新,因此你的应用程序总是能保持最新的状态并且永远不会被破坏。

捆绑软件

单个 snap 包可以内嵌多个不同来源的软件,从而提供一个能够快速启动和运行的解决方案。当你安装 snap 包时,你会发现安装速度是很快的,这是因为单个 snap 包捆绑了所有它需要的依赖。这和安装 deb 包有些不同,因为它需要下载所有的依赖然后分别进行安装。

Snap 包制作简单

在 Ubuntu 工作的时候,我花了很多时间为 Debian 制作 OpenStack 的安装包。这是一种很特殊技能,需要花很长时间才能理解其中的细微差别。与 snap 包相比,deb 包和 snap 包在复杂性上的差异有天壤之别。snap 包简单易行,并且相当有趣。

Snap 包的其它特性

  • 每个 snap 包都安装在其独有的只读 squashfs 文件系统中。
  • 每个 snap 包都运行在一个由 AppArmor 和 seccomp 策略构建的严格沙箱环境中。
  • snap 包能事务更新。新版本的 snap 包会安装到一个新的只读 squashfs 文件系统中。如果升级失败,它将回滚到旧版本。
  • 当有新版本可用时,snap 包将自动更新。
  • OpenStack 的 snap 包能保证与 OpenStack 的上游约束保持一致。打包的人不需要再为 OpenStack 依赖链维护单独的包。这真是太爽了!

OpenStack snap 包介绍

现在,下面这些项目已经有了相应的 snap 包:

  • Keystone —— 这个 snap 包为 OpenStack 提供了身份鉴证服务。
  • Glance —— 这个 snap 包为 OpenStack 提供了镜像服务。
  • Neutron —— 这个 snap 包专门提供了 neutron-server 过程,作为 OpenStack 部署过程的一个 snap 包。
  • Nova —— 这个 snap 包提供 OpenStack 部署过程中的 Nova 控制器组件。
  • Nova-hypervisor —— 这个 snap 包提供 OpenStack 部署过程中的 hypervisor 组件,并且配置使用通过 deb 包安装的 Libvirt/KVM + Open vSwitch 组合。这个 snap 包同时也包含 nava-lxd,这允许我们使用 nova-lxd 而不用 KVM。

这些 snpa 包已经能让我们部署一个简单可工作的 OpenStack 云。你可以在 github 上找到所有这些 OpenStack snap 包的源码。有关 OpenStack snap 包更多的细节,请参考上游存储库中各自的 README。在那里,你可以找到更多有关管理 snap 包的信息,比如覆盖默认配置、重启服务、设置别名等等。

想要创建自己的 OpenStack snap 包吗?

查看 snap cookie 工具。我很快就会写一篇博文,告诉你如何使用 snap cookie 工具。它非常简单,并且能帮助你在任何时候创建一个新的 OpenStack snap 包。

测试 OpenStack snap 包

我们已经用简单的脚本初步测试了 OpenStack snap 包。这个脚本会在单个节点上安装 sanp 包,还会在安装后提供额外的配置服务。来尝试下吧:

git clone https://github.com/openstack-snaps/snap-test
cd snap-test
./snap-deploy

这样,我们就已经在 Ubuntu Xenial(16.04) 上做了所有的测试。要注意的是,这将在你的系统上安装和配置相当多的软件,因此你最好在可自由使用的机器上运行它。

追踪 OpenStack

现在,你可以从 snap 商店的边缘通道来安装 snap 包,比如:

sudo snap install --edge keystone

OpenStack 团队正在努力使 CI/CD 配置到位,以便让 snap 包的发布能够交叉追踪 OpenStack 的发布(比如一个追踪 Ocata,另一个追踪 Pike 等)。每个 轨道 track 都有 4 个不同的通道。每个轨道的边缘通道将包含 OpenStack 项目对应分支最近的内容,测试、候选和稳定通道被保留用于已发布的版本。这样我们将看到如下的用法:

sudo snap install --channel=ocata/stable keystone
sudo snap install --channel=pike/edge keystone

其它

我们可以使用多个环境变量来简化 snap 包的制作。这里 有相关的说明。实际上,你无需深入的研究他们,但是在安装完 snap 包后,你也许会想要了解这些位置:

$SNAP == /snap/<snap-name>/current

这是 snap 包和它所有的文件挂载的位置。所有东西都是只读的。比如我当前安装的 keystone,$SNAP 就是 /snap/keystone/91。幸好,你不需要知道当前版本号,因为在 /snap/keystone/ 中有一个软链接(LCTT 译注:/snap/keystone/current/)指向当前正在使用版本对应的文件夹。

$ ls /snap/keystone/current/
bin                     etc      pysqlite2-doc        usr
command-manage.wrapper  include  snap                 var
command-nginx.wrapper   lib      snap-openstack.yaml
command-uwsgi.wrapper   meta     templates

$ ls /snap/keystone/current/bin/
alembic                oslo-messaging-send-notification
convert-json           oslo-messaging-zmq-broker
jsonschema             oslo-messaging-zmq-proxy
keystone-manage        oslopolicy-checker
keystone-wsgi-admin    oslopolicy-list-redundant
keystone-wsgi-public   oslopolicy-policy-generator
lockutils-wrapper      oslopolicy-sample-generator
make_metadata.py       osprofiler
mako-render            parse_xsd2.py
mdexport.py            pbr
merge_metadata.py      pybabel
migrate                snap-openstack
migrate-repository     sqlformat
netaddr                uwsgi
oslo-config-generator

$ ls /snap/keystone/current/usr/bin/
2to3               idle     pycompile     python2.7-config
2to3-2.7           pdb      pydoc         python2-config
cautious-launcher  pdb2.7   pydoc2.7      python-config
compose            pip      pygettext     pyversions
dh_python2         pip2     pygettext2.7  run-mailcap
easy_install       pip2.7   python        see
easy_install-2.7   print    python2       smtpd.py
edit               pyclean  python2.7

$ ls /snap/keystone/current/lib/python2.7/site-packages/
...

$SNAP_COMMON == /var/snap/<snap-name>/common

这个目录用于存放系统数据,对于 snap 包的多个修订版本这些数据是共用的。在这里,你可以覆盖默认配置文件和访问日志文件。

$ ls /var/snap/keystone/common/
etc  fernet-keys  lib  lock  log  run

$ sudo ls /var/snap/keystone/common/etc/
keystone  nginx  uwsgi

$ ls /var/snap/keystone/common/log/
keystone.log  nginx-access.log  nginx-error.log  uwsgi.log

严格限制

每个 snap 包都是在一个由 seccomp 和 AppArmor 策略构建的严格限制的环境中运行的。更多关于 snap 约束的细节可以在 这里 查看。

snap 包即将到来的新特性和更新

我正在期待 snap 包一些即将到来的新特性和更新(LCTT 译注:此文发表于 7 月 6 日):

  • 我们正在致力于实现 libvirt AppArmor 策略,这样 nova-hypervisor 的 snap 包就能够访问 qcow2 的 支持文件 backing files

    • 现在,作为一种变通方法,你可以将 virt-aa-helper 放在 complain 模式下:sudo aa-complain /usr/lib/libvirt/virt-aa-helper
  • 我们还在为 snapd 开发额外的接口策略,以便为部署的实例启用网络连接。

    • 现在你可以在 devmode 模式下安装 nova-hypervisor snap 包,它会禁用安全限制:snap install -devmode -edge nova-hypervisor
  • 自动连接 nova-hypervisor 的接口。我们正在努力实现在安装时自动定义 nova-hypervisor 接口。

    • 定义 AppArmor 和 seccomp 策略的接口可以允许 snap 包访问系统的资源。
    • 现在,你可以手动连接需要接口,在 nova-hypervisor snap 包的 README 中有相关的描述。
  • 命令自动定义别名。我们正在努力实现 snap 包在安装时为命令自动定义别名。

    • 这使得我们可以使用传统的命令名。安装 snap 包后,你将可以使用 nova-manage db sync 而无需再用 nova.manage db sync
    • 现在,你可以在安装 snap 包后手动设置别名,比如:snap alias nova.manage nova-manage。如想获取更多细节请查看 snap 包的 README 。
  • 守护进程自动定义别名。当前 snappy 仅支持为命令(非守护进程)定义别名。一旦针对守护进程的别名可用了,我们将设置它们在安装的时候自动配置。

    • 这使得我们可以使用额外的单元文件名。我们可以使用 systemctl restart nova-compute 而无需再用 systemctl restart snap.nova.nova-compute
  • snap 包资产跟踪。这使得我们可以追踪用来构建 snap 包的版本以便在将来构建时重复使用。

如果你想多聊一些关于 snap 包的内容,你可以在 freenode 的 #openstack-snaps 这样的 IRC 上找到我们。我们欢迎你的反馈和贡献!感谢并祝你玩得开心!Corey


作者简介:

Corey Bryant 是 Ubuntu 的核心开发者和 Canonical 公司 OpenStack 工程团队的软件工程师,他主要专注于为 Ubuntu 提供 OpenStack 的安装包以及为 Juju 进行 OpenStack 的魅力开发。他对开源软件充满热情,喜欢与来自世界各地的人一起工作。

译者简介:

snapcraft.io 的钉子户,对 Ubuntu Core、Snaps 和 Snapcraft 有着浓厚的兴趣,并致力于将这些还在快速发展的新技术通过翻译或原创的方式介绍到中文世界。有兴趣的小伙伴也可以关注译者个人的公众号: Snapcraft,最近会在上面连载几篇有关 Core snap 发布策略、交付流程和验证流程的文章,欢迎围观 :)

via: https://insights.ubuntu.com/2017/07/06/openstack-in-a-snap/

作者:Corey Bryant 译者:Snapcrafter 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

Getting Started With GitHub

GitHub 是一个在线平台,旨在促进在一个共同项目上工作的个人之间的代码托管、版本控制和协作。通过该平台,无论何时何地,都可以对项目进行操作(托管和审查代码,管理项目和与世界各地的其他开发者共同开发软件)。GitHub 平台为开源项目和私人项目都提供了项目处理功能。

关于团队项目处理的功能包括:GitHub Flow> 和 GitHub Pages 。这些功能可以让需要定期部署的团队轻松处理工作流程。另一方面,GitHub 页提供了页面用于展示开源项目、展示简历、托管博客等。

GitHub 也为个人项目提供了必要的工具,使得个人项目可以轻松地处理。它也使得个人可以更轻松地与世界分享他们的项目。

注册 GitHub 并启动一个项目

在 GitHub 上启动新项目时,您必须先使用您的电子邮件地址创建一个帐户。

github homepage

然后,在验证邮箱的时候,用户将自动登录到他们的 GitHub 帐户。

1、 创建仓库

之后,我们会被带到一个用于创建 仓库 repository 的页面。​仓库存储着包括修订历史记录在内的所有项目文件。仓库可以是公开的或者是私有的。公开的仓库可以被任何人查看,但是,只有项目所有者授予权限的人才可以提交修改到这个仓库。另一方面,私有仓库提供了额外的控制,可以将项目设置为对谁可见。因此,公开仓库适用于开源软件项目,而私有仓库主要适用于私有或闭源项目。

  • 填写 “ 仓库名称 Repository Name ” 和 “ 简短描述 Short Description ”。
  • 选中 “ 以一个 README 文件初始化 Initialize this repository with a README ”。
  • 最后,点击底部的 “ 创建仓库 Create Repository ” 按钮。

create a github repository

2、 添加分支

在 GitHub 中, 分支 branch 是一种同时操作单个仓库的各种版本的方式。默认情况下,任何创建的单个仓库都会被分配一个名为 “MASTER” 的分支,它被认为是最后一个分支。在 GitHub 中,分支在被合并到 主干 master (最后的分支)之前,可以在对仓库进行实验和编辑中发挥作用。

为了使项目适合每一个人的需求,通常情况下,总是需要添加几个格外的分支来匹配不同的项目。在主分支上创建一个分支和复制主分支时的当前状态是一样的。

add a branch to github repository

创建分支与在不同版本中保存单个文件是类似的。它通过在特定仓库上执行的任务重命名来实现。

分支在保持错误修复和功能添加工作中同样被证明是有效。在进行必要的修改后,这些分支会被合并到主分支中。

在创建仓库后创建一个分支:

  • 在这个例子中,点击仓库名称 “Hello-World” 跳转到你的新仓库。
  • 点击顶部的 “Branch:Master” 按钮,会看到一个下拉菜单,菜单里有填写分支名称的空白字段。
  • 输入分支名称,在这个例子中我们输入 “readme-edits“。
  • 按下回车键或者点击蓝色的 “ 创建分支 create branch ” 框。

这样就成功创建了两个分支:master 和 readme-edits。

3、 修改项目文件并提交

此步骤提供了关于如何更改仓库并保存修改的指导。在 GitHub 上, 提交 commit 被定义为保存的修改的意思。每一次提交都与一个 提交信息 commit message 相关联,该提交信息包含了保存的修改的历史记录,以及为何进行这些更改。这使得其他贡献者可以很轻松地知道你做出的更改以及更改的原因。

要对仓库进行更改和提交更改,请执行以下步骤:

  • 点击仓库名称 “Hello-World”。
  • 点击右上角的铅笔图标查看和编辑文件。 commit changes to github repository
  • 在编辑器中,写一些东西来确定你可以进行更改。
  • 提交消息 commit message 字段中做简要的总结,以解释为什么以及如何进行更改。
  • 点击 提交更改 commit changes 按钮保存更改。

请注意,这些更改仅仅影响到 readme-edits 分支,而不影响主分支。

commit branch to master

4、 开启一个拉取请求

​拉取请求 pull request 是一个允许贡献者提出并请求某人审查和合并某些更改到他们的分支的功能。​拉取请求还显示了几个分支的差异(diffs)。更改、添加和删减通常以红色和绿色来表示。一旦提交完成就可以开启​拉取请求,即使代码还未完成。

开启一个​拉取请求:

  • 点击​ ​拉取请求 pull requests 选项卡。 github pull request
  • 点击 新建拉取请求 new pull requests 按钮。
  • 选择 readme-edits 分支与 master 分支进行比较。 compare commit changes github
  • 确定请求,并确定这是您要提交的内容。
  • 点击创建​拉取请求绿色按钮并输入一个标题。 open a pull request in github repository
  • 按下回车键。

用户可以通过尝试创建并保存拉取请求来证实这些操作。

5、 合并拉取请求

最后一步是将 readme-edits 分支和 master 分支合并到一起。如果 readme-edits 分支和 master 分支不会产生冲突,则会显示 merge pull request 合并拉取请求 的按钮。

merge the pull request github

当合并拉取时,有必要确保 评论 comment 和其他字段被正确填写。合并拉取:

  • 点击 merge pull request 合并拉取请求 的按钮。
  • 确认合并。
  • 按下紫色的删除分支按钮,删除 readme-edits 分支,因为它已经被包含在 master 分支中。(LCTT 译注:如果是合并他人提交的拉取请求,则无需也无法删除合并过来的他人的分支。)

本文提供了 GitHub 平台从注册到使用的基本操作,接下来由大家尽情探索吧。


via: http://www.linuxandubuntu.com/home/getting-started-with-github

作者:LinuxAndUbuntu 译者:firmianay 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出