阿兰·图灵

说到计算机的历史,就不得不提到一个人——图灵,那么图灵是什么人?

阿兰·图灵,英国著名的数学家和计算机科学家,被誉为计算机科学之父、人工智能之父和密码学之父。

第二次世界大战中,阿兰·图灵是一位密码破译专家,协助英国政府破解了德国的密码,对盟军的胜利作出了贡献。

在1939年,英国参加了二战,他加入英国布莱切利园的一个密码破译组织,负责破解德军用的一种名为 Enigma 加密机的通信加密信息。

Enigma 看起来像一台打字机,有键盘、灯板、插线板和转子。键盘上按下一个字母键,灯板就会显示加密后的字母。

其中最重要的是转子,Enigma 的转子会轮换替代映射到密文。更改映射的能力很重要,因为一旦某人推导出一个字母替代规则,那么他将会知道密文中每个字母替换规则,因此需要将这些配对都改变,每次编码字母时都更改。

Enigma 实现方式是将所有布线嵌入到车轮/转子中。通过在保持字母静止的同时转动转子,字母之间的连接会发生变化。重复替换步骤,然后转动每个字母的转子。在转子中,每根导线的两端都有外部接触点。这允许这些转子中的多个并排放置,相邻触点接触。在内部,每个转子的接线方式不同,即每个转子都包含不同的密码。在一些Enigma机器中,有三个转子,最常用的是八个。每个转子还有一个附加的字母环,该字母环随转子转动并用于设置转子的初始位置。

每个转子都可以转动到任何位置。这意味着对于第一个转子,有26条可能的路径通过一个字母。但是一旦我们沿着导线穿过第一个转子,现在有26条可能的路径通过第二个转子。然后通过第三条路径还有26条可能的路径。因此,2条通过所有三个转子的路径总数为17576。如果是5个转子,我们可以从五个转子中选择用于左侧的转子,然后从剩余的四个转子中选择用于中间的转子,然后从三个转子中选择用于正确的转子。这提供了60种可能的方式来选择用于消息的三个转子。由于一个字母可以通过转子有17576条可能的路径,因此总共有1054560种可能性。

1930年,德国军队版本增加了一个插板,允许交换字母。由于有26个字母,最多可以进行13个掉期,但通常只有10个。计算连接插板的可能方法数量的数学有点复杂,但数字是150738274937250。乘以我们上面给出的其他可能的组合,我们得到一个字母可以采用的可能路径总数是158962555217826360000。

可能性超多的,在那个只能用真空管做布尔计算的时代,想要破译这些可能,是一件很难的事情。

那么当时盟军是怎么破译的呢?

早期替换加密规律很简单,比如凯撒加密把信件中的字母向前挪三个位置,还有玛丽女王密谋杀伊丽莎白女王的密文,通过统计字母出现频率之类的规则,当破解了一个字母替换方法就能找出通篇原文,没有计算机也能够手工破解出来,而 Enigma 每个字母的可能性都海量的,导致盟军在很长一段时间都没法破译 Enigma 加密的内容。

1932年波兰数学家马里安·雷耶夫斯基、杰尔兹·罗佐基和亨里克·佐加尔斯基按照法国情报人员秘密获取的 Enigma 的原理破解了 Enigma。由于波兰数学家们利用的漏洞不断被德军修复,算力无法及时算出结果,后来将破解方法告诉了英国。

图灵基于波兰破解方法,利用字母加密后一定会是一个和自己不同的字母这个缺陷,设计了一个叫 Bombe 的计算机,对加密消息尝试多种组合,如发现字母解密后和原先一样,这个组合就会被跳过,接着试另一组,因此 Bombe 大幅减少了搜索量,这样就能保证及时破解信息。

战争历史学家 Harry Hinsley 肯定了图灵和布莱切利园组织的工作,说由于他们的工作让战争缩短了两年多,挽救了1400万人的生命。

加密算法

如今加密技术怎样了呢?进入民用了么?我们能够利用加密技术保护我们的数据安全吗?

对称加密算法

加密技术从硬件转向了软件,早期加密算法是1977年的 DES。DES 是一种对称加密算法,它的原理是将明文分成64位的块,通过一系列的置换、替换和移位操作,使用一个56位的密钥对明文进行加密,得到64位的密文。意味着有2的56次方,或大约72千万亿个不同密钥。当时是没有计算能力可以暴力破解所有可能密钥的。

DES 加密算法的具体步骤如下:

  • 初始置换(IP):将明文按照一定的规则进行置换,得到一个新的64位明文。
  • 分组:将置换后的明文分成左右两个32位的块。
  • 轮函数:对右半部分进行一系列的置换、替换和移位操作,使用一个48位的子密钥对其进行加密。
  • 左右交换:将左半部分和右半部分进行交换。
  • 重复执行第3步和第4步,共进行16轮。
  • 合并:将左右两个32位的块合并成一个64位的块。
  • 末置换(FP):将合并后的块按照一定的规则进行置换,得到一个新的64位密文。

DES 解密算法的步骤与加密算法相反,主要是将加密算法中的子密钥按照相反的顺序使用,对密文进行解密。

DES 加密算法的安全性在当时是比较高的。

到了1999年,计算机芯片计算能力指数增加,一台计算机就能在几天内将 DES 的所有可能密钥都试一遍。因此,DES 已经不再被广泛使用,取而代之的是更加安全的加密算法,例如 AES。

2001年 AES 是一种对称加密算法,它的原理是将明文分成128位的块,通过一系列的置换、替换和移位操作,使用一个128位、192位或256位的密钥对明文进行加密,得到128位的密文。

AES 加密算法的具体步骤如下:

  • 密钥扩展:根据密钥长度,对密钥进行扩展,生成多个轮密钥。
  • 初始轮:将明文按照一定的规则进行置换,得到一个新的128位明文。
  • 轮函数:对明文进行一系列的置换、替换和移位操作,使用一个轮密钥对其进行加密。
  • 重复执行第3步,共进行多轮。
  • 末轮:对明文进行最后一轮的置换、替换和移位操作,使用最后一个轮密钥对其进行加密。
  • 得到密文。

AES 解密算法的步骤与加密算法相反,主要是将加密算法中的轮密钥按照相反的顺序使用,对密文进行解密。

AES 加密算法的安全性很高,主要基于其密钥长度和轮函数的复杂性。AES 支持三种密钥长度:128位、192位和256位,其中256位密钥的安全性最高。此外,AES 的轮函数使用了多种复杂的操作,例如有限域上的乘法和逆变换,使得密码破解变得更加困难。

AES 在性能和安全性间取得平衡。如今 AES 被广泛使用,比如 iPhone 上加密文件,访问 HTTPS 网站等。

进入互联网时代,以前加密技术中的密钥在网上传递过程中会被截获,截获到密钥就能够直接解密通信了。

那要怎么做才能够保证密钥不会被截获呢?

这就要用到密钥交换技术了。

密钥交换是一种不发送密钥,但依然让两台计算机在密钥上达成共识的算法。我们可以用单向函数来做。单向函数是一种数学操作,很容易算出结果,但想从结果逆向推算出输入非常困难。

密钥交换的原理是基于数学问题的难解性,例如离散对数问题。

其中,Diffie-Hellman 密钥交换协议是一种常见的密钥交换协议,在 Diffie-Hellman 里单向函数是模幂运算。意思是先做幂运算,拿一个数字当底数,拿一个数字当指数。其具体原理如下:

  • 选择两个大质数 p 和 g,其中 g 是 p 的原根。
  • 小明选择一个私钥 a,并计算 A=g^a(mod p),将 A 发送给小强。
  • 小强选择一个私钥 b,并计算 B=g^b(mod p),将 B 发送给小明。
  • 小明计算 s=B^a(mod p)
  • 小强计算 s=A^b(mod p)
  • 现在,小明和小强都拥有相同的密钥 s,可以在通信过程中使用它来加密和解密消息。

Diffie-Hellman 密钥交换协议的安全性基于离散对数问题的难解性,即使已知 p、g、A 和 B,也很难计算出 a 和 b。因此,Diffie-Hellman 密钥交换协议被广泛应用于安全通信和密钥交换等领域。

另外还可以用混色来比喻 Diffie-Hellman 密钥交换协议。

将颜色混合在一起很容易。但想知道混了什么颜色很难。要试很多种可能才知道,用这个比喻,那么我们的密钥是一种独特的颜色,首先,有一个公开的颜色 C,所有人都可以看到。然后小明和小强各自选一个秘密颜色 A 和颜色 C,只有自己知道,然后小明发给小强 A 和 C 的混色。小强也这样做,把他的秘密颜色 B 和公开颜色 C 混在一起,然后发给小明。小明收到小强的颜色后,把小明的秘密颜色 A 加进去,现在3种颜色混合在一起。小强也一样做。这样,小强和小明就有了一样的颜色。他们可以把这个颜色当密钥,尽管他们从来没有给对方发过这颜色。外部截获信息的人可以知道部分信息,但无法知道最终颜色。

Diffie-Hellman 密钥交换是建立共享密钥的一种方法。双方用一样的密钥加密和解密消息,这叫对称加密,因为密钥一样,凯撒加密,英格玛,AES 都是对称加密。

对称加密的内容两个人都能解密看到,如果加密的信息只想有一方可以解密查看就要用到非对称加密。非对称加密,有两个不同的密钥,一个是公开的,另一个是私有的,用公钥加密消息,只有有私钥的人能解密。

就好像把一个箱子和锁给你,你可以锁上箱子,但不能打开箱子,锁箱子就是公钥加密,能够打开箱子的是有钥匙的人,解锁就是私钥解密。

非对称加密算法

常见的非对称加密算法包括RSA、DSA和ECC等。目前最流行的非对称加密技术是 RSA。名字来自发明者:Rivest,Shamir,Adleman。

RSA 的原理是基于数学问题的难解性,例如大质数分解。RSA的具体原理如下:

  • 选择两个大质数 p 和 q,计算它们的乘积 n=p*q
  • 选择一个整数e,使得1<e<φ(n),且 e 与 φ(n) 互质,φ(n)=(p-1)*(q-1)
  • 计算 e 关于 φ(n) 的模反元素 d,即满足 e*d≡1(mod φ(n)) 的最小正整数 d。
  • 公钥为 (n,e),私钥为 (n,d)
  • 加密时,将明文 m 转换为整数 M,计算密文 C=M^e(mod n)
  • 解密时,将密文 C 计算出明文 m,即 M=C^d(mod n)

RSA 的安全性基于大质数分解的难度,即使已知公钥和密文,也很难计算出私钥。因此,RSA被广泛应用于数字签名、密钥交换和安全通信等领域。比如数字签名就是公钥来解密,大家都能公开看到签名内容,只有服务器端能够用私钥来加密,这样就能够证明签名是没有伪造的。

对称加密,密钥交换和公钥密码这些就是现代密码学。和图灵那个时代相比更加安全,加解密速度的提高让应用场景也更加地广泛了。

图灵机

图灵除了密码破译外还做了一件对现代计算机影响深远的事情。

1935年,德国数学家大卫·希尔伯特提出的问题,就是“可判定性问题”,可判定性问题是指是否存在一种算法,输入逻辑语句,可以判断是和否。

图灵发明了一种叫做图灵机的东西,这个机器可以模拟任何其他的计算机,通过图灵机回答了可判定性问题,这个问题虽然看似简单,但是实际上却相当复杂,因为涉及了形式语言的理论、递归的原理等概念。

图灵机可以用于证明停机问题,即判断一个给定的程序是否会在有限时间内停止运行。停机问题是计算机科学中的一个经典问题,它在理论上是不可解的,即不存在一种通用的算法可以解决所有停机问题。这个图灵机可以接受一个程序集合作为输入,并输出一个程序,该程序与输入集合中的所有程序的行为都不同。通过对这个图灵机的构造和分析,图灵证明了停机问题的不可解性。

具体来说,当程序不递归自己,输出停机,测试程序就调用它,使其不停机;如果程序递归调用自己,输出不停机,测试程序不调用它,使其停机。那么问题是测试程序递归调用自己时。

另外还有个更形象的和停机问题一样的理发师悖论,具体说就是有个理发师他有个原则,有人不能刮胡子,他刮;有人刮胡子,他不能刮。无法回答的问题是,理发师会自己刮胡子么?因为他能自己刮,但根据他的原则他又不能刮,但他不能刮的话他又要刮。

图灵机是图灵对计算机的设想,他假设时间足够多,存储足够大,图灵机可以实现任何计算,另外通过停机问题也证明了并不是所有问题都能用计算来解决,也就是提前证明了计算机的极限。开启了可计算性理论,也就是丘奇-图灵论题。

图灵机工作过程和人处理问题的过程类似,获取外部信息,处理当前信息,将处理结果暂存,接下来再获取新的信息重复这个过程。为了完成这个过程,图灵设计的机器有用于输入信息的纸带,处理信息的状态规则,暂存结果的状态寄存器,以及用于获取信息和存储信息的读写器。图灵机的工作过程为:

  • 从纸带上读取信息
  • 通过状态规则查找状态并按规则执行
  • 状态寄存器存储结果
  • 进入新状态
  • 重复过程

现代计算机的设计和实现受到了图灵机的启发。计算机的核心部件包括中央处理器(CPU)、存储器、输入输出设备等,这些部件的设计和实现都是基于图灵机的模型。例如,CPU 可以看作是图灵机的控制器,存储器可以看作是图灵机的纸带,输入输出设备可以看作是图灵机的输入输出接口。

另外,现代计算机的编程语言和算法也受到了图灵机的影响。图灵机可以模拟任何可计算的问题,因此它可以用来证明某个问题是可计算的,也可以用来设计算法和编写程序。现代计算机的编程语言和算法都是基于图灵机的模型,它们可以用来描述和解决各种计算问题。

总的来说现代计算机实现了图灵对计算机的设想,也深入到了我们每个人的生活。一些本来机器解决不了而人类可以解决的问题,机器也可以通过大量数据学习人类来解决。

计算机硬件发展史

接下来,我先简单介绍下计算机最核心的计算处理控制器发展,是怎么从图灵时代的继电器发展到现代 CPU 的。

上古时代–继电器时代

图灵所在二战时代最大的计算机叫哈佛一号,由哈佛大学和 IBM 公司合作研制,有76万5千个组件,300万个连接点和500英里长的导线。哈佛一号采用电子管和机械继电器作为计算元件,可以进行加、减、乘、除等基本运算,还可以进行对数、三角函数等高级运算。继电器是用电控制机械开关。可以把继电器控制线路想成水龙头,打开水龙头,水会流出来,关闭水龙头,水就没了。只不过继电器控制的是电子而不是水。机械开关速度有限,最好的继电器1秒翻转50次。

哈佛一号的体积庞大,重达 5 吨,占地面积达 51 平方米,需要 3 个人来操作。哈佛一号的设计和实现受到了图灵机的启发,它采用了分程序控制和存储程序的思想,可以根据不同的程序进行自动切换。哈佛一号的设计者之一霍华德·艾肯曾说过:“我们试图建造一台机器,它可以像人一样思考,但是我们失败了。相反,我们建造了一台机器,它可以像机器一样思考。”

哈佛一号的研制历时 11 年,耗资 500 万美元,是当时世界上最先进的计算机之一。

哈佛马克一号一秒3次加减,6秒乘法,15秒除法。更复杂操作比如三角函数需要1分钟以上。除了速度慢,齿轮也容易磨损,继电器数量多故障率也会增加,哈佛马克一号有3500个继电器。昆虫也会造成继电器故障,1947年9月操作员从故障继电器中拔出一只死虫,那时每当电脑出了问题,就说它出了 bug。这个就是术语 bug 的来源。

古代–真空管时代

继电器的替代品是真空管。真空管是一种电子器件,它的工作原理基于热电子发射和电子在真空中的运动。真空管由阴极、阳极和控制网格等部件组成,其中阴极是一个加热的金属丝,当温度升高时,会发射出大量的自由电子。这些电子被加速器电场加速,穿过控制网格,最终撞击到阳极上,产生电流。真空管内通过电流控制开闭实现继电器功能,由于真空管内没有会动的组件,这样速度更快,磨损更少,每秒可以开闭数千次。

真空管的工作过程可以分为三个阶段:发射阶段、传输阶段和收集阶段。在发射阶段,阴极发射出大量的自由电子,这些电子被加速器电场加速,形成电子流。在传输阶段,电子流穿过控制网格,受到网格电场的控制,形成一个电子束。在收集阶段,电子束撞击到阳极上,产生电流。

真空管的工作原理与晶体管等现代电子器件不同,它需要加热阴极才能发射电子,因此功耗较大,体积较大,寿命较短。但是真空管具有高功率、高频率、高压等特点,在一些特殊的应用场合仍然得到广泛应用。

真空管很贵,收音机一般只用一个,但计算机可能要上百甚至上千个。一般只有政府才会使用真空管做计算机。第一个大规模用真空管做的计算机是巨人一号,由工程师 Tommy Flower 设计,1943年12月完成。巨人一号在英国的布莱切利园里,用来破解日本的通信。巨人一号是基于图灵机的原理设计的,它采用了存储程序的思想,可以自动执行多个程序。同在布莱切利园的图灵的 bombe 机器没有使用真空管,而是使用的机械装置。核心部件是旋转轮机,它通过模拟密码机的运行过程来破解密码。Bombe 机器的工作原理与真空管电子计算机不同,它不需要电子元件,而是通过机械装置来实现计算和控制。巨人一号和图灵的 bombe 机器在破解密码的方式上也存在一些区别。巨人一号主要使用了穷举法和字典攻击等方法,而图灵的 bombe 则主要使用了差分密码分析等方法。

近现代–半导体时代

晶体管

计算机硬件技术真正实现突破沿用至今的时刻发生在1947年,当年为了降低计算机成本和大小,同时提高可靠性和速度,1947年贝尔实验室科学家 John BardeenWalter Brattain,William Shockley 发明了晶体管。晶体管由三个掺杂不同材料的半导体层构成,其中中间的层被称为基底,两侧的层被称为掺杂层。当掺杂层中注入电子或空穴时,它们会在基底中形成一个电子或空穴浓度较高的区域,这个区域被称为 PN 结。PN 结可以用来控制电流的流动,从而实现放大和开关电信号的功能。晶体管的发明是电子技术史上的重要里程碑,它的出现标志着电子器件从真空管时代进入了半导体时代。

晶体管的物理学相当复杂,牵扯到量子力学。晶体管有两个电极,电极之间有一种材料隔开他们,这种材料有时候导电,有时候不导电,叫半导体。半导体每秒可以开关10000次,与玻璃制作的真空管相比,晶体管是固态的,不容易坏,而且比真空管更小更便宜。

1957年 IBM 推出完全用晶体管的 IBM 608,由于便宜,消费者也可以买得到。它有3000个晶体管,每秒执行4500次加法,80次乘除法。IBM 将晶体管计算机带入千家万户。现在计算机里的晶体管小于50纳米,而一张纸的厚度大概是10万纳米。每秒可以切换上百万次,工作很多年。

晶体管和半导体的开发在圣克拉拉谷,半导体材料大部分是硅,硅很特别,它是半导体,它有时导电,有时不导电,我们可以控制导电时机,所以硅是做晶体管的绝佳材料。硅的蕴藏量丰富,占地壳四分之一,这个地方后来被称为硅谷

集成电路

1960年代,为了解决电子器件体积大、功耗高、可靠性差等问题。在德州仪器工作的 Jack Killby 把多个组件包在一起,变成一个新的独立组件,这个组件就是集成电路。Robert Noyce仙童半导体让集成电路变为现实。最开始一个 IC 只有几个晶体管,把简单电路,逻辑门封装成单独组件。

在集成电路中,数百万个晶体管、电容器、电阻器等元件被集成在一个芯片上,从而大大减小了电路的体积和功耗,提高了电路的可靠性和性能。

在集成电路出现之前,电子器件主要采用离散元件的方式进行组装。这种方式需要大量的电子元件,而且需要手工进行组装和连接,不仅体积大、功耗高,而且可靠性差。随着半导体技术的发展,人们开始尝试将多个晶体管、电容器、电阻器等元件集成在一个芯片上,从而形成了集成电路。

集成电路的出现极大地推动了电子技术的发展。它不仅使电子器件的体积和功耗大大减小,而且提高了电路的可靠性和性能。随着集成电路技术的不断发展,芯片上的晶体管数量不断增加,集成度不断提高。

为了创造更复杂的电路并能够大规模生产,出现了通过蚀刻金属线的方式,把零件连接到一起的印刷电路板技术,简称 PCB。是一种用于连接和支持电子元件的基板,它通过在表面覆盖一层导电材料(通常是铜)并在其上刻蚀出电路图案,从而实现电路的连接和布局。印刷电路板广泛应用于电子设备中,例如计算机、手机、电视等。

印刷电路板的制作过程通常包括以下几个步骤:

  • 设计电路图:首先需要根据电路的功能和布局设计电路图,通常使用电路设计软件进行设计。
  • 制作印刷电路板:将电路图转换为印刷电路板的图案,并使用光刻技术将图案转移到覆盖在基板上的光阻膜上。然后,使用化学蚀刻技术将未被光阻膜保护的铜层蚀刻掉,从而形成电路图案。
  • 镀金层:在印刷电路板表面镀上一层金属,通常是镀金,以提高电路板的导电性和耐腐蚀性。
  • 焊接元件:将电子元件焊接到印刷电路板上,通常使用表面贴装技术(Surface Mount Technology,SMT)或插件式技术(Through-Hole Technology,THT)。
  • 测试电路板:使用测试设备对印刷电路板进行测试,以确保电路板的功能和性能符合要求。

为了在相同体积下集成更多晶体管,全新的光刻工艺出现了,用光把复杂图案印到材料上,比如半导体。其基本原理是利用光敏材料对光的敏感性,通过光的照射和化学反应来形成所需的图案。光刻使用材料包括光掩膜,光刻胶,金属化,氧化层和晶圆。我们可以用晶圆做基础,把复杂金属电路放上面,集成所有东西,非常适合做集成电路。

光刻的基本步骤包括:

  • 涂覆光刻胶:将光刻胶涂覆在待加工的基板表面上,形成一层均匀的薄膜。
  • 曝光:将光刻胶暴露在紫外线下,通过掩膜将光刻胶暴露在特定的区域,形成所需的图案。
  • 显影:将光刻胶进行显影,将未暴露在紫外线下的光刻胶溶解掉,形成所需的图案。
  • 退光刻胶:使用退光刻胶剂将光刻胶进行退除,以便进行下一步的工艺步骤。

在曝光过程中,光刻胶中的光敏剂会吸收光子能量,从而发生化学反应,使得光刻胶在曝光区域发生物理或化学变化。在显影过程中,未曝光的光刻胶会被溶解掉,而曝光区域的光刻胶则会保留下来,形成所需的图案。在退光刻胶过程中,使用退光刻胶剂将光刻胶进行退除,以便进行下一步的工艺步骤。

用类似制作步骤,光刻可以制作其他电子元件,比如电阻和电容,都在一片硅上。而且互相连接的电路也做好了。现实中,光刻法一次会做上百万个细节。

有了光刻技术晶体管越来越小,密度也变得更高,戈登·摩尔发现了一个趋势,就是每两年相同空间所放晶体管数量会增加两倍,后来这个规律被称为摩尔定律。戈登·摩尔罗伯特·诺伊斯联手成立了一家新公司,结合 Intergrated 和 Electronics 两个词,取名 Intel,是现在最大的芯片制造商。CPU 晶体管数量按摩尔定律一直在指数级地增长,1980年,一个芯片有3万晶体管。到1990年达到了100万,2010年一个芯片里已经可以放进10亿晶体管,现在苹果 M1 Ultra 的晶体管数量约为1140亿。英特尔说,到2030年,芯片将拥有约1万亿个晶体管。先进的芯片中晶体管的尺寸是以纳米为单位,小到2纳米,比血红细胞小2800倍。除了 CPU 还有内存,显卡,固态硬盘和摄像头感光元件等都得益于光刻带来的摩尔定律发展。现在的电路设计都是超大规模集成(VLSI)自动生成的设计。

目前由于光的波长精度已经接近极限,因此需要波长更短的光源来投射更小图案。另外晶体管小到一定程度电极之间可能只有原子长,会发生量子隧道贯穿,也就是电子会跳过间隙。不过相信只要有需求,这些技术问题终将被克服。

那么究竟都有什么样的需求一直推动着计算机技术爆发增长呢?

计算机软件发展史

最早计算机的用途主要就是做数学计算,比如二战的炮手,需要根据射程和大气压力来计算近似多项式,多项式可以描述几个变量的关系,这些函数手算很麻烦耗时。Charles Babbage 提出一种新型机械装置叫差分机将欲求多项方程的前3个初始值输入到机器,推论出固定不变的差数,接下来每个值就可以将差数和前一个阶段的值相加得到。求多项方程的结果完全只需要用到加和减。

在19世纪末,美国人口10年一次普查,然而手工编制需要七年时间,编制完成已经过时了,1890年人口激增,手工编制普查数据需要13年之久。Herman Hollerith 发明了打孔卡片制表机,机器是电动机械的,用传统机械计数,用电动结构连接其他组件。用打孔来表示数据,每个孔代表一个二进制数码,机器会读取孔的位置将其转成数字,打孔卡片制表机的工作方式如下:

  • 使用打孔机将有关个人的数据记录在打孔卡片上。在卡片上打孔,代表一个人的姓名、年龄、职业等信息。
  • 打好的卡片被送入Hollerith的制表机。该机器有金属刷子,可以从卡片上的孔中穿过。
  • 当刷子经过一个开孔时,一个电路就会完成,一个计数器就会递增。计数器记录着有多少张牌具有某些特征。
  • 计数器还可以使用连接在机器上的打印机将结果打印在纸上。它将根据计数器的计数来打印数据的摘要。

与手工操作相比,Hollerith的系统加快了数据的统计过程,速度是手动的十倍。美国人口普查局在1890年采用了他的打孔卡系统,使他们能够在两年半内完成人口普查数据处理。

Herman Hollerith 后来成立了制表机器公司,服务于会计、保险评估和库存管理等数据密集行业。后来这家公司和其他公司合并后改名国际商业机器公司,简称 IBM。

二战时期及二战后冷战时期各国对计算机的需求达到了鼎盛,比如我前面提到图灵他们做的破译 Enigma 的机器。政府对计算机投入资源的时期是美国和苏联的冷战,这也得益于二战时计算机在曼哈顿计划和破解德军加密对自身价值的证明。其中阿波罗计划是投入经费最多的项目,雇了40多万人,还有2万多家大学和公司参与了其中。复杂轨道的计算需求是最大的,因此 NASA制造了阿波罗导航计算机,这台计算机首先使用了集成电路,当时首先使用了集成电路的价格是很贵的,一个芯片就需要五十多美元,而阿波罗导航计算机需要上千个这样的芯片。另外军事上,洲际导弹和核弹也促进了集成电路规模化生产。

随着冷战的结束,政府在计算机上的投入也逐渐减少,计算机迎来了家用消费级时代。

70年代初,计算机各个组件的成本都有大幅下降,可以做出低成本适用于个人使用的电脑,第一台取得商业成功的个人计算机是 Altair 8800,很多计算机爱好者都会购买,计算机的程序要用机器码编写,由于编写麻烦,比尔·盖茨编写了 BASIC 解释器,可以将 BASIC 代码转换成可执行机器码,这个解释器叫 Altair BASIC,也是微软的第一个产品。

24岁的 Steve Wozniak 受到 Altair 8800 的启发,做了一台自己的计算机,他的同学 Steve Jobs 看中了其中机会,1976年4月1日创立了苹果计算机公司,1976年7月开始将 Steve Wozniak 设计的计算机进行售卖,这也是苹果计算机公司的第一款产品。后来苹果的 Apple-II 卖了上百万套,苹果公司一战成名。

和苹果的封闭架构不同的是 IBM 发布的 IBM PC,IBM PC 采用的是开放式架构,这样每个公司都可以遵循这个标准做出自己的计算机,核心硬件和外设都可以有不同组合,这样的计算机也称为 IBM 兼容计算机。

开放的架构也繁荣了生态,更多公司比如康柏和戴尔加入了个人计算机领域。

让计算机进入更多普通人家庭的是交互上的革命。

1984年苹果发布了 Macintosh,使用图形界面取代了用命令行交互的终端。

更多用户对计算机的使用也带来视觉和听觉感官的诉求。那么图形和声音是怎么让计算机识别处理和保存的呢?

当一个图像以特定的格式保存时,构成图像的数字数据–像素和它们的颜色值–被编码并根据该格式的规范进行压缩。该文件还包含元数据,如图像大小、分辨率和色彩模式。

图像文件格式决定了数字数据的组织和压缩方式,图像文件格式的主要类型有:

  • JPEG:一种 “有损 “的压缩格式,通常用于照片。它压缩图像数据以减少文件大小,导致图像质量的一些损失。
  • PNG:一种 “无损 “的压缩格式,适用于带有文字、线条和图形的图像。用这种格式保存时,没有图像质量的损失。
  • GIF: 一种适用于颜色数量有限的图像的格式,通常用于网络上的简单图形和动画。
  • BMP: 一种未压缩的格式,存储图像的精确像素数据。BMP文件的尺寸往往很大。

数字音频文件是由代表音频波形的二进制数据组成。文件格式规定了这种二进制数据的结构和组织方式,以表示音频样本、比特深度、采样率、通道数量和其他元数据。像媒体播放器这样的计算机程序可以读取文件格式并解码二进制数据以播放音频。

常见的音频文件格式包括:

  • WAV:一种标准的未压缩的音频格式,由原始样本组成。WAV文件往往尺寸较大,但具有较高的音频质量。
  • MP3: 一种压缩的音频格式,使用有损压缩来减少文件大小。MP3文件较小,但与WAV相比,其音频质量略低。
  • AAC: 另一种压缩的音频格式,提供良好的压缩率,同时保持相对较高的音频质量。AAC文件通常用于iPod等设备。
  • FLAC: 一种无损压缩的音频格式,在保留所有原始音频信息和质量的同时压缩文件以减小尺寸。

如今,计算机已经可以大致模拟出我们所能感受到的东西,而图灵对计算机的构想也正随着硬件高速发展而逐步被实现,并走进每一个人的生活。关于图灵证明的计算机的极限,计算机已通过学习大量数据来模仿人类进行突破,学会根据情况忽略一些悖论来避免宕机。和计算机不同,我们的生命有限,记忆的容量有限,但也正因为如此,我们才能更好地享受和珍惜每一次对未知事物探索过程的回忆,而不是结果。

一、VSCODE 与 Android Lua Helper 的功能特点

Visual Studio Code(VSCODE)是一款功能强大的代码编辑器,它以其高度可定制的界面、强大的扩展生态系统、流畅的性能表现以及对众多编程语言的天然支持而备受开发者青睐。在众多的开发场景中,VSCODE 都展现出了卓越的性能和灵活性。
Lua 作为一种轻量级的脚本语言,在游戏开发、移动应用开发等领域有着广泛的应用。然而,由于 Lua 是一门小众语言,相关的开发工具并不像主流语言那样完善。Android Lua Helper 插件的出现,为开发者提供了一系列强大的功能,极大地提高了 Lua 代码的开发效率和质量。
Android Lua Helper 插件具有多种功能,如符号定义跳转、代码格式化、符号查找、全局引用查找以及智能代码补全、语法错误检测、Lua 代码片段提示等。这些功能使得开发者在编写 Lua 代码时更加高效和准确。例如,代码补全功能可以大大减少开发者的输入时间,提高开发速度;语法错误检测功能可以帮助开发者及时发现并修复代码中的错误,避免在运行时出现问题。Android Lua Helper插件还拥有低内存消耗和高实时性的优点,即便面对规模庞大的项目,也能流畅运行,毫无卡顿之感。
此外,Android Lua Helper 插件还支持多种 Lua 版本,如 Lua 5.1Lua 5.3,满足了不同项目的需求。插件的不断更新和改进也为开发者提供了更好的开发体验。Android Lua Helper 支持使用安卓 ADB 工具连接手机,实现远程调试,安装卸载应用,截屏到本地,启动scrcpy等。
总之,VSCODEAndroid Lua Helper 的结合为 Android 开发中的 Lua 语言项目提供了强大的支持,使得开发者能够更加高效地进行开发工作。

二、VSCODE 与 Android Lua Helper 的集成

首先,我们需要安装 VSCODE,然后安装 Android Lua Helper 插件。
点击此链接直接在VSCODE中安装,或在 VSCODE 的软件商店安装 Android Lua Helper 插件极为简便。首先打开 VSCODE,目光聚焦于侧边栏底部,那里有一个扩展商店入口图标,形似方块。轻轻点击此图标,便会开启一个全新视图,其中罗列着众多插件。接着,在搜索栏中输入 Android Lua Helper 并按下回车键。随后,在搜索结果里找到 Android Lua Helper 插件,点击 “安装” 按钮。待安装完成,再点击 “重新加载” 按钮以启用该插件。
插件安装完成后,我们需要配置 VSCODE,以支持 Android Lua Helper 的调试。

三、调试配置全流程

(一)创建 launch.json 文件

在安装好 Android Lua Helper 插件后,点击运行按钮,接着点击创建 launch.json,选择调试器时,我们要选择 AndroidLuaHelper:Debug。这个过程就像是为我们的调试之旅搭建起了一座桥梁。它为后续的调试工作提供了基础的配置框架,确保我们能够顺利地进行 Lua 代码的调试。 1
launch.json 文件中,我们添加了如下内容:4

  • "connectionPort" 字段用于指定调试端口,默认为 8818
  • stopOnEntry 字段尤为关键。通过将其置为 false,可以避免在程序入口处进行断点,使得调试过程更加流畅。例如,在一些复杂的项目中,如果 stopOnEntrytrue,可能会导致调试过程一开始就被中断,难以快速定位到实际的问题点。通过将其置为 false,我们可以更加灵活地控制调试的起点,提高调试效率。
  • enableRemotePath 字段用于是否启用远程路径,默认为 true
  • remotePath 字段用于指定远程路径,默认为 /sdcard/Download/script.zip。会把本地的工程文件打包成zip包,上传到手机的此目录。APP需要加载此目录的脚本来进行解压,然后运行和调试。

(二)添加LuaPanda.lua文件到项目中

按快捷键Ctrl+Shift+P,输入AndroidLuaHelper:Copy Debug file,再按回车键。
选择合适的项目目录后LuaPanda.lua文件会自动复制到目录下,并命名为 LuaPanda.lua
2

(三)插入调试代码到合适的文件

在合适的文件(如:main.lua)中,按快捷键Ctrl+Shift+P,输入AndroidLuaHelper:Insert Debugger Code,再按回车键。
插入如下代码:
3
默认端口为 8818,与 launch.json 文件中的"connectionPort"要一致。

(四)连接到安卓手机

Android Lua Helper 会自动检测手机,如果检测不到,则需要手动输入。
点击边栏上的安卓按钮,接着点击 Input Device IP Address按钮,或按快捷键Ctrl+Shift+P,输入AndroidLuaHelper:Input Device IP Address,再按回车键,依次输入IP地址和端口号。
5

(五)运行

在需要的位置点入断点,点击运行按钮,选择启动调试,调试器会进入监听模式,在手机上运行App即可进行调试。

我们用IDA去分析二进制中代码的逻辑。而二进制本身其实只有`0`和`1`二进制数据而已。而想要分析代码,即查看对应二进制对应的`汇编代码`(以及后续的`伪代码`),所包含的函数,所包含的字符串等等信息,则就需要IDA对二进制进行充分的分析,最后才能显示出我们要的上述的各种信息。

阅读全文 »

  1. 进入官方语言翻译网页:https://translations.telegram.org
  2. 点击网页中的 Start Translating 地址,就会弹出选择语言的界面
  3. 选择对应的语言之后会跳转到对应的网页,然后点击下面 Actions 下的 Use Telegram in Xxx (Xxx表示对应的语言)
  4. 之后会在新页面内唤醒本地安装的 Telegram 软件并提示你切换为对应语言,确认即可(多端都支持)
    注:也可以使用 Sharing Link 内的链接发送给任何人,点击访问后也会唤起软件自动设置为对应语言

设置为简体中文

如果你不想自己找,我这里给出官方的简中语言包设置地址:
简体中文
繁体中文
俄语
日语
乌克兰

经过以上操作,就可以开心的使用软件翻译语言包啦~

函数实现逻辑在llvm/lib/Transforms/Obfuscation/StringEncryption.cpp文件中,IndirectBranch,集成自类ModulePass,实现了runOnModule函数

Module(模块):

Module是LLVM的最高级别的组织单元,它代表一个编译单元或一个独立的代码模块
Module包含了全局变量、函数定义、类型定义等
一个Module可以包含多个Function
Function(函数):

Function代表一个具体的函数,包含函数的定义和实现
Function定义了函数的参数类型、返回类型、函数名等信息
Function还包含了函数的基本块(Basic Block)和指令(Instruction)
在LLVM的编译过程中,首先创建一个Module,然后在Module中创建和添加Function,最后为每个Function添加基本块和指令

一、字符串加密的实现逻辑

1.1 字符串收集

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// llvm/lib/Transforms/Obfuscation/StringEncryption.cpp

std::set<GlobalVariable *> ConstantStringUsers;

// collect all c strings

LLVMContext &Ctx = M.getContext();
ConstantInt *Zero = ConstantInt::get(Type::getInt32Ty(Ctx), 0);
for (GlobalVariable &GV : M.globals()) {
if (!GV.isConstant() || !GV.hasInitializer()) {
continue;
}
// 获取module下面的全局变量
Constant *Init = GV.getInitializer();
if (Init == nullptr)
continue;
if (ConstantDataSequential *CDS = dyn_cast<ConstantDataSequential>(Init)) {
if (CDS->isCString()) {
CSPEntry *Entry = new CSPEntry();
StringRef Data = CDS->getRawDataValues();
Entry->Data.reserve(Data.size());
// 保存字符数据到Data字段
for (unsigned i = 0; i < Data.size(); ++i) {
Entry->Data.push_back(static_cast<uint8_t>(Data[i]));
}
Entry->ID = static_cast<unsigned>(ConstantStringPool.size());
ConstantAggregateZero *ZeroInit = ConstantAggregateZero::get(CDS->getType());
GlobalVariable *DecGV = new GlobalVariable(M, CDS->getType(), false, GlobalValue::PrivateLinkage,
ZeroInit, "dec" + Twine::utohexstr(Entry->ID) + GV.getName());
GlobalVariable *DecStatus = new GlobalVariable(M, Type::getInt32Ty(Ctx), false, GlobalValue::PrivateLinkage,
Zero, "dec_status_" + Twine::utohexstr(Entry->ID) + GV.getName());
DecGV->setAlignment(GV.getAlignment());
Entry->DecGV = DecGV;
Entry->DecStatus = DecStatus;
ConstantStringPool.push_back(Entry);
CSPEntryMap[&GV] = Entry;
collectConstantStringUser(&GV, ConstantStringUsers);
}
}
}

ConstantStringPool收集CSPEntry实例,包含字符串 CSPEntryMap包含对应的GV

1.2 字符加密并构建解密函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
// llvm/lib/Transforms/Obfuscation/StringEncryption.cpp

for (CSPEntry *Entry: ConstantStringPool) {
// 生成enckey,针对每个module不同
getRandomBytes(Entry->EncKey, 16, 32);
// 每个字符串进行加密
for (unsigned i = 0; i < Entry->Data.size(); ++i) {
Entry->Data[i] ^= Entry->EncKey[i % Entry->EncKey.size()];
}
// 为每个module的解密函数生成
Entry->DecFunc = buildDecryptFunction(&M, Entry);
}

void StringEncryption::getRandomBytes(std::vector<uint8_t> &Bytes, uint32_t MinSize, uint32_t MaxSize) {
uint32_t N = RandomEngine.get_uint32_t();
uint32_t Len;

assert(MaxSize >= MinSize);

if (MinSize == MaxSize) {
Len = MinSize;
} else {
Len = MinSize + (N % (MaxSize - MinSize));
}

char *Buffer = new char[Len];
RandomEngine.get_bytes(Buffer, Len);
for (uint32_t i = 0; i < Len; ++i) {
Bytes.push_back(static_cast<uint8_t>(Buffer[i]));
}

delete[] Buffer;
}

Function *StringEncryption::buildDecryptFunction(Module *M, const StringEncryption::CSPEntry *Entry) {
LLVMContext &Ctx = M->getContext();
IRBuilder<> IRB(Ctx);
// 根据开头所说,module包含func、func包含块,因此创建逻辑也根据此
FunctionType *FuncTy = FunctionType::get(Type::getVoidTy(Ctx), {IRB.getInt8PtrTy(), IRB.getInt8PtrTy()}, false);
// 函数创建
Function *DecFunc =
Function::Create(FuncTy, GlobalValue::PrivateLinkage, "goron_decrypt_string_" + Twine::utohexstr(Entry->ID), M);
// 参数
auto ArgIt = DecFunc->arg_begin();
Argument *PlainString = ArgIt; // output
++ArgIt;
Argument *Data = ArgIt; // input

PlainString->setName("plain_string");
PlainString->addAttr(Attribute::NoCapture);
Data->setName("data");
Data->addAttr(Attribute::NoCapture);
Data->addAttr(Attribute::ReadOnly);

// 创建块
BasicBlock *Enter = BasicBlock::Create(Ctx, "Enter", DecFunc);
BasicBlock *LoopBody = BasicBlock::Create(Ctx, "LoopBody", DecFunc);
BasicBlock *UpdateDecStatus = BasicBlock::Create(Ctx, "UpdateDecStatus", DecFunc);
BasicBlock *Exit = BasicBlock::Create(Ctx, "Exit", DecFunc);

IRB.SetInsertPoint(Enter);
ConstantInt *KeySize = ConstantInt::get(Type::getInt32Ty(Ctx), Entry->EncKey.size());
Value *EncPtr = IRB.CreateInBoundsGEP(Data, KeySize);
Value *DecStatus = IRB.CreateLoad(Entry->DecStatus);
Value *IsDecrypted = IRB.CreateICmpEQ(DecStatus, IRB.getInt32(1));
IRB.CreateCondBr(IsDecrypted, Exit, LoopBody);

IRB.SetInsertPoint(LoopBody);
PHINode *LoopCounter = IRB.CreatePHI(IRB.getInt32Ty(), 2);
LoopCounter->addIncoming(IRB.getInt32(0), Enter);

Value *EncCharPtr = IRB.CreateInBoundsGEP(EncPtr, LoopCounter);
Value *EncChar = IRB.CreateLoad(EncCharPtr);
Value *KeyIdx = IRB.CreateURem(LoopCounter, KeySize);

Value *KeyCharPtr = IRB.CreateInBoundsGEP(Data, KeyIdx);
Value *KeyChar = IRB.CreateLoad(KeyCharPtr);

Value *DecChar = IRB.CreateXor(EncChar, KeyChar);
Value *DecCharPtr = IRB.CreateInBoundsGEP(PlainString, LoopCounter);
IRB.CreateStore(DecChar, DecCharPtr);

Value *NewCounter = IRB.CreateAdd(LoopCounter, IRB.getInt32(1), "", true, true);
LoopCounter->addIncoming(NewCounter, LoopBody);

Value *Cond = IRB.CreateICmpEQ(NewCounter, IRB.getInt32(static_cast<uint32_t>(Entry->Data.size())));
IRB.CreateCondBr(Cond, UpdateDecStatus, LoopBody);

IRB.SetInsertPoint(UpdateDecStatus);
IRB.CreateStore(IRB.getInt32(1), Entry->DecStatus);
IRB.CreateBr(Exit);

IRB.SetInsertPoint(Exit);
IRB.CreateRetVoid();

return DecFunc;
}

对ConstantStringPool中的字符串进行加密并生成解密函数

1.3 init函数构建

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// build initialization function for supported constant string users
for (GlobalVariable *GV: ConstantStringUsers) {
if (isValidToEncrypt(GV)) {
Type *EltType = GV->getType()->getElementType();
ConstantAggregateZero *ZeroInit = ConstantAggregateZero::get(EltType);
GlobalVariable *DecGV = new GlobalVariable(M, EltType, false, GlobalValue::PrivateLinkage,
ZeroInit, "dec_" + GV->getName());
DecGV->setAlignment(GV->getAlignment());
GlobalVariable *DecStatus = new GlobalVariable(M, Type::getInt32Ty(Ctx), false, GlobalValue::PrivateLinkage,
Zero, "dec_status_" + GV->getName());
CSUser *User = new CSUser(GV, DecGV);
User->DecStatus = DecStatus;
User->InitFunc = buildInitFunction(&M, User);
CSUserMap[GV] = User;
}
}

每个GV都生成CSUser并保存在CSUserMap中

1.4 离散字符串常量池

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// emit the constant string pool
// | junk bytes | key 1 | encrypted string 1 | junk bytes | key 2 | encrypted string 2 | ...
std::vector<uint8_t> Data;
std::vector<uint8_t> JunkBytes;

JunkBytes.reserve(32);
for (CSPEntry *Entry: ConstantStringPool) {
JunkBytes.clear();
// 生成垃圾代码
getRandomBytes(JunkBytes, 16, 32);
// 插入垃圾代码在enckey之前
Data.insert(Data.end(), JunkBytes.begin(), JunkBytes.end());
Entry->Offset = static_cast<unsigned>(Data.size());
Data.insert(Data.end(), Entry->EncKey.begin(), Entry->EncKey.end());
Data.insert(Data.end(), Entry->Data.begin(), Entry->Data.end());
}
Constant *CDA = ConstantDataArray::get(M.getContext(), ArrayRef<uint8_t>(Data));
EncryptedStringTable = new GlobalVariable(M, CDA->getType(), true, GlobalValue::PrivateLinkage,
CDA, "EncryptedStringTable");

保存全量的加密字符串

1.5 动态解密

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
bool Changed = false;
for (Function &F:M) {
if (F.isDeclaration())
continue;
Changed |= processConstantStringUse(&F);
}

for (auto &I : CSUserMap) {
CSUser *User = I.second;
Changed |= processConstantStringUse(User->InitFunc);
}

// delete unused global variables
deleteUnusedGlobalVariable();
for (CSPEntry *Entry: ConstantStringPool) {
if (Entry->DecFunc->use_empty()) {
Entry->DecFunc->eraseFromParent();
}
}

包括加密字符串的处理和未使用的全局变量的删除

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
bool StringEncryption::processConstantStringUse(Function *F) {
......
LowerConstantExpr(*F);
SmallPtrSet<GlobalVariable *, 16> DecryptedGV; // if GV has multiple use in a block, decrypt only at the first use
bool Changed = false;
for (BasicBlock &BB : *F) {
DecryptedGV.clear();
for (Instruction &Inst: BB) {
// 处理每行指令
if (PHINode *PHI = dyn_cast<PHINode>(&Inst)) {
for (unsigned int i = 0; i < PHI->getNumIncomingValues(); ++i) {
if (GlobalVariable *GV = dyn_cast<GlobalVariable>(PHI->getIncomingValue(i))) {
auto Iter1 = CSPEntryMap.find(GV);
auto Iter2 = CSUserMap.find(GV);
if (Iter2 != CSUserMap.end()) { // GV is a constant string user
CSUser *User = Iter2->second;
if (DecryptedGV.count(GV) > 0) {
Inst.replaceUsesOfWith(GV, User->DecGV);
} else {
Instruction *InsertPoint = PHI->getIncomingBlock(i)->getTerminator();
IRBuilder<> IRB(InsertPoint);
IRB.CreateCall(User->InitFunc, {User->DecGV});
Inst.replaceUsesOfWith(GV, User->DecGV);
MaybeDeadGlobalVars.insert(GV);
DecryptedGV.insert(GV);
Changed = true;
}
} else if (Iter1 != CSPEntryMap.end()) { // GV is a constant string
CSPEntry *Entry = Iter1->second;
if (DecryptedGV.count(GV) > 0) {
// 字符串替换成加密字符串
Inst.replaceUsesOfWith(GV, Entry->DecGV);
} else {
Instruction *InsertPoint = PHI->getIncomingBlock(i)->getTerminator();
IRBuilder<> IRB(InsertPoint);

Value *OutBuf = IRB.CreateBitCast(Entry->DecGV, IRB.getInt8PtrTy());
Value *Data = IRB.CreateInBoundsGEP(EncryptedStringTable, {IRB.getInt32(0), IRB.getInt32(Entry->Offset)});
IRB.CreateCall(Entry->DecFunc, {OutBuf, Data});

Inst.replaceUsesOfWith(GV, Entry->DecGV);
MaybeDeadGlobalVars.insert(GV);
DecryptedGV.insert(GV);
Changed = true;
}
}
}
}
} else {
for (User::op_iterator op = Inst.op_begin(); op != Inst.op_end(); ++op) {
if (GlobalVariable *GV = dyn_cast<GlobalVariable>(*op)) {
auto Iter1 = CSPEntryMap.find(GV);
auto Iter2 = CSUserMap.find(GV);
if (Iter2 != CSUserMap.end()) {
CSUser *User = Iter2->second;
if (DecryptedGV.count(GV) > 0) {
Inst.replaceUsesOfWith(GV, User->DecGV);
} else {
IRBuilder<> IRB(&Inst);
IRB.CreateCall(User->InitFunc, {User->DecGV});
Inst.replaceUsesOfWith(GV, User->DecGV);
MaybeDeadGlobalVars.insert(GV);
DecryptedGV.insert(GV);
Changed = true;
}
} else if (Iter1 != CSPEntryMap.end()) {
CSPEntry *Entry = Iter1->second;
if (DecryptedGV.count(GV) > 0) {
Inst.replaceUsesOfWith(GV, Entry->DecGV);
} else {
IRBuilder<> IRB(&Inst);

Value *OutBuf = IRB.CreateBitCast(Entry->DecGV, IRB.getInt8PtrTy());
Value *Data = IRB.CreateInBoundsGEP(EncryptedStringTable, {IRB.getInt32(0), IRB.getInt32(Entry->Offset)});
IRB.CreateCall(Entry->DecFunc, {OutBuf, Data});
Inst.replaceUsesOfWith(GV, Entry->DecGV);
MaybeDeadGlobalVars.insert(GV);
DecryptedGV.insert(GV);
Changed = true;
}
}
}
}
}
}
}
return Changed;
}

清空未使用的变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
void StringEncryption::deleteUnusedGlobalVariable() {
bool Changed = true;
while (Changed) {
Changed = false;
for (auto Iter = MaybeDeadGlobalVars.begin(); Iter != MaybeDeadGlobalVars.end();) {
GlobalVariable *GV = *Iter;
if (!GV->hasLocalLinkage()) {
++Iter;
continue;
}

GV->removeDeadConstantUsers();
if (GV->use_empty()) {
if (GV->hasInitializer()) {
Constant *Init = GV->getInitializer();
GV->setInitializer(nullptr);
if (isSafeToDestroyConstant(Init))
Init->destroyConstant();
}
Iter = MaybeDeadGlobalVars.erase(Iter);
GV->eraseFromParent();
Changed = true;
} else {
++Iter;
}
}
}
}

Lua是一个小巧的脚本语言,其设计目的是为了通过灵活嵌入应用程序中从而为应用程序提供灵活的扩展和定制功能。Lua由标准C编写而成,几乎在所有操作系统和平台上都可以编译,运行。Lua并没有提供强大的库,这是由它的定位决定的。

Lua脚本可以很容易的被C/C++ 代码调用,也可以反过来调用C/C++的函数,这使得Lua在应用程序中可以被广泛应用。不仅仅作为扩展脚本,也可以作为普通的配置文件,代替XML,ini等文件格式,并且更容易理解和维护。

运行可以通过 Lua 的交互模式,也可以用记事本编辑代码保存为 .lua 的格式,通过 Lua 编译器运行。也可以通过第三方工具,将 Lua 打包独立运行。

特性

  • 轻量级: Lua语言的官方版本只包括一个精简的核心和最基本的库。这使得Lua体积小、启动速度快。

源码行数对比表

语言 行数
python所有c源码 54万行
python核心c源码 约17万行
lua所有c源码 约2.4万行
  • 可扩展: Lua提供了非常易于使用的扩展接口和机制:由宿主语言(通常是C或C++)提供这些功能,Lua可以使用它们,就像是本来就内置的功能一样。

用途

其设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。大多数游戏和一些应用都是用lua嵌入其他语言中使用。

  1. 游戏开发
  2. 独立应用脚本
  3. Web 应用脚本
  4. 扩展和数据库插件如:MySQL Proxy 和 MySQL WorkBench
  5. 安全系统,如入侵检测系统

基本语法

  • 交互式编程模式: lua -i 或 lua
1
2
docker lua -i
Lua 5.1.5 Copyright (C) 1994-2012 Lua.org, PUC-Rio
  • 脚本式编程: 类似于python脚本。lua test.lua或 加#!/usr/local/bin/lua直接运行test.lua

  • 注释: 单行-- 、多行--[[ text --]]

  • 标识符和关键词:
    最好不要使用下划线+大写字母、不允许使用@ $ %定义标识符,区分大小写。

and break do else
elseif end false for
function if in local
nil not or repeat
return then true until
while goto
  • lua数据类型:

Lua 中有 8 个基本类型分别为:nil、boolean、number、string、userdata、function、thread 和 table。

数据类型 描述
nil 这个最简单,只有值nil属于该类,表示一个无效值(在条件表达式中相当于false)。
boolean 包含两个值:false和true。
number 表示双精度类型的实浮点数
string 字符串由一对双引号或单引号来表示
function 由 C 或 Lua 编写的函数
userdata 表示任意存储在变量中的C数据结构
thread 表示执行的独立线路,用于执行协同程序
table Lua 中的表(table)其实是一个”关联数组”(associative arrays),数组的索引可以是数字、字符串或表类型。在 Lua 里,table 的创建是通过”构造表达式”来完成,最简单构造表达式是{},用来创建一个空表。

局部变量: local b = 5,全局不需要

  • 函数:
    格式:function … end,可多返回值,变参...
    1
    2
    3
    4
    function foo()
    c = 5
    return c
    end

select(‘#’, …) 返回可变参数的长度。
select(n, …) 用于返回从起点 n 开始到结束位置的所有参数列表

  • 循环:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    while(condition)
    do
    statements
    end

    for var=exp1,exp2,exp3 do
    <执行体>
    end
    -- var 从 exp1 变化到 exp2,每次变化以 exp3 为步长递增 var,并执行一次 "执行体"。exp3 是可选的,如果不指定,默认为1。

    --泛型for循环
    --打印数组a的所有值
    a = {"one", "two", "three"}
    for i, v in ipairs(a) do
    print(i, v)
    end
    -- i是数组索引值,v是对应索引的数组元素值。ipairs是Lua提供的一个迭代器函数,用来迭代数组。

    repeat
    statements
    until( condition )
    各个循环可嵌套。

pairs 和 ipairs异同
同:都是能遍历集合(表、数组)

异:ipairs 仅仅遍历值,按照索引升序遍历,索引中断停止遍历。即不能返回 nil,只能返回数字 0,如果遇到 nil 则退出。它只能遍历到集合中出现的第一个不是整数的 key。

pairs 能遍历集合的所有元素。即 pairs 可以遍历集合中所有的 key,并且除了迭代器本身以及遍历表本身还可以返回 nil。

  • 流程控制:
    Lua认为false和nil为假,true和非nil为真。
    要注意的是Lua中 0 为 true
    1
    2
    3
    4
    5
    --[ 0 为 true ]
    if(0)
    then
    print("0 为 true")
    end
  • 运算符:
    算术运算符:
操作符 描述 实例
+ 加法 A + B 输出结果 30
- 减法 A - B 输出结果 -10
* 乘法 A * B 输出结果 200
/ 除法 B / A 输出结果 2
% 取余 B % A 输出结果 0
^ 乘幂 A^2 输出结果 100
- 负号 -A 输出结果 -10

逻辑运算符:

操作符 描述 实例
== 等于,检测两个值是否相等,相等返回 true,否则返回 false (A == B) 为 false。
~= 不等于,检测两个值是否相等,不相等返回 true,否则返回 false (A ~= B) 为 true。
> 大于,如果左边的值大于右边的值,返回 true,否则返回 false (A > B) 为 false。
< 小于,如果左边的值大于右边的值,返回 false,否则返回 true (A < B) 为 true。
>= 大于等于,如果左边的值大于等于右边的值,返回 true,否则返回 false (A >= B) 返回 false。
<= 小于等于, 如果左边的值小于等于右边的值,返回 true,否则返回 false (A <= B) 返回 true。

逻辑运算符:

操作符 描述 实例
and 逻辑与操作符。 若 A 为 false,则返回 A,否则返回 B。 (A and B) 为 false。
or 逻辑或操作符。 若 A 为 true,则返回 A,否则返回 B。 (A or B) 为 true。
not 逻辑非操作符。与逻辑运算结果相反,如果条件为 true,逻辑非为 false。 not(A and B) 为 true。
lua5.1没有异或等位算数运算符。后面到5.3支持位运算,可以在opcodes中看到BXOR等操作指令。

其他运算符:

操作符 描述 实例
连接两个字符串 a…b ,其中 a 为 “Hello “ , b 为 “World”, 输出结果为 “Hello World”。
# 一元运算符,返回字符串或表的长度。 #“Hello” 返回 5

运算符优先级:

由高到低
^
not - (unary)
* /
+ -
..
< > <= >= ~= ==
and
or
  • 模块与包:
    模块类似于一个封装库,从 Lua 5.1 开始,Lua 加入了标准的模块管理机制,可以把一些公用的代码放在一个文件里,以 API 接口的形式在其他地方调用,有利于代码的重用和降低代码耦合度。

以下为创建自定义模块 module.lua,文件代码格式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
-- 文件名为 module.lua
-- 定义一个名为 module 的模块
module = {}

-- 定义一个常量
module.constant = "这是一个常量"

-- 定义一个函数
function module.func1()
io.write("这是一个公有函数!\n")
end

local function func2()
print("这是一个私有函数!")
end

function module.func3()
func2()
end

return module

使用require加载模块:

1
2
3
4
5
6
7
8
9
10
11
12
-- test_module.lua 文件
-- module 模块为上文提到到 module.lua
require("module")
-- 别名变量 m
-- local m = require("module")
print(module.constant)
module.func3()
--[[
result:
这是一个常量
这是一个私有函数!
--[[

lua、c互调例子,lua调用so库例子

  • lua栈:
    lua中的栈是一个很奇特的数据结构,普通的栈只有一排索引,但是在lua中有两排索引,正数1索引的位置在栈底,负数-1索引的位置在栈顶。如下图所示。
1
2
3
4
5
6
  _______________
5 |____data5____| -1
4 |____data4____| -2
3 |____data3____| -3
2 |____data2____| -4
1 |____data1____| -5
  • 当索引是1的时候对应的是栈底
  • 当索引是-1的时候对于的是栈顶。
1
2
3
4
5
6
  _______________
5 |____.....____| -1
4 |____"str"____| -2
3 |____"343"____| -3
2 |___"table"___| -4
1 |____"func"___| -5
  • lua_pushcclosure(L, func, 0) // 创建并压入一个闭包
  • lua_createtable(L, 0, 0) // 新建并压入一个表
  • lua_pushnumber(L, 343) // 压入一个数字
  • lua_pushstring(L, “mystr”) // 压入一个字符串
    存入栈的数据类型包括数值, 字符串, 指针, talbe, 闭包等。
    压入的值在C看来是不同类型的,在lua看来都是TValue结构。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
typedef struct lua_TValue {
Value value;
int tt
} TValue;

/*
** Union of all Lua values
*/
typedef union {
GCObject *gc;
void *p;
lua_Number n;
int b;
} Value;

/*
** Union of all collectable objects
*/
union GCObject {
GCheader gch;
union TString ts;
union Udata u;
union Closure cl;
struct Table h;
struct Proto p;
struct UpVal uv;
struct lua_State th; /* thread */
};

TValue结构对应于lua中的所有数据类型, 是一个{值, 类型} 结构, 这就lua中动态类型的实现, 它把值和类型绑在一起, 用tt记录value的类型, value是一个联合结构, 由Value定义, 可以看到这个联合有四个域, 先简单的说明:

  • p –> 可以存一个指针, 实际上是lua中的light userdata结构

  • n –> 所有的数值存在这里, 不光是int , 还是float

  • b –> Boolean值存在这里, 注意, lua_pushinteger不是存在这里, 而是存在n中, b只存布尔

  • gc –> 其他诸如table, thread, closure, string需要内存管理垃圾回收的类型都存在这里
    gc是一个指针, 它可以指向的类型由联合体GCObject定义, 从图中可以看出, 有string, userdata, closure, table, proto, upvalue, thread

  • lua中, number, boolean, nil, light userdata四种类型的值是直接存在栈上元素里的, 和垃圾回收无关.

  • lua中, string, table, closure, userdata, thread存在栈上元素里的只是指针, 他们都会在生命周期结束后被垃圾回收.

详见

  • lua常用api
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    lua_State* L=luaL_newstate(); luaL_newstate()函数返回一个指向堆栈的指针
    lua_createtable(L,0,0);新建并压入一张表
    lua_pushstring(L,0,0);压入一个字符串
    lua_pushnumber(L,0,0);压入一个数字
    lua_tostring(L,1);取出一个字符串 return const char *
    lua_tointeger(L,1);取出数字 return int
    double b=lua_tonumber();取出一个double类型的数字
    lua_load()函数 当这个函数返回0时表示加载
    luaL_loadfile(filename) 这个函数也是只允许加载lua程序文件,不执行lua文件。它是在内部去用lua_load()去加载指定名为filename的lua程序文件。当返回0表示没有错误。
    luaL_dofile 这个函数不仅仅加载了lua程序文件,还执行lua文件。返回0表示没有错误。
    lua_push*(L,data)压栈,
    lua_to*(L,index)取值,
    lua_pop(L,count)出栈。
    lua_close(L);释放lua资源
    lua_getglobal(L, "val");//获取全局变量的val的值,并将其放入栈顶

lua调用c库

lua为程序主题调用c库函数,lua用require "myLualib"来请求c库,之后可以调用c函数,例子:
c库代码,需要添加lua.h和lauxlib.h头文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#include <lua.h>
#include <lauxlib.h>
#include <stdio.h>
/* 库 open 函数的前置声明 */
int luaopen_myLualib(lua_State *L);
static int ltest1(lua_State *L) {
int num = luaL_checkinteger(L, 1); /*检查参数是否有误*/
printf("--- ltest1, num:%d\n", num);
return 0; /*如果有返回结果,则lua_pushnumber(L,op1 + op2);等回传结果,并return [返回的个数]*/
}

static int ltest2(lua_State *L) {
size_t len = 0;
const char * msg = luaL_checklstring(L, 1, &len); /*checklstring计算string长度给第三个参数*/
printf("--- ltest2, msg:%s, len:%d\n", msg, len);
return 0;
}

static int ltest3(lua_State *L) {
size_t len = 0;
int num = luaL_checkinteger(L, 1);
const char * msg = luaL_checklstring(L, 2, &len);
printf("--- ltest3, num:%d, msg:%s, len:%d\n", num, msg, len);
return 0;
}
/* 将定义的函数名集成到一个结构数组中去,建立 lua 中使用的方法名与 C 的函数名的对应关系 */
static const luaL_reg myLualib_lib[] = {
{ "test1", ltest1 },
{ "test2", ltest2 },
{ "test3", ltest3 },
{ NULL, NULL },
};

/* 库打开时的执行函数(相当于这个库的 main 函数),执行完这个函数后, lua 中就可以加载这个 so 库了 */
int luaopen_myLualib(lua_State *L)
{
/* 把那个结构体数组注册到 mt (名字可自己取)库中去 */
luaL_register(L, "myLualib", myLualib_lib);
return 1;
}
/*5.4.x
int luaopen_myLualib(lua_State *L) {

luaL_Reg l[] = {
{ "test1", ltest1 },
{ "test2", ltest2 },
{ "test3", ltest3 },
{ NULL, NULL },
};
luaL_newlib(L, l);

return 1;
}

luaL_register现在已经弃用,取而代之的是luaL_newlib
lua主程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function test3( ... )
print("----- test myCustomLib")
package.cpath = "./?.so" --so搜寻路径
local f = require "myLualib" -- 对应luaopen_myLualib中的myLualib

f.test1(123)
f.test2("hello world")
f.test3(456, "yangx")
end

test3()
--[[
----- test myCustomLib
--- ltest1, num:123
--- ltest2, msg:hello world, len:11
--- ltest3, num:456, msg:yangx, len:5

--]]

lua调用c还可以是先在c中注册好函数供lua调用,详见pwnsky调用流程。

c调用lua

C++ C调lua遵守的原则

  1. 所有lua中的值由lua自己管理 C++/C并不知道 并且他们其中的值Lua也不知道 如果C++/C要lua中的东西 由lua产生放到栈上 C++/C通过API接口获取这个值
  2. 凡是lua的变量 lua负责这些变量的生命周期和垃圾回收

main.lua文件:

1
2
3
4
5
6
7
8
name = "bob"
age= 20
mystr="hello lua"
mytable={name="tom",id=123456}

function add(x,y)
return 2*x+y
end

test.c:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
#include <stdio.h>
#include "lua.h"
#include "lualib.h"
#include "lauxlib.h"
void gettable(lua_State *L){
printf("读取lua table中对应的值\n");
//将全局变量mytable压入栈
lua_getglobal(L, "mytable"); //获取table对象
/*//压入表中的key
lua_pushstring(L, "name");

//lua_gettable会在栈顶取出一个元素并且返回把查找到的值压入栈顶
lua_gettable(L, 1);
*/
lua_getfield(L,-1,"name"); //lua_getfield(L,-1,"name")的作用等价于 lua_pushstring(L,"name") + lua_gettable(L,1)
const char *name = lua_tostring(L, -1); //在栈顶取出数据
printf("name:%s\n", name);

lua_pushstring(L,"id");//压入id
lua_gettable(L, 1);//在lua mytable表中取值返回到栈顶
int id = lua_tonumber(L, -1); //在栈顶取出数据
printf("id:%d\n", id);
}
void add(lua_State *L){
//调用函数,依次压入参数
lua_getglobal(L, "add");
lua_pushnumber(L, 10);
lua_pushnumber(L, 20);
//查看压入栈的元素
for (int i=1;i<3;i++)
{
printf("number:%f\n",lua_tonumber(L, -i));
}
//lua_pcall(L,2,1,0):传入两个参数 期望得到一个返回值,0表示错误处理函数在栈中的索引值,压入结果前会弹出函数和参数
int pcallRet = lua_pcall(L, 2, 1, 0); //lua_pcall将计算好的值压入栈顶,并返回状态值
if (pcallRet != 0)
{
printf("error %s\n", lua_tostring(L, -1));
return -1;
}

printf("pcallRet:%d\n", pcallRet);
int val = lua_tonumber(L, -1); //在栈顶取出数据
printf("val:%d\n", val);
lua_pop(L, -1); //弹出栈顶
//再次查看栈内元素,发现什么都没有,因为lua在返回函数计算值后会清空栈,只保留返回值
for (int i=1;i<3;i++)
{
printf("number:%f\n",lua_tonumber(L, -i));
}
}
int main()
{
lua_State *L = luaL_newstate();
luaL_openlibs(L);
int retLoad = luaL_loadfile(L, "main.lua");
if (retLoad == 0)
{
printf("load file success retLoad:%d\n", retLoad);
}
if (retLoad || lua_pcall(L, 0, 0, 0))
{
printf("error %s\n", lua_tostring(L, -1));
return -1;
}

lua_getglobal(L, "name"); //lua获取全局变量name的值并且返回到栈顶
lua_getglobal(L, "age"); //lua获取全局变量age的值并且返回到栈顶,这个时候length对应的值将代替width的值成为新栈顶
//注意读取顺序
int age = lua_tointeger(L, -1); //栈顶
const char *name = lua_tostring(L, -2);//次栈顶
printf("name = %s\n", name);
printf("age = %d\n", age);
add(L);
gettable(L);
lua_close(L);
return 0;
}

注意:这个时候我们修改一下lua中的add函数:把2改为4

1
2
3
function add(x,y)
return 4*x+y
end

这时不进行编译,直接再运行一下./main,可以看到这个结果改变了从40变成了60,这是在我们没有进行重复编译的情况下直接产生的变化。
漂亮的证明了lua在c语言里的嵌入特性,lua中的函数就像是文本一样被读取,但是又确实是作为程序被执行。当我们的项目很大的时候,每次编译都需要几十分钟,这个时候如果合理的利用lua特性,仅仅是需要修改lua文件就可以避免这十几分钟的空白时间。

lua文件格式,及源码修改 ,lua5.1字节码修改方法,以及几个有趣的自定义解释器的例子,以及相应反编译工具的修改重打包。

luac文件格式

lua头文件有12字节:
1b 4c 75 61 51 00 01 04 08 04 08 00
源码定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/*
* make header src/lundump.c
*/
void luaU_header (char* h)
{
int x=1;
memcpy(h,LUA_SIGNATURE,sizeof(LUA_SIGNATURE)-1); //4bytes
h+=sizeof(LUA_SIGNATURE)-1; //移动指针
*h++=(char)LUAC_VERSION; //1
*h++=(char)LUAC_FORMAT; //1
*h++=(char)*(char*)&x; /* endianness */ 1
*h++=(char)sizeof(int); //1
*h++=(char)sizeof(size_t); //1
*h++=(char)sizeof(Instruction); //1
*h++=(char)sizeof(lua_Number); //1
*h++=(char)(((lua_Number)0.5)==0); /* is lua_Number integral? */ 1
}

其中第1-4字节为:“\033Lua”;
第5字节标识lua的版本号,lua5.1为 0x51;
第6字节为官方中保留,lua5.1中为 0x0;
第7字节标识字节序,little-endian为0x01,big-endian为0x00;
第8字节为sizeof(int);
第9字节为sizeof(size_t);
第10字节为sizeof(Instruction),Instruction为lua内的指令类型,在32位以上的机器上为unsigned int;
第11字节为sizeof(lua_Number),lua_Number即为double;
第12字节是判断lua_Number类型起否有效,一般为 0x00;

不同版本的字节码头文件有些许差别。
文件头后面就是函数体部分。函数都被解释成Proto结构体进行解析,函数体涉及结构体太多,先掠过。

字节码修改

如今灰产横行,而lua对嵌入式支持很好,有很强的可配置性被广泛应用到游戏等,一些嵌入式设备也会用到,但是由于lua这一特性,很容易被人修改,所以各大厂商会对lua进行自己的修改,来防止使用反编译工具直接得到源码。修改方法有以下几种:

  1. 普通的对称加密,在加载脚本之前解密
  2. 修改lua虚拟机中opcode的顺序
  3. 交叉使用

这里简单说下字节码修改过程:
一共涉及到两个文件的修改:lopcodes.hlopcodes.c
lopcodes.h:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/*
** grep "ORDER OP" if you change these enums 老外留的提示
*/

typedef enum {
/*----------------------------------------------------------------------
name args description
------------------------------------------------------------------------*/
OP_MOVE,/* A B R(A) := R(B) */
OP_LOADK,/* A Bx R(A) := Kst(Bx) */
OP_LOADBOOL,/* A B C R(A) := (Bool)B; if (C) pc++ */
OP_LOADNIL,/* A B R(A) := ... := R(B) := nil */
OP_GETUPVAL,/* A B R(A) := UpValue[B] */
*
*
*
OP_CLOSE,/* A close all variables in the stack up to (>=) R(A)*/
OP_CLOSURE,/* A Bx R(A) := closure(KPROTO[Bx], R(A), ... ,R(A+n)) */

OP_VARARG/* A B R(A), R(A+1), ..., R(A+B-1) = vararg */
} OpCode;

作者提示了如果修改enum要更改ORDER OP
lopcodes.c:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/* ORDER OP */

const char *const luaP_opnames[NUM_OPCODES+1] = {
"MOVE",
"LOADK",
"LOADBOOL",
"LOADNIL",
"GETUPVAL",
*
*
*
"CLOSE",
"CLOSURE",
"VARARG",
NULL
};

#define opmode(t,a,b,c,m) (((t)<<7) | ((a)<<6) | ((b)<<4) | ((c)<<2) | (m))

const lu_byte luaP_opmodes[NUM_OPCODES] = {
/* T A B C mode opcode */
opmode(0, 1, OpArgR, OpArgN, iABC) /* OP_MOVE */
,opmode(0, 1, OpArgK, OpArgN, iABx) /* OP_LOADK */
,opmode(0, 1, OpArgU, OpArgU, iABC) /* OP_LOADBOOL */
,opmode(0, 1, OpArgR, OpArgN, iABC) /* OP_LOADNIL */
*
*
*
,opmode(0, 0, OpArgU, OpArgU, iABC) /* OP_SETLIST */
,opmode(0, 0, OpArgN, OpArgN, iABC) /* OP_CLOSE */
,opmode(0, 1, OpArgU, OpArgN, iABx) /* OP_CLOSURE */
,opmode(0, 1, OpArgU, OpArgN, iABC) /* OP_VARARG */
};

这三处顺序可以随意修改。

Luac指令完整由:OpCode、OpMode操作模式,以及不同模式下使用的不同的操作数组成。

官方5.1版本的Lua使用的指令有3种格式,使用OpMode表示,它的定义如下:

1
enum OpMode {iABC, iABx, iAsBx};  /* basic instruction format */

其中,i表示6位的OpCode;A表示一个8位的数据;B表示一个9位的数据,C表示一个9位的无符号数据;后面跟的x表示数据组合,如Bx表示B与C组合成18位的无符号数据。sBx前的s表示是有符号数,即sBx是一个18位的有符号数。OpMode的表格luaP_opmodes指出了每个opcode属于哪种opmode。
定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/*===========================================================================
We assume that instructions are unsigned numbers.
All instructions have an opcode in the first 6 bits.
Instructions can have the following fields:
`A' : 8 bits
`B' : 9 bits
`C' : 9 bits
`Bx' : 18 bits (`B' and `C' together)
`sBx' : signed Bx

A signed argument is represented in excess K; that is, the number
value is the unsigned value minus K. K is exactly the maximum value
for that argument (so that -max is represented by 0, and +max is
represented by 2*max), which is half the maximum for the corresponding
unsigned argument.
===========================================================================*/
/*
** size and position of opcode arguments.
*/
#define SIZE_C 9
#define SIZE_B 9
#define SIZE_Bx (SIZE_C + SIZE_B)
#define SIZE_A 8

#define SIZE_OP 6

#define POS_OP 0
#define POS_A (POS_OP + SIZE_OP)
#define POS_C (POS_A + SIZE_A)
#define POS_B (POS_C + SIZE_C)
#define POS_Bx POS_C

以小端序为例,完整的指令格式定义如下表所示:

OpMode B C A OpCode
iABC B(23~31) C(14~22) A(6~13) opcode(0~5)
iABx Bx (14~31) A(6~13) opcode(0~5)
iAsBx sBx (14~31) A(6~13) opcode(0~5)

虚拟机修改-运算

lparser.c:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static BinOpr getbinopr (int op) {
switch (op) {
case '+': return OPR_ADD;
case '-': return OPR_SUB;
case '*': return OPR_MUL;
case '/': return OPR_DIV;
case '%': return OPR_MOD;
case '^': return OPR_POW;
case TK_CONCAT: return OPR_CONCAT;
case TK_NE: return OPR_NE;
case TK_EQ: return OPR_EQ;
case '<': return OPR_LT;
case TK_LE: return OPR_LE;
case '>': return OPR_GT;
case TK_GE: return OPR_GE;
case TK_AND: return OPR_AND;
case TK_OR: return OPR_OR;
default: return OPR_NOBINOPR;
}
}

+-操作交换。可以达到另一种混淆效果。

虚拟机修改-函数

修改了 function 关键字,llex.h、llex.c:
llex.c:

1
2
3
4
5
6
7
8
9
10
/* ORDER RESERVED */
const char *const luaX_tokens [] = {
"and", "break", "do", "else", "elseif",
"end", "false", "for", "function", "if",
"in", "local", "nil", "not", "or", "repeat",
"return", "then", "true", "until", "while",
"..", "...", "==", ">=", "<=", "~=",
"<number>", "<name>", "<string>", "<eof>",
NULL
};

llex.h:

1
2
3
4
#define FIRST_RESERVED	257

/* maximum length of a reserved word */
#define TOKEN_LEN (sizeof("function")/sizeof(char))

可以将上述两个funtion字段改成其他你想改的字段,如funtion -> cyber,代码可写成:

1
2
3
4
5
cyber foo(a,b)
return a+b
end

print('a+b=',foo(3,5))

修改建议本地install,以免覆盖原始文件。

1
make linux test && make local

lua各个版本之间的区别还是很大的,最新的版本5.4已经有80多条指令。

反编译工具修改

对应的修改反编译工具就行,对于opcode修改只变换反编译工具opcode顺序:
unluac:

1
2
3
4
5
6
7
8
9
10
11
12
13
public OpcodeMap(Version.OpcodeMapType type) {
switch (type) {
case LUA50:
map = new Op[35];
map[0] = Op.MOVE;
map[1] = Op.LOADK;
map[2] = Op.LOADBOOL;
map[3] = Op.LOADNIL;
map[4] = Op.GETUPVAL;
map[5] = Op.GETGLOBAL;
map[6] = Op.GETTABLE;
.
.

重新编译jar包。
源码
release

luadec:
将修改好的lua官方源码替换掉原始源码,重新编译,注意确认好源码是官方来源。

修改lua解释器,文件加密落地,防止通用反编译工具

dump前加密:
wb+可读写方式打开,位置/src/luac.c:

1
2
3
4
5
6
7
8
9
10
 FILE* D= (output==NULL) ? stdout : fopen(output,"wb+"); <-------
// printf("%s\n",output);
if (D==NULL) cannot("open");
lua_lock(L);
luaU_dump(L,f,writer,D,stripping);
lua_unlock(L);
D = GetFileSizeAndDump(D); <------
if (ferror(D)) cannot("write");
if (fclose(D)) cannot("close");
}

load后解密:
rb+可读写方式打开,位置/src/lauxlib.c:

1
2
3
4
5
6
7
8
9
10
11
12
13
if (c == LUA_SIGNATURE[0] && filename) {  /* binary file? */
lf.f = freopen(filename, "rb+", lf.f); /* reopen in binary mode */
lf = GetFileSizeAndLoad(c,lf); <-----------
if (lf.f == NULL) return errfile(L, "reopen", fnameindex);
/* skip eventual `#!...' */
while ((c = getc(lf.f)) != EOF && c != LUA_SIGNATURE[0]) ;
lf.extraline = 0;
}
ungetc(c, lf.f);
status = lua_load(L, getF, &lf, lua_tostring(L, -1));
lf = GetFileSizeAndLoad(c,lf); <---------
readstatus = ferror(lf.f);
if (filename) fclose(lf.f); /* close file (even in case of errors) */

修改遇到的问题:
加载流程和定位合适的处理点搞了好久:
文件dump是通过DumpBlock函数调用writer函数来dump,而且是根据文件格式分段dump,dump的size跟前面的还有依赖,所以在writer里直接加密不合适。往上找,找到所有的dump操作完成后,fclose之前讲文件流改写。

load是根据文件头第一个字符来判断文件类型(binary or src),修改要注意lua加载的两种文件类型都用到了 status = lua_load(L, getF, &lf, lua_tostring(L, -1)); getF(),为了不影响源码文件执行,必修在此之前修改文件流,还有一个点是在解密完文件流后,fclose时会将解密的内容重新写回文件,所以在fclose时,要重新加密文件流,注意此时要判断一下文件类型。

最终实现效果:lua文件头不变(也可以加密),luac生成字节码是加密的,lua执行时解密执行。

加密方式可以自行决定,这里仅仅是亦或了一下。

修改后的opcode顺序简单还原

准备

  1. 被修改opcode顺序后的lua虚拟机生成的luac字节码
  2. 同版本原始lua虚拟机生成的luac字节码
  3. 一个尽可能多的覆盖所有操作指令的lua脚本
    思路
  4. 来自于同一个lua脚本修改前后的luac字节码进行对比,不同的就是opcode部分。
  5. 用正长的字节码去校队修改后的字节码
    通常经过luac编译的字节码*.luac 文件其中的数据是由 luaU_dump 函数产生,在 luac.c 文件中被调用,而 luaU_dump 的另一个入口在 lapi.c 的 lua_dump,被绑定到 Lua 的 string.dump 函数。
    通过在 Lua 脚本中对需要 dump 的函数用 string.dump,可以得到对应的字节码。
    对于只修改了opcode顺序luac文件,对比正常文件之后也是只有6bit操作码不同,数据是相同的,所以只要对比出二者不同就是opcode的位置,即可还原出修改后的顺序。
    当然这个只针对仅仅修改了opcode顺序的情况,如果还修改了其他的地方就需要甄别转换了。
    源码lvm.c中的luaV_execute函数进行了opcode识别,可知是由GET_OPCODE(i)来获取opcode的,看源码知道他是在lopcodes.h中的宏定义:
    1
    2
    3
    4
    5
    6
    7
    #define cast(t, exp)	((t)(exp))
    #define SIZE_OP 6

    #define POS_OP 0
    /* creates a mask with `n' 1 bits at position `p' */
    #define MASK1(n,p) ((~((~(Instruction)0)<<n))<<p)
    #define GET_OPCODE(i) (cast(OpCode, ((i)>>POS_OP) & MASK1(SIZE_OP,0)))
    可知i就是OpCode枚举类的元素,MASK1(SIZE_OP,0)经过计算是0x3f,经过&0x3f后得到opcode。
    尝试写lua脚本,实现对opcode还原:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    local bit = require('bit') -- lua 位操作脚本
    local test = require("test") -- 测试的lua脚本,尽可能多的覆盖所有指令

    -- 加载用正常 lua 的 dump 文件
    local fp = io.open("test.luac","rb")
    local ori_data = fp:read("*all")
    fp:close()

    -- print('data len '.. #data)
    print('ori_data len ' .. #ori_data)

    local ori_op_name = {
    "MOVE",
    "LOADK",
    "LOADBOOL",
    "LOADNIL",
    "GETUPVAL",
    "GETGLOBAL",
    "GETTABLE",
    "SETGLOBAL",
    "SETUPVAL",
    "SETTABLE",
    "NEWTABLE",
    "SELF",
    "ADD",
    "SUB",
    "MUL",
    "DIV",
    "MOD",
    "POW",
    "UNM",
    "NOT",
    "LEN",
    "CONCAT",
    "JMP",
    "EQ",
    "LT",
    "LE",
    "TEST",
    "TESTSET",
    "CALL",
    "TAILCALL",
    "RETURN",
    "FORLOOP",
    "FORPREP",
    "TFORLOOP",
    "SETLIST",
    "CLOSE",
    "CLOSURE",
    "VARARG",
    }
    local data = string.dump(test) -- dump
    print('modify_data len ' .. #data)

    local new_op = {}
    -- 用目标 lua 和正常 lua 的 dump 数据对比
    for i = 1, #data do
    local by_ori = string.byte(ori_data,i)
    local by_new = string.byte(data,i)
    if by_ori ~= by_new then
    local op_name = ori_op_name[bit:_and(0x3F,by_ori) + 1] -- enum第0元素为nil
    local op_idx = bit:_and(0x3F,by_new)
    new_op[op_name] = op_idx
    end
    end
    -- print(ori_op_name[1])
    print("old \t new \t name")
    for idx, op_name in pairs(ori_op_name) do
    local tmp = ''
    if new_op[op_name] ~= nil then
    tmp = new_op[op_name]
    end
    print((idx - 1) .. "\t" .. tmp .. "\t" .. op_name )
    end
    bindiff对操作指令敏感,对指令的操作数是识别不了的。对源码进行修改可以根据bindiff对比异同,快速定位,比如+、-换位,opcode换位等,可以通过分析加载流程确定。

汇编语言中的程序控制流常依赖于处理器的状态标志来进行决策。在x86架构中,ZF(Zero Flag)、OF(Overflow Flag)和SF(Sign Flag)是在执行比较和算术指令后设置的重要标志位。本文将探讨这些标志位以及与之相关的常用条件跳转指令,并提供代码案例以加深理解。

ZF:零标志位(Zero Flag)
零标志位指示了上一个算术或比较操作的结果是否为零。如果结果为零,ZF被设置为1;否则,置为0。

条件跳转指令:

  • je(Jump if Equal):当ZF=1时跳转。

  • jne(Jump if Not Equal):当ZF=0时跳转。

代码案例1:使用ZF进行循环控制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
section .text
global _start

_start:
mov ecx, 10 ; 设置循环初始值为10

loop_start:
dec ecx ; 将ecx递减1
jz loop_end ; 如果结果为0(ecx已减至0),则跳转到loop_end
; 在这里可以放置循环体中的其他指令
jmp loop_start ; 回到循环开始

loop_end:
; 循环结束后的操作
mov eax, 1 ; 设置退出代码
int 0x80 ; 调用系统中断来退出程序
OF:溢出标志位(Overflow Flag)

溢出标志位指示有符号运算结果是否超出了目标数据类型的表示范围。

条件跳转指令:

  • jo(Jump if Overflow):当OF=1时跳转。

  • jno(Jump if No Overflow):当OF=0时跳转。

代码案例2:检测算术操作的溢出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
section .text
global _start

_start:
mov al, 0x7f ; 将al设置为最大的正有符号字节值127
add al, 1 ; 尝试将1加到al上, 这将导致溢出

jo overflowed ; 如果发生溢出,则跳转到overflowed
jno no_overflow ; 如果没有溢出,则跳转到no_overflow

overflowed:
; 溢出时的处理代码
jmp end

no_overflow:
; 没有溢出时的处理代码

end:
; 程序结束

SF:符号标志位(Sign Flag)
符号标志位反映了上一个算术或比较操作的结果的符号。如果结果为负,SF被设置为1;否则,置为0。

条件跳转指令:

  • js(Jump if Sign):当SF=1时跳转,即结果为负数时跳转。

  • jns(Jump if No Sign):当SF=0时跳转,即结果为正数或零时跳转。

代码案例3:根据结果的符号进行分支

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
section .text
global _start

_start:
mov eax, -5 ; 将eax设置为负数-5

test eax, eax ; 这将设置SF(和其他)标志
js negative ; 如果eax是负数,跳转到negative
jns positive ; 如果eax是非负数,跳转到positive

negative:
; 处理负数结果的代码
jmp end

positive:
; 处理正数或零的结果的代码

end:
; 程序结束

综合案例:使用ZF、OF、SF进行复杂控制流

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
section .text
global _start

_start:
mov eax, 0x7fffffff ; eax设置为32位有符号整数的最大值
add eax, 1 ; 尝试增加1,将会导致溢出

jo overflow ; 检测溢出
jno no_overflow ; 检测是否没有溢出

cmp eax, 0 ; 比较eax与0
je equal_to_zero ; 检测是否等于零
jne not_equal ; 检测是否不等于零

overflow:
; 处理溢出的情况
jmp end

no_overflow:
; 处理未溢出的情况
jmp compare

equal_to_zero:
; 处理等于零的情况
jmp end

not_equal:
; 处理不等于零的情况

compare:
test eax, eax
js negative_number ; 检测结果是否为负
jns non_negative ; 检测结果是否为非负

negative_number:
; 处理负数情况的代码
jmp end

non_negative:
; 处理非负数情况的代码

end:
; 程序结束

结论
了解和正确使用ZF、OF和SF标志位及相关的条件跳转指令对于编写可靠的汇编程序至关重要。这些标志位提供了执行算术和比较操作后的关键信息,条件跳转指令则依据这些信息来决定程序的执行路径。通过结合这些指令和标志位,可以在汇编语言中实现复杂的控制流逻辑,编写出响应不同运行时状态的高效代码。

在汇编语言中,程序的流程控制是通过各种跳转指令来实现的。跳转指令分为条件跳转和无条件跳转。本文将深入探讨汇编中等于条件跳转(je/jz)和无条件跳转(jmp)的使用,并通过代码示例展现它们的实际应用。

无条件跳转(jmp)
jmp 是无条件跳转指令,它告诉处理器无条件地将控制权转移给指定的地址。无论什么情况,jmp 指令后的指令都会被处理器忽略,并跳转到目标地址执行指令。

示例代码:简单的循环
section .text
global _start

_start:
mov ecx, 5 ; 设置循环计数器为 5

loop_start:
; 在这里执行循环体中的一些操作
dec ecx ; 每次循环减少计数器的值
jnz loop_start ; 如果 ecx 不是 0,继续循环

jmp exit        ; 无条件跳转到程序结束部分

exit:
; 退出程序
在上面的代码中,使用 jmp 指令无条件地跳转到 exit 标签,结束程序的执行。

等于条件跳转(je/jz)
je(Jump if Equal)和 jz(Jump if Zero)是条件跳转指令,它们基于之前的比较指令或测试指令的结果来决定是否跳转。如果比较的结果是相等的(或者零标志ZF被设置),控制将转移到指定的标签。

示例代码:比较和跳转
section .text
global _start

_start:
mov eax, 1 ; 将 eax 设置为 1
mov ebx, 1 ; 将 ebx 设置为 1
cmp eax, ebx ; 比较 eax 和 ebx

je equal        ; 如果 eax 等于 ebx,跳转到 equal 标签
jmp notequal    ; 如果不相等,跳转到 notequal 标签

equal:
; 如果二者相等,执行这部分代码
jmp end ; 跳转到程序的结束

notequal:
; 如果二者不相等,执行这部分代码

end:
; 程序结束
在这个例子中,如果 eax 和 ebx 相等,程序将跳转到 equal 标签执行相应的代码。否则,它将跳转到 notequal 标签执行不同的代码。

总结
汇编语言通过跳转指令实现程序流程的控制。jmp 是无条件跳转,而 je/jz 是基于条件的跳转。在编写汇编代码时,理解并正确使用这些跳转指令对于控制程序的流程至关重要。无条件跳转通常用于循环的退出和程序的结束,而条件跳转则用于基于某些条件执行不同的代码路径。这些跳转指令的适当使用能够让你的汇编程序更加灵活和强大。

在汇编语言中,根据程序中的条件来决定执行流程是一项基本的操作。理解并有效地使用条件跳转指令,是编写高效汇编代码的关键。本文将重点讲解不等条件跳转指令 jne(Jump if Not Equal)和 jnz(Jump if Not Zero),包括它们的工作原理和一些实用的代码示例。

条件跳转基础
jnejnz 是条件跳转指令,用于在满足特定条件时改变程序的执行流。具体来说,当比较操作之后的结果不相等,或者某个测试操作没有设置零标志(Zero Flag,ZF),这两个指令会将程序的控制权转移到指定的标签地址。

虽然 jnejnz 有不同的名称,但它们在功能上是等效的。在汇编语言中,通常使用 cmp 指令来比较两个值。如果比较的结果不相等,ZF将不被设置,jnejnz 将触发跳转。

示例代码
接下来,我们将通过一系列示例来展示 jne/jnz 指令的使用。

示例 1:基础条件判断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
section .text
global _start

_start:
mov eax, 1 ; 将 eax 设置为 1
mov ebx, 2 ; 将 ebx 设置为 2
cmp eax, ebx ; 比较 eax 和 ebx

jne not_equal ; 如果 eax 不等于 ebx,跳转到 not_equal 标签
jmp end ; 否则,跳到程序结束部分

not_equal:
; 如果 eax 和 ebx 不相等,执行这里的代码
; 在这里可以插入相应的处理逻辑

end:
; 程序结束部分

在这个例子中,eaxebx 显然不相等,所以程序会跳转到 not_equal 标签。

示例 2:循环中的条件判断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
section .text
global _start

_start:
mov ecx, 5 ; 设置循环计数器为 5

loop_start:
; 在这里执行循环体中的一些操作
dec ecx ; 每次循环减少计数器的值
jnz loop_start ; 如果 ecx 不是 0,继续循环

; 当 ecx 为 0 时,流程会继续向下执行,而不是跳转回 loop_start
; 这里可以进行循环后需要执行的逻辑

jmp end ; 跳转到程序结束部分

end:
; 退出程序

在这个例子中,jnz 指令用来检查循环计数器 ecx 是否达到零。如果不为零,循环继续。

示例 3:多条件分支

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
section .text
global _start

_start:
mov eax, 1 ; 将 eax 设置为 1
mov ebx, 2 ; 将 ebx 设置为 2

compare_values:
cmp eax, ebx
je values_equal
jne values_not_equal

values_equal:
; 如果 eax 等于 ebx,执行这部分代码
jmp end

values_not_equal:
; 如果 eax 不等于 ebx,执行这部分代码

end:
; 程序结束

在这个例子中,我们使用 jejne 来创建一个多条件分支,根据 eax 和 ebx 的比较结果跳转到不同的代码块。

总结
理解并能够准确使用 jne/jnz 指令是掌握汇编语言中条件跳转的关键环节。这些指令使得程序员能够根据不同的运行时条件来改变程序的执行路径。在实际应用中,根据比较结果决定下一步的操作是编程中的常见需求,jne/jnz 提供了实现这一需求的基础。通过上述代码示例,您可以看到如何在实际编程中运用这些条件跳转指令。

编译OLLVM

1
2
3
4
5
git clone https://github.com/heroims/obfuscator.git -b llvm-9.0.1 --depth 1
cd obfuscator
mkdir build && cd build
cmake .. -DCMAKE_BUILD_TYPE=Release
make -j4

编译好后从build/bin/目录把下面几个文件拷贝到/toolchains/llvm/prebuilt/linux-x86_64/bin/
clangclang++clang-9.0clang-format
把以下几个文件从build/include/拷贝到/sysroot/usr/include/
Stdarg.hStddef.h__stddef_max_align_t.hfloat.hstdbool.h

命令和编译选项简介

  • fla:控制流扁平化;该选项使用函数级别的混淆来隐藏程序的结构。这通过随机重命名函数、添加不必要的控制流和删除调用的函数来实现。
    这个模式主要是把一些if-else语句,嵌套成do-while语句,增加了反编译和分析代码的难度。

    -mllvm -fla:激活控制流扁平化

  • split:该选项使用控制流混淆来增加程序的复杂性。这通过将函数分成几个基本块、添加随机的跳转指令和在运行时随机重组这些基本块来实现。这使得代码的流程更难以跟踪,从而增加了破解和反编译的难度。

    -mllvm -split:激活基本块分割。在一起使用时改善展平。
    -mllvm -split_num=3:如果激活了传递,则在每个基本块上应用3次。默认值:1

  • sub:指令替换;该选项使用字符串混淆来隐藏代码中的常量字符串。这通过将字符串分成几个小块、将其存储在数组中并在运行时重新组合来实现。
    这个模式主要用功能上等效但更复杂的指令序列替换标准二元运算符(+ , – , & , | 和 ^),使得分析代码和查找敏感信息更加困难。

    -mllvm -sub:激活指令替换
    -mllvm -sub_loop=3:如果激活了传递,则在函数上应用3次。默认值:1

  • bcf:虚假控制流程;该选项使用基本块级别的混淆来隐藏代码的结构。这通过改变基本块之间的控制流、添加不必要的基本块和移除基本块之间的条件分支来实现。这个模式主要嵌套几层判断逻辑,一个简单的运算都会在外面包几层if-else,所以这个模式加上编译速度会慢很多因为要做几层假的逻辑包裹真正有用的代码。

    另外说一下这个模式编译的时候要浪费相当长时间包哪几层不是闹得!

    -mllvm -bcf:激活虚假控制流程
    -mllvm -bcf_loop=3:如果激活了传递,则在函数上应用3次。默认值:1
    -mllvm -bcf_prob=40:如果激活了传递,基本块将以40%的概率进行模糊处理。默认值:30

  • sobf:源代码混淆;该选项使用源代码混淆技术来隐藏代码的逻辑和结构。这通过使用类似加密的方式对代码进行变换,使其难以理解和分析。这可以通过运行时解密来执行,从而隐藏代码的真实功能。
    变量名混淆:将变量名更改为随机字符串,使其难以理解。
    控制流混淆:改变代码的控制流程,增加条件分支和跳转,使代码更难以分析。
    字符串加密:将字符串加密,以防止明文字符串在二进制文件中可见。
    函数名混淆:将函数名更改为随机名称,增加代码的复杂性。

    -mllvm -sobf:激活源代码混淆
    -mllvm -aesSeed=0xada46ab5da824b96a18409c49dc91dc3:用于为 AES 加密提供一个种子值,从而生成随机数以增加混淆的难度。这有助于保护软件免受逆向工程的攻击,因为它会使得分析混淆后的代码更加困难。

单个函数混淆:

1
2
3
4
5
__attribute((__annotate__("bcf")))
__attribute((__annotate__("fla")))
__attribute((__annotate__("sub")))
__attribute((__annotate__("split")))
__attribute((__annotate__("sobf")))

C++中,有四种类型转换操作符:static_cast, dynamic_cast, const_castreinterpret_cast。它们的作用和区别是什么呢?

static_cast是最常用的一种类型转换,它可以在编译时进行基本类型之间的转换,也可以进行类层次结构中的向上或向下转换。例如:

1
2
3
4
5
6
7
int i = 10;
double d = static_cast<double>(i); // 基本类型转换
class A {};
class B : public A {};
A* a = new A();
B* b = static_cast<B*>(a); // 向下转换,不安全
a = static_cast<A*>(b); // 向上转换,安全

dynamic_cast主要用于类层次结构中的向下转换,它可以在运行时检查转换的合法性,如果转换失败,会返回空指针或抛出异常。例如:

1
2
3
4
A* a = new A();
B* b = dynamic_cast<B*>(a); // 向下转换,失败,返回空指针
a = new B();
b = dynamic_cast<B*>(a); // 向下转换,成功,返回非空指针

const_cast用于去除或添加const或volatile属性,它可以改变对象的底层const性。例如:

1
2
3
4
5
6
const int x = 10;
int* p = const_cast<int*>(&x); // 去除const属性
*p = 20; // 修改x的值,未定义行为
volatile int y = 10;
int* q = const_cast<int*>(&y); // 去除volatile属性
*q = 20; // 修改y的值

reinterpret_cast是最危险的一种类型转换,它可以将任意类型的指针或引用转换为任意类型的指针或引用,也可以将整数类型转换为指针类型或反之。它不会进行任何运行时检查或类型调整,只是简单地按位重新解释对象的内存表示。例如:

1
2
3
4
5
int i = 10;
char* c = reinterpret_cast<char*>(&i); // 将int*转换为char*
*c = 'A'; // 修改i的低字节为65
void* v = reinterpret_cast<void*>(i); // 将int转换为void*
i = reinterpret_cast<int>(v); // 将void*转换为int

reinterpret_cast和其它cast的区别主要体现在以下几个方面:

  • 转换范围:reinterpret_cast可以用于互不相关类型之间的转换,而其他cast只能用于相关类型之间的转换。
  • 安全性:reinterpret_cast不进行任何安全检查,不保证转换后的结果有任何意义,它只是按照程序员的意图进行强制类型转换。因此使用reinterpret_cast时要非常小心,它可能会导致未定义行为、数据丢失或程序崩溃,而其他cast会进行一定的安全检查,以减少安全隐患。
  • 移植性:reinterpret_cast的结果可能因编译器或硬件平台的不同而不同,因此具有较差的移植性,而其他cast的结果通常具有较好的移植性。

reinterpret_cast的使用场景主要有以下几种:

  • 底层操作,如将内存地址转换为指针或整数。
  • 调试工具,如将指针转换为整数以便在调试器中查看其值。
  • 特定的硬件平台,如在某些特定的硬件平台上,reinterpret_cast可以用于实现特定的功能。

最近不清楚使用原因,原本使用的好好vscode ssh远程连接,无法正常的的连接到远程服务器上。去查看vscode的文档,发现新增了tunnel模式。经过测试,可以正常的连接使用。

在远程服务器安装vscode

我使用的服务器是ubuntu 20.04 server,使用deb的方式安装,也可以使用snap或者直接下载安装。

1
2
3
4
5
6
sudo apt update
sudo apt install software-properties-common apt-transport-https wget
wget -q https://packages.microsoft.com/keys/microsoft.asc -O- | sudo apt-key add -
sudo add-apt-repository "deb [arch=amd64] https://packages.microsoft.com/repos/vscode stable main"
sudo apt update
sudo apt install code

创建远程通道

1
code tunnel --accept-server-license-terms

使用vscode连接到远程通道

无论使用浏览器打开上一步的通道链接,还是使用vscode连接通道,都需要使用相同的账号。

在本地打开vscode,点击左下角绿色方块,选择连接到隧道,使用之前创建通道的账号,再次登录,就会看到服务器地址。

当连接完成后,后续操作和操作本地无明显差别。

注意使用vscode连接需要安装扩展ms-vscode.remote-server

protocol-buffers proto3 语言指南

前言

近日在学习gRPC框架的相关知识时接触到Protobuf(protocol-buffers,协议缓冲区),proto3等知识。网上很多文章/帖子经常把gRPC与proto3放在一起,为避免初学者产生混淆,这里先简单介绍一下gRPC、Protobuf、proto3三者以及他们之间的关系:

gRPC:一个高性能、开源的通用RPC框架,它可以使用Protobuf定义服务
Protobuf:协议缓冲区是一种与语言无关、与平台无关的可扩展机制,用于序列化结构化的数据(参考JSON)
proto3:proto是一种语言规范,Protobuf就遵循这种语言规范,目前最高版本是proto3

本指南介绍如何使用协议缓冲区语言来构造协议缓冲区数据,包括文件语法以及如何从文件生成数据访问类。它涵盖了协议缓冲区语言的proto3版本。

定义消息类型

首先让我们看一个非常简单的例子。假设您想定义一个搜索请求消息格式,其中每个搜索请求都有一个查询字符串、您感兴趣的特定结果页以及每页的结果数。下面是用于定义.proto消息类型的文件。

1
2
3
4
5
6
7
syntax = "proto3";

message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 result_per_page = 3;
}
  • 文件第一行指定您使用的语法:如果不这样做,协议缓冲区编译器将假定您使用的是proto2。这必须是.proto3文件的第一个非空、非注释行。
  • 消息定义指定了三个字段(名称/值对),每个字段对应于要包含在该类型消息中的数据段。
指定字段类型

在上面的例子中,所有的字段都是标量类型:两个整数和一个字符串。但是,你同样可以为你的字段指定复合类型,包括枚举和其他消息类型。

分配字段编号

如您所见,消息定义中的每个字段都有一个唯一编号。这些字段编号用于标识消息二进制格式的字段,并且在消息类型投入使用后不应更改。请注意,1到15范围内的字段编号需要一个字节进行编码,编码内包括字段号和字段类型(您可以在协议缓冲区编码中了解更多信息)。16到2047范围内的字段编号需要两个字节(进行编码)。因此,您应该把1到15的消息编号留给非常频繁出现的消息元素。请记住为将来可能添加的频繁出现的元素留出一些空间。可以指定的最小字段号为1,最大字段号为229-1或536870911。您也不能使用数字19000到19999(字段描述符),因为它们是协议缓冲区的保留数字,如果你在你的.proto中使用了这些数字,编译器会报错。同样,不能使用任何以前保留的字段号。

指定字段规则

消息字段可以是以下字段之一:

  • singular(单一的):格式良好的消息可以有零个或一个字段(但不能超过一个),这是proto3语法的默认字段规则。
  • repeated(重复的):此字段可以在格式良好的消息中重复任意次数(包括零次),重复值的顺序将被保留。
    在proto3中,标量数字类型的重复字段默认使用压缩(packed)编码。
    您可以在协议缓冲区编码中找到有关压缩编码的更多信息。
添加更多消息类型

在一个.proto文件中可以定义多种消息类型。建议您在一个.proto文件中定义多个相关的消息类型。例如,如果要定义与SearchResponse消息类型对应的回复消息格式,可以将其添加到同一个.proto文件中:

1
2
3
4
5
6
7
8
9
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 result_per_page = 3;
}

message SearchResponse {
...
}
添加注释

.proto文件的注释和C/C++语法一样,//用作单行注释,/*...*/用作多行注释:

1
2
3
4
5
6
7
8
/* SearchRequest represents a search query, with pagination options to
* indicate which results to include in the response. */

message SearchRequest {
string query = 1;
int32 page_number = 2; // Which page number do we want?
int32 result_per_page = 3; // Number of results to return per page.
}
保留字段

如果通过完全删除某个字段或把它注释掉来更新消息类型,则将来的用户可以在对该类型进行自己的更新时重用该字段编号。如果以后加载相同.proto的旧版本,这可能会导致数据损坏、隐私漏洞等严重问题。确保不会发生这种情况的一种方法是使用reserved关键字指定已删除字段的字段编号为保留编号(也要指定已删除字段名称为保留名称(name),以规避JSON序列化问题)。将来有任何用户试图使用这些字段标识符时,协议缓冲区编译器将报错。

1
2
3
4
message Foo {
reserved 2, 15, 9 to 11;
reserved "foo", "bar";
}

不能在同一个reserved语句中混合使用字段名和字段号。

你的.proto文件生成了什么?

运行协议缓冲区编译器编译.proto文件时,编译器将以您选择的语言生成代码。您将需要使用文件中描述的消息类型,来进行获取和设置字段值、将消息序列化为输出流以及从输入流解析消息等工作。

  • 对于C++,编译器从每个.proto成一个.h.cc文件,其中每个文件中描述的每个消息类型都有一个类。
  • 对于Java,编译器生成一个.Java文件,其中包含每个消息类型的类,以及用于创建消息类实例的特殊生成器类。
  • Python有点不同–Python编译器生成一个模块,其中包含.proto中每个消息类型的静态描述符,然后与元类一起使用,在运行时创建必要的Python数据访问类。
  • 对于Go,编译器为文件中每种消息类型生成一个.pb.go文件。
  • 对于Ruby,编译器生成一个.rb文件,其中包一个Ruby模块,模块中包含你文件中的消息类型。
  • 对于Objective-C,编译器从每个.proto生成一个pbobjc.hpbobjc.m文件,并为文件中描述的每个消息类型生成一个类。
  • 对于C#,编译器从每个.proto生成一个.cs文件,其中每个消息类型对应一个类。
  • 对于Dart,编译器从每个.proto生成一个.pb.dart文件,其中每个消息类型对应一个类。
    通过遵循所选语言的教程(proto3版本即将推出),您可以了解更多关于为每种语言使用api的信息。有关更多API详细信息,请参阅相关API参考(proto3版本也即将推出)。

标量值类型

标量消息字段可以具有以下类型之一 —— 下表显示了.proto文件中指定的类型,以及自动生成的类中相应的类型:

.proto Type 说明 C++ Type Java Type Python Type[2] Go Type Ruby Type C# Type PHP Type Dart Type
double double double float float64 Float double float double
float float float float float32 Float float float double
int32 使用可变长度编码。负数的编码效率低下 —— 如果您的字段可能有负值,请改用sint32。 int32 int int int32 Fixnum or Bignum (as required) int integer int
int64 使用可变长度编码。负数的编码效率低下 —— 如果您的字段可能有负值,请改用sint64 int64 long int/long[3] int64 Bignum long integer/string[5] Int64
uint32 使用可变长度编码。 uint32 int[1] int/long[3] uint32 Fixnum or Bignum (as required) uint integer int
uint64 使用可变长度编码。 uint64 long[1] int/long[3] uint64 Bignum ulong integer/string[5] Int64
sint32 使用可变长度编码。有符号int值。它们比普通的int32更有效地编码负数 int32 int int int32 Fixnum or Bignum (as required) int integer int
sint64 使用可变长度编码。有符号int值。 它们比普通的int64更有效地编码负数 int64 long int/long[3] int64 Bignum long integer/string[5] Int64
fixed32 注意:总是4个字节(定长编码)。如果值通常大于228,则比uint32更有效。 uint32 int[1] int/long[3] uint32 Fixnum or Bignum (as required) uint integer int
fixed64 注意:总是8个字节(定长编码)。如果值通常大于228,则比uint64更有效 uint64 long[1] int/long[3] uint64 Bignum ulong integer/string[5] Int64
sfixed32 注意:总是4个字节(定长编码)。 int32 int int int32 Fixnum or Bignum (as required) int integer int
sfixed64 注意:总是8个字节(定长编码)。 int64 long int/long[3] int64 Bignum long integer/string[5] Int64
bool bool boolean bool bool TrueClass/FalseClass bool boolean bool
string 必须是UTF-8编码或7位ASCII文本的字符串,长度必须小于232字节。 string String str/unicode[4] string String (UTF-8) string string String
bytes 可以包含任何长度不超过232的任意字节序列。 string ByteString str []byte String (ASCII-8BIT) ByteString string List

要想了解更多关于序列化你的消息时这些类型如何编码的,请参阅协议缓冲区编码

  • [1] 在Java中,无符号的32位和64位整数用有符号的整数表示,最高位存储在符号位中。
  • [2] 在所有情况下,将值设置为字段将执行类型检查以确保其有效。
  • [3] 64位或无符号32位整数在解码时始终表示为long,但如果在设置字段时给定int,则可以表示为int。在所有情况下,值必须适合设置时表示的类型。见[2]。
  • [4] Python字符串在解码时表示为unicode,但如果给定ASCII字符串,则可以是str(这可能会更改)。
  • [5] 整数用于64位机器,字符串用于32位机器。

默认值

解析消息时,如果编码的消息不包含特定的单数元素,则解析对象中的相应字段将设置为该字段的默认值。这些默认值是特定于类型的:

  • string:默认值为空字符串
  • bytes:默认值为空字节
  • boolean:默认值为false
  • 数值类型:默认值为0
  • 枚举:默认值为第一个定义的枚举值,该值必须是0
  • 消息字段:不设默认值,它的确切值取决于语言。有关详细信息,请参阅生成代码指南
    重复字段的默认值为空(通常是适当语言中的空列表)。

请注意,对于标量消息字段,一旦解析了消息,就无法判断字段是显式设置为默认值还是根本没有设置(例如,布尔类型字段值是设置为false,还是默认的false):在定义消息类型时,应该记住这一点。例如,布尔类型字段设置为false时会触发一些行为,如果您不希望这些行为在默认情况下也发生,那么就不要使用布尔类型。另请注意,如果标量消息字段设置为其默认值,则不会在连接上序列化该值。
有关默认值如何在生成代码中工作的更多详细信息,请参阅所选语言的生成代码指南

枚举类型

定义消息类型时,可能希望其中一个字段只包含预定义值列表中的一个。例如,假设您想为每个SearchRequest添加一个corpus(语料库)字段,其中语料库的值可以是UNIVERSAL、WEB、IMAGES、LOCAL、NEWS、PRODUCTS或VIDEO。您只需在消息定义中添加一个枚举,每个可能的值都有一个常量,就可以做到这一点。
在下面的示例中,我们添加了一个名为Corpus的枚举,其中包含所有可能的值,以及一个类型为Corpus的字段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 result_per_page = 3;
enum Corpus {
UNIVERSAL = 0;
WEB = 1;
IMAGES = 2;
LOCAL = 3;
NEWS = 4;
PRODUCTS = 5;
VIDEO = 6;
}
Corpus corpus = 4;
}

如您所见,Corpus枚举的第一个常量映射到零:每个枚举定义必须包含一个映射到零的常量作为其第一个元素。这是因为:

  • 必须有一个零值,以便我们可以使用0作为数字默认值
  • 兼容proto2语法,按照proto2语法,第一个枚举值是默认值(而不是0)
    可以通过将相同的值赋给不同的枚举常量来定义别名。为此,您需要将allow_alias选项设置为true,否则协议编译器将在找到别名时将生成错误消息。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    message MyMessage1 {
    enum EnumAllowingAlias {
    option allow_alias = true;
    UNKNOWN = 0;
    STARTED = 1;
    RUNNING = 1;
    }
    }
    message MyMessage2 {
    enum EnumNotAllowingAlias {
    UNKNOWN = 0;
    STARTED = 1;
    // RUNNING = 1; // Uncommenting this line will cause a compile error inside Google and a warning message outside.
    }
    }
    枚举器常量必须在32位整数的范围内。由于enum使用可变编码,因此负值效率很低,所以不建议使用。您可以在定义的消息内部定义枚举,如上面的示例所示,也可以在外部定义枚举——这些枚举可以在.proto文件中的任何消息定义中重用。您还可以使用_MessageType_._EnumType_语法将一条消息中声明的枚举类型用作另一条消息中的字段类型。
    当使用协议缓冲区编译器编译一个使用了枚举.proto文件时,对于JavaC++来说,会生成一个对应的枚举类型;对于Python,会生成一个特殊EnumDescriptor类,用于在运行时生成的类中创建一组具有整数值的符号常量。

警告:生成的代码可能会受到特定语言的枚举数限制(一种语言的枚举数低千)。请检查您计划使用的语言的限制。

在反序列化过程中,无法识别的枚举值将保留在消息中,尽管反序列化消息时如何表示这些值取决于语言。在支持具有指定枚举范围以外值的开枚举类型的语言中,例如C++GO,未知的枚举值被简单地存储为其基础整数表示形式。在具有封闭枚举类型的语言(如Java)中,枚举中的大小写用于表示无法识别的值,并且可以使用特殊的访问器访问基础整数。在这两种情况下,如果消息被序列化,则无法识别的值仍将与消息一起序列化。
有关如何在应用程序中使用消息枚举的详细信息,请参阅所选语言的生成代码指南

保留值

如果通过完全删除枚举条目或把它注释掉来更新枚举类型,则将来的用户可以在自己更新该类型时重用该数值。如果以后加载相同.proto的旧版本,这可能会导致严重的数据损坏、隐私漏洞等问题。确保不会发生这种情况的一种方法是指定保留已删除条目的数值(和/或名称,这也可能导致JSON序列化问题)。如果将来有任何用户试图使用这些标识符,协议缓冲区编译器就会报错。您可以使用max关键字指定您的保留数值范围提高到可能的最大值。

1
2
3
4
enum Foo {
reserved 2, 15, 9 to 11, 40 to max;
reserved "FOO", "BAR";
}

请注意,不能在同一个reserved语句中混合使用字段名和数值。

Using Other Message Types - 使用其他消息类型

可以将其他消息类型用作字段类型。例如,假设您希望在每个SearchResponse消息中包含Result消息——为此,您可以在同一.proto中定义Result消息类型,然后在SearchResponse中指定类型为Result的字段:

1
2
3
4
5
6
7
8
9
message SearchResponse {
repeated Result results = 1;
}

message Result {
string url = 1;
string title = 2;
repeated string snippets = 3;
}
导入定义

请注意,此功能在Java中不可用。

在上面的示例中,Result消息类型与SearchResponse在同一个文件中定义——如果要用作字段类型的消息类型已经在另一个.proto文件中定义了呢?
通过导入其他.proto文件,可以使用这些文件中的定义。要导入另一个.proto的定义,请在文件顶部添加一个import语句:

1
import "myproject/other_protos.proto";

默认情况下,只能使用直接导入的.proto文件中的定义。但是,有时可能需要将.proto文件移动到新位置。不用直接移动.proto文件并在一次更改中更新所有import调用,现在可以在旧位置放置一个伪.proto文件,使用import public概念将所有导入转发到新位置。任何导入包含import public语句的proto的人都可以传递地依赖import public依赖项。例如:

1
2
// new.proto
// All definitions are moved here
1
2
3
4
// old.proto
// This is the proto that all clients are importing.
import public "new.proto";
import "other.proto";
1
2
3
// client.proto
import "old.proto";
// You use definitions from old.proto and new.proto, but not other.proto

协议编译器使用-I/--proto_path路径标志在协议编译器命令行上指定的一组目录中搜索导入的文件。如果没有给定标志,它将在调用编译器的目录中查找。通常,您应该将--proto_path标志设置为项目的根目录,并对所有导入使用完全限定名。

使用proto2消息类型

可以导入proto2消息类型并在proto3消息中使用它们,反之亦然。但是,proto2枚举不能在proto3语法中直接使用(如果导入的proto2消息可以使用他们)。

Nested Types - 嵌套类型

您可以在其他消息类型中定义和使用消息类型,如以下示例所示——这里的Result消息是在SearchResponse消息中定义的:

1
2
3
4
5
6
7
8
message SearchResponse {
message Result {
string url = 1;
string title = 2;
repeated string snippets = 3;
}
repeated Result results = 1;
}

如果要在其父消息类型之外重用此消息类型,请将其指定为_Parent_._Type_

1
2
3
message SomeOtherMessage {
SearchResponse.Result result = 1;
}

您可以将消息嵌套到任意深度:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
message Outer {                  // Level 0
message MiddleAA { // Level 1
message Inner { // Level 2
int64 ival = 1;
bool booly = 2;
}
}
message MiddleBB { // Level 1
message Inner { // Level 2
int32 ival = 1;
bool booly = 2;
}
}
}

Updating A Message Type - 更新消息类型

如果现有的消息类型不再满足您的所有需要(例如,您希望消息格式有一个额外的字段),但是您仍然希望使用用旧格式创建的代码,不要担心!在不破坏任何现有代码的情况下更新消息类型非常简单。记住以下规则:

  • 不要更改任何现有字段的字段编号。
  • 如果添加新字段,则使用“旧”消息格式的代码序列化的任何消息仍然可以由新生成的代码进行解析。您应该记住这些元素的默认值,以便新代码可以正确地与旧代码生成的消息交互。类似地,由新代码创建的消息也可以由旧代码解析:旧二进制文件在解析时忽略新字段。有关详细信息,请参阅未知字段部分。
  • 可以删除字段,前提是在更新的消息类型中不再使用此字段编号。您可能需要重命名字段,或者添加前缀OBSOLETE_,或者使用reserved保留字段编号,以便.proto的未来用户不会意外地重用该编号。
  • int32uint32int64uint64bool都是兼容的——这意味着您可以将字段从一种类型更改为另一种类型,而不会破坏向前或向后的兼容性。如果从不适合于相应类型的Wire中解析一个数,则将获得与在C++中将该数转换为该类型的相同效果(例如,如果64位数字被读取为一个int32类型,则它将被截断到32位)。
  • sint32sint64彼此兼容,但与其他整数类型不兼容。
  • 只要字节是有效的UTF-8编码,stringbytes就可以兼容。
  • 如果bytes包含消息的编码版本,则嵌入的消息与字节兼容。
  • fixed32sfixed32兼容,fixed64sfixed64兼容。
  • 对于stringbytes和消息字段,optionalrepeated兼容。给定重复字段的序列化数据作为输入,如果该字段是基元类型字段,则期望该字段为optional(可选的)字段的客户端将获取最后一个输入值;如果该字段是消息类型字段,则合并所有输入元素。请注意,对于数值类型(包括boolenum),这通常是不安全的。数字类型的重复字段可以按压缩格式序列化,当需要可选字段时,将无法正确解析压缩格式。
  • enumwire格式化方面与int32uint32int64uint64兼容(请注意,如果值不适合,它们将被截断)。但是,请注意,在反序列化消息时,客户端代码可能会对它们进行不同的处理:例如,未识别的proto3枚举类型将保留在消息中,但在反序列化消息时如何表示这些类型取决于客户端语言。整型字段总是保持它们的值。
  • 将单个值更改为新oneof值的成员是安全的,并且二进制兼容。如果您确定没有代码一次设置多个字段,那么将多个字段移到其中一个新oneof字段中可能是安全的。将任何字段移到现有oneof字段中都不安全。

Unknown Fields - 未知字段

未知字段是格式良好的协议缓冲区序列化数据,表示解析器无法识别的字段。例如,当一个旧二进制代码解析一个带有新字段的新二进制代码发送的数据时,这些新字段在旧二进制代码中成为未知字段。
最初,proto3消息在解析过程中总是丢弃未知字段,但在3.5版中,我们重新引入了未知字段的保留,以匹配proto2的行为。在版本3.5和更高版本中,解析期间保留未知字段,并将其包含在序列化输出中。

Any - 任意类型

Any消息类型允许您将消息作为嵌入类型使用,而无需使用它们的.proto定义。Any包含一个以字节表示的任意序列化消息,以及一个URL,该URL充当该消息的全局唯一标识符并解析为该消息的类型。要使用Any类型,您需要导入google/protobuf/any.proto。

1
2
3
4
5
6
import "google/protobuf/any.proto";

message ErrorStatus {
string message = 1;
repeated google.protobuf.Any details = 2;
}

给定的消息类型的默认类型的URL是type.googleapis.com/_packagename_._messagename_
不同的语言实现将支持运行时库助手以类型安全的方式打包和解包任何值——例如,在Java中,Any类型有特殊的pack()unpack()访问器,而在C++中有PackFrom()UnpackTo()方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Storing an arbitrary message type in Any.
NetworkErrorDetails details = ...;
ErrorStatus status;
status.add_details()->PackFrom(details);

// Reading an arbitrary message from Any.
ErrorStatus status = ...;
for (const Any& detail : status.details()) {
if (detail.Is<NetworkErrorDetails>()) {
NetworkErrorDetails network_error;
detail.UnpackTo(&network_error);
... processing network_error ...
}
}

目前,用于处理Any类型的运行库正在开发中。
如果您已经熟悉proto2语法,那么Any类型可以保存任意proto3消息,类似于可扩展的proto2消息。

Oneof结构

如果消息包含多个字段,并且最多只能同时设置一个字段,则可以使用oneof功能强制执行此行为并节省内存。
oneof字段与常规字段类似,但oneof共享内存中的所有字段除外,并且oneof最多只能同时设置一个字段。设置oneof的任何成员将自动清除所有其他成员。您可以使用特殊的case()WhichOneof()方法检查oneof中设置了哪个值(如果有被设置),具体取决于您选择的语言。

使用oneof结构

要在.proto中定义oneof结构,请使用oneof关键字后跟oneof名称。请看示例test_oneof

1
2
3
4
5
6
message SampleMessage {
oneof test_oneof {
string name = 4;
SubMessage sub_message = 9;
}
}

然后将oneof字段添加到oneof定义。你可以添加任意类型的字段,除了maprepeated类型的字段。
在生成的代码中,oneof字段与常规字段一样具有getter(访问器)和setter(设置器)。您还可以使用一种特殊的方法来检查oneof结构中设置了哪个值(如果有的话)。您可以在相关的API参考中找到有关所选语言的oneof API的更多信息。

oneof的功能

设置oneof字段将自动清除oneof的所有其他成员。因此,如果您设置了几个字段中的一个,那么只有最后设置的字段有效。

1
2
3
4
5
SampleMessage message;
message.set_name("name");
CHECK(message.has_name());
message.mutable_sub_message(); // Will clear name field.
CHECK(!message.has_name());
  • 如果解析器在wire上遇到同一个oneof的多个成员,则在解析的消息中只使用看到的最后一个成员。
  • oneof结构不能使用repeated修饰符。
  • 反射API适用于oneof字段。
  • 如果将oneof字段设置为默认值(例如将int32类型的oneof字段设置为0),则将设置该oneof字段的case,并且该值将在wire上序列化。
  • 如果使用C++,请确保代码不会导致内存崩溃。下面的示例代码将崩溃,因为在调用set_name()方法时已经删除了sub_message。
    1
    2
    3
    4
    SampleMessage message;
    SubMessage* sub_message = message.mutable_sub_message();
    message.set_name("name"); // Will delete sub_message
    sub_message->set_... // Crashes here
  • 同样在C++中,如果你使用Swap()方法交换两条使用了oneof结构的消息,每条消息都将以另外一条消息的oneof case结束:在下面的示例中,msg1将有一个sub_message字段,msg2将有一个name字段。
    1
    2
    3
    4
    5
    6
    7
    SampleMessage msg1;
    msg1.set_name("name");
    SampleMessage msg2;
    msg2.mutable_sub_message();
    msg1.swap(&msg2);
    CHECK(msg1.has_sub_message());
    CHECK(msg2.has_name());
向后(下)兼容问题

添加或删除oneof字段时要小心。如果检查oneof字段的值返回None/NOT_SET,则可能意味着此oneof尚未设置或已将其设置为oneof的其他版本中的字段。因为无法知道wire的未知字段是否是其中一个字段的成员,所以无法区分两者之间的区别。

标签重用问题
  • 将字段移入或移出oneof:消息序列化和解析后,可能会丢失一些信息(某些字段将被清除)。但是,您可以安全地将single字段移动到新的oneof字段中,并且如果已知只设置了一个字段,则可以移动多个字段。
  • 删除oneof字段并将其加回:这可能会在序列化和解析消息后清除当前设置的oneof字段。
  • 拆分或合并oneof字段:这与移动常规字段有类似的问题

Maps - 映射

如果要创建关联映射作为数据定义的一部分,协议缓冲区提供了一种方便的快捷语法:

1
map<key_type, value_type> map_field = N;

…其中key_type可以是任何整型或字符串类型(因此,除了浮点类型和字节之外的任何标量类型)。注意enum不是一个有效的key_typevalue_type可以是除其他map以外的任何类型。
因此,例如,如果您想创建一个项目映射,其中每个Project消息都与一个字符串键相关联,您可以这样定义它:

1
map<string, Project> projects = 3;

映射字段不能使用repeated关键字。
映射值的Wire格式排序和映射迭代排序未定义,因此不能依赖特定顺序的映射项。
.proto生成文本格式时,映射按键排序。数字键按数字排序。
如果为映射字段提供键但没有值,则序列化字段时的行为与语言有关。在C++JavaPython中,类型的默认值被序列化,而在其他语言中没有任何序列化。
生成的map API目前适用于所有proto3支持的语言。你可以在相关的API参考中找到关于你所选择语言的map API的更多信息。

向下兼容

map语法相当于wire上的以下语法,因此不支持map的协议缓冲区实现仍然可以处理您的数据:

1
2
3
4
5
6
message MapFieldEntry {
key_type key = 1;
value_type value = 2;
}

repeated MapFieldEntry map_field = N;

任何支持映射的协议缓冲区实现都必须生成并接受上述定义可以接受的数据。

Packages - 包

可以向.proto文件中添加可选的package明符,以防止协议消息类型之间的名称冲突。

1
2
package foo.bar;
message Open { ... }

然后你可以在定义你的消息类型的字段时使用包说明符:

1
2
3
4
5
message Foo {
...
foo.bar.Open open = 1;
...
}

包说明符对生成代码的影响取决于您选择的语言:

  • 在C++中,生成的类被封装在C++命名空间内。例如,Open将位于名称空间foo::bar中。
  • 在Java中,该包用作Java包,除非在.proto文件中显式提供option java_package
  • 在Python中,package指令被忽略,因为Python模块是根据它们在文件系统中的位置来组织的。
  • 在Go中,包用作Go包名称,除非在.proto文件中显式提供option go_package
  • 在Ruby中,生成的类被包装在嵌套的Ruby名称空间中,并转换为所需的Ruby大写样式(第一个字母大写;如果第一个字符不是字母,则在前面加上PB_)。例如,Open将位于名称空间Foo::Bar中。
  • 在C#中,包在转换为帕斯卡命名法后用作命名空间,除非在.proto文件中显式提供选项option csharp_namespace。例如,Open将位于Foo.Bar命名空间中。
包和名称的解析

协议缓冲区语言中的类型名称解析的工作方式与C++类似:首先搜索最内层的作用域,然后搜索下一个最内层的作用域,以此类推,每个包都被认为是其父包的“内层”。以“.”开头(例如.foo.bar. baz)意味着从最外层的作用域开始。
协议缓冲区编译器通过解析导入的.proto文件来解析所有类型名。每种语言的代码生成器都知道如何引用该语言中的每种类型,即使它有不同的作用域规则。

Defining Services - 定义服务

如果要在RPC(Remote Procedure Call,远程过程调用)系统中使用消息类型,可以在.proto文件中定义RPC服务接口,协议缓冲区编译器将根据所选语言生成服务接口代码和存根。因此,例如,如果您想用一个方法定义一个RPC服务,该方法接受您的SearchRequest并返回一个SearchResponse,您可以在.proto文件中这样定义它:

1
2
3
service SearchService {
rpc Search(SearchRequest) returns (SearchResponse);
}

与协议缓冲区一起使用的最直接的RPC系统是gRPC:Google开发的一个与语言和平台无关的开源RPC系统。gRPC与协议缓冲区配合得特别好,允许您使用特殊的协议缓冲区编译器插件直接从.proto文件生成相关的RPC代码。
如果您不想使用gRPC,也可以在自己的RPC实现中使用协议缓冲区。你可以在Proto2语言指南中找到更多相关信息。
还有一些正在进行的第三方项目正在为协议缓冲区开发RPC实现。有关我们知道的项目的链接列表,请参阅第三方加载项wiki页面

JSON Mapping - JSON映射

Proto3支持JSON中的规范编码,使得在系统之间共享数据更加容易。下表按类型对编码进行了描述。
如果JSON编码的数据中缺少一个值或者它的值为null,那么在解析到协议缓冲区时,它将被解释为适当的默认值。如果某个字段在协议缓冲区中有默认值,则在JSON编码的数据中默认会省略该字段以节省空间。一个实现可以提供在JSON编码的输出中使用默认值发出字段的选项。

proto3 JSON JSON example 描述【译】
message object {“fooBar”: v, “g”: null, …} 生成JSON对象。消息字段名被映射到lowerCamelCase并成为JSON对象键。如果指定了json_name选项,则指定的值将用作键。解析器接受小驼峰命秘法名称(或由json_name选项指定的名称)和原始proto字段名称。null是所有字段类型的可接受值,并被视为相应字段类型的默认值。
enum string “FOO_BAR” 使用proto中指定的枚举值的名称。解析器接受枚举名和整数值。
map<K,V> object {“k”: v, …} 所有键都转换为字符串。
repeated V array [v, …] null被接受为空列表[]。
bool true, false true, false
string string “Hello World!”
bytes base64 string “YWJjMTIzIT8kKiYoKSctPUB+” JSON值将是使用带填充的标准base64编码的字符串编码的数据。包含或不包含填充的标准或url安全base64编码都可以接受。
int32, fixed32, uint32 number 1, -10, 0 JSON值将是一个十进制数。可接受数字或字符串。
int64, fixed64, uint64 string “1”, “-10” JSON值将是十进制字符串。可接受数字或字符串。
float, double number 1.1, -10.0, 0, “NaN”, “Infinity” JSON值将是一个数字或特殊字符串值”NaN”, “Infinity”,和”-Infinity”中的一个。数字或字符串都可以接受。指数符号也被接受。-0被认为等于0。
Any object {“@type”: “url”, “f”: v, … } 如果Any类型包含一个具有特殊JSON映射的值,它将按如下方式转换:{“@type”:xxx,”value”:yyy}。否则,该值将转换为JSON对象,并插入“@type”字段以指示实际的数据类型。
Timestamp string “1972-01-01T10:00:20.021Z” 使用RFC3339,其中生成的输出将始终是Z规格化的,并使用0、3、6或9个小数位数。也接受“Z”以外的偏移。
Duration string “1.000340012s”, “1s” 生成的输出总是包含0、3、6或9个小数位数(取决于所需的精度),后跟“s”后缀。接受任何小数位数(没有小数也可以),只要它们符合纳秒精度,并且需要“s”后缀。
Struct object { … } 任何JSON对象。见struct.proto。
Wrapper types various types 2, “2”, “foo”, true, “true”, null, 0, … 包装器在JSON中使用与包装原语类型相同的表示形式,只是在数据转换和传输期间允许并保留null。
FieldMask string “f.fooBar,h” 见field_mask.proto
ListValue array [foo, bar, …]
Value value 任何JSON值。详见google.protobuf.Value
NullValue null JSON null
Empty object {} 空的JSON对象
JSON选项

proto3的JSON实现可以提供以下选项:

  • 发出具有默认值的字段:在proto3 JSON输出中,带有默认值的字段默认省略。该实现可以提供一个选项来覆盖这个行为并用它们的默认值输出字段。
  • 忽略未知字段:proto3 JSON解析器在默认情况下应该拒绝未知字段,但是可以提供一个在解析中忽略未知字段的选项。
  • 使用proto字段名而不是lowerCamelCase名称:默认情况下,proto3 JSON打印器应将字段名转换为lowerCamelCase并将其用作JSON名称。一个实现可以提供一个使用proto字段名作为JSON名称的选项。proto3 JSON解析器需要同时接受转换后的lowerCamelCase名称和proto字段名称。
  • 以整数而不是字符串的形式发出枚举值:默认情况下,在JSON输出中使用枚举值的名称。可以提供一个选项来使用枚举值的数值。

Options - 选项

.proto文件中的单个声明可以使用许多 选项 进行注释。选项不会更改声明的总体含义,但可能会影响在特定上下文中处理声明的方式。可用选项的完整列表在google/protobuf/descriptor.proto中定义。
有些选项是文件级选项,这意味着它们应该写在顶级作用域中,而不是写在任何消息、枚举或服务定义中。有些选项是消息级别的选项,这意味着它们应该写在消息定义中。有些选项是字段级选项,这意味着它们应该写在字段定义中。也可以在枚举类型、枚举值、字段之一、服务类型和服务方法上编写选项;但是,目前尚无任何有用的选项可供选择。
以下是一些最常用的选项:

  • java_package(文件选项):您要用于生成的Java类的包。如果在.proto文件中没有给出显式的java_package选项,那么默认情况下将使用proto包(使用.proto文件中的“package”关键字指定)。然而,proto包通常不是好的Java包,因为proto包不应该以反向域名开始。如果不生成Java代码,则此选项无效。
    1
    option java_package = "com.example.foo";
  • java_multiple_files(文件选项):如果为false,则只为该.proto文件生成一个.java文件,并且为顶级消息、服务和枚举生成的所有Java类/枚举等将嵌套在外部类中(请参见java_outer_classname)。如果为true,则将为为顶级消息、服务和枚举生成的每个Java类/枚举等生成单独的.java文件,并且为此.proto文件生成的java“外部类”将不包含任何嵌套类/枚举等。这是一个默认为false的布尔选项。如果不生成Java代码,则此选项无效。
    1
    option java_multiple_files = true;
  • optimize_for(文件选项):可以设置为SPEED, CODE_SIZE,或LITE_RUNTIME。这将以如下方式影响C++和Java代码生成器(可能还有第三方生成器):
    • SPEED(默认值):协议缓冲区编译器将生成用于序列化、解析和对消息类型执行其他常见操作的代码。这段代码是高度优化的。
    • CODE_SIZE:协议缓冲区编译器将生成最小的类,并依赖共享的、基于反射的代码来实现序列化、解析和各种其他操作。因此,生成的代码将比使用SPEED时小得多,但操作将更慢。类仍将实现与在SPEED模式下完全相同的公共API。这种模式在包含大量.proto文件的应用程序中非常有用,而且不需要所有文件都非常快。
    • LITE_RUNTIME:协议缓冲区编译器将生成仅依赖于lite运行时库(libprotobuf-lite而不是libprotobuf)的类。lite运行时比完整库要小得多(大约小一个数量级),但省略了某些特性,如描述符和反射。这对于运行在手机等受限平台上的应用程序特别有用。编译器仍然会像在速度模式下一样生成所有方法的快速实现。生成的类将只在每种语言中实现MessageLite接口,它只提供完整message接口的方法子集。
      1
      option optimize_for = CODE_SIZE;
  • cc_enable_arenas(文件选项):为C++生成的代码启用arena allocation
  • objc_class_prefix(文件选项):设置Objective-C类前缀,该前缀位于此.proto中所有Objective-C生成的类和枚举之前。没有默认值。你应该使用Apple推荐的3-5个大写字符的前缀。请注意,所有2个字母前缀都由Apple保留。
  • deprecated(文件选项):如果设置为true,则表示该字段已弃用,不应由新代码使用。在大多数语言中,这没有实际效果。在Java中,这变成了@Deprecated注释。将来,其他特定于语言的代码生成器可能会在字段的访问器上生成弃用注释,这反过来会导致在编译试图使用该字段的代码时发出警告。如果该字段没有被任何人使用,并且您想阻止新用户使用它,请考虑用reserved语句替换字段声明。
    1
    int32 old_field = 6 [deprecated = true];
自定义选项

协议缓冲区还允许您定义和使用自己的选项。这是大多数人不需要的高级功能。如果您确实认为需要创建自己的选项,请参阅Proto2语言指南了解详细信息。请注意,创建自定义选项会使用扩展,这些扩展只允许用于proto3中的自定义选项。

Generating Your Classes - 生成类

要生成Java、Python、C++、Go、Ruby、ObjuleC或C代码,需要使用.proto文件中定义的消息类型,还需要在.proto上运行协议缓冲区编译器protoc。如果尚未安装编译器,请下载该软件包并按照自述文件中的说明进行操作。对于Go,您还需要为编译器安装一个特殊的代码生成器插件:您可以在GitHub上的golang/protobuf存储库中找到这个插件和安装说明。
协议编译器的调用方式如下:

1
protoc --proto_path=IMPORT_PATH --cpp_out=DST_DIR --java_out=DST_DIR --python_out=DST_DIR --go_out=DST_DIR --ruby_out=DST_DIR --objc_out=DST_DIR --csharp_out=DST_DIR path/to/file.proto
  • IMPORT_PATH:指定解析import指令时要在其中查找.proto文件的目录。如果省略,则使用当前目录。通过多次传递-proto_path选项可以指定多个导入目录;它们将按顺序进行搜索。-I=_IMPORT_PATH_可以用作--proto_PATH的缩写形式。

  • 可以提供一个或多个输出指令:

    • ——cpp_outDST_DIR中生成C++代码。更多信息请参见C++生成代码参考
    • ——java_outDST_DIR中生成Java代码。有关更多信息,请参阅Java生成代码参考
    • ——python_outDST_DIR中生成Python代码。更多信息请参见Python生成代码参考
    • ——go_outDST_DIR中生成Go代码。更多信息请参见Go生成代码参考
    • ——ruby_outDST_DIR中生成Ruby代码。更多信息请参见Ruby生成代码参考
    • ——objc_outDST_DIR中生成Objective-C代码。更多信息请参见Objective-C生成代码参考
    • ——csharp_outDST_DIR中生成c#代码。更多信息请参见C#生成代码参考
    • ——php_outDST_DIR中生成PHP代码。有关更多信息,请参阅PHP生成代码参考。为了方便起见,如果DST_DIR.zip.jar结尾,编译器会将输出写入一个给定名称的zip格式存档文件。注意,如果输出存档已经存在,它将被覆盖;编译器不够智能,无法向现有存档添加文件。
  • 必须提供一个或多个.proto文件作为输入。可以一次指定多个.proto文件。尽管这些文件是相对于当前目录命名的,但每个文件都必须驻留在IMPORT_PATH导入的其中一个路径中,以便编译器可以确定其规范名称。

在安卓7.0以上的系统版本中,app默认不信任用户安装的证书,只默认信任系统证书,需要将FiddlerRoot证书导入在系统证书内。

在雷电9设置的其他设置中开启ROOT模式,并且在性能设置中开启System.vmdk可写入,保存后重启雷电。
再把证书使用adb push到/sdcard/Download/FiddlerRoot.crt,再使用adb shell执行以下命令:

1
2
3
4
5
su
mount -o rw,remount /
cp /sdcard/Download/FiddlerRoot.crt /system/etc/security/cacerts/364618e0.0
chmod 644 /system/etc/security/cacerts/364618e0.0
reboot