【Augustzhang 张元龙】知根知底,方能游刃有余

小编语:据江湖传闻,龙哥从初中就开始写代码,高中通过计算机竞赛免试上了大学,大学里则是ACM大神。2010年毕业加入腾讯,先后从事密保、验证码等后台研发工作,现在主要负责安全平台部大数据平台的研发工作,致力于研究每秒GB级的数据如何进行实时分析等问题。龙哥是一个深度的技术爱好者,比较喜欢各种技术的钻研并乐在其中,今天有幸邀请到龙哥和大家分享一下在技术领域的一些学习经历和心得。 (注:标题中提到的知根知底,是指对底层原理的透彻理解。)

一 前言

在今天,应用层的技术日新月异,新的东西每天都在产生。学会一样新的语言/框架,你就可以快速的解决某个领域的问题,但是学习一些底层的知识,其实并不会立刻给你带来什么看得到的收益。这就像是建筑工程一样,学习一样新的技术框架可以迅速为你盖出一栋小别墅,而学习底层知识则像是在挖地基,虽然当时看不到什么收益,但在未来,你却可以在这个地基上盖出摩天大楼。

底层知识对人的影响是潜移默化的,有的时候你甚至感觉不到它的存在,但它却无时无刻不在影响着你,让你做出更高质量的实现。以拷贝一个8字节char数组为例,刚入门的程序员可能会用for循环逐字节赋值,而有一点经验之后,他会改成用memcpy来拷贝。随着经验的逐渐丰富,他开始使用把这个数组强制转换为64位整数指针来做赋值拷贝。再到后来,他了解了更多有关编译器的知识,于是他又改回了原来的memcpy的方式拷贝,这是因为他知道,编译器(加优化选项)在应对这种写法时会自动优化成64位整数拷贝的形式,两种写法在运行上并没什么区别,而这种写法更易于理解。

二 如何学习底层知识?

培养思考原理的习惯

举个很简单的例子,大家都知道TCP建立连接是三次握手,那么为啥设计上要是三次,而不是两次,或者四次呢?再比如说,在x86平台下,C语言参数传递的压栈顺序是从右到左,那么为什么不能从左到右呢?为什么当今大多数的硬件都采用小端架构而不是大端架构呢?有些问题,思考之后才能明白为什么。因此,保持对事物的好奇,带着问题去学习去思考,比漫无目的的去摸索更有效的多。

早期在学习C++的时候碰到了一个问题,有关类的内存是如何组织的问题。这个问题其实在很多C++的教材上并不会讲,无奈只能自己思考。在单继承的时候很好理解,内存的头部区域属于父类,而后面的部分则是子类独有的。这样,一个子类的指针可以直接作用于父类的方法。但我们知道,C++是支持多重继承的,如果一个类有两个父类,那这个时候情况就不一样了,无论头部区域属于哪一个父类,在转换为另一个类的时候就会出问题。这个时候最简单的方法就是写一段测试代码测试一下编译器是怎么处理的,最后发现,当子类的指针尝试转换为另一个父类指针时,编译器会自动为该指针加上一个偏移,那么整个问题就迎刃而解了。看似一个简单的赋值操作,其实是包含了加法运算的,如果你不了解这个原理,那么很可能会在使用上碰到一些坑,比如把这些指针转成其中一个父类指针,或者void指针存储,再转到另一个父类时就发现怎么都不对了。

了解一些常用漏洞的原理

漏洞挖掘是一件很有技术含量的事情,它体现了挖掘者对于底层运作原理的深刻理解,直接学习这些漏洞的利用原理,无异于你拜了一位大师,并且学习了他对底层最深刻的理解,你还可以在学习的过程中编写自己的攻击代码,实现一次漏洞攻击,这个过程不会乏味,非常有趣。同时,了解这些常用的漏洞也是一种借鉴,它能帮助你写出更加安全的代码。

今年上半年有幸参与了一次公司内的CTF比赛,初赛中有一题名字叫做echo,用gdb调试了一下,发现就是一个很简单的回显程序,fgets读取数据,然后printf输出。当时第一感觉就是printf用的不对,因为格式串是输入可控的,可能会出问题,然后输入了一个%s,果然就core了。上网搜了一下漏洞的利用,发现真的是脑洞大开,原来printf函数还有%n这种神奇的东西,它的设计本意只是用来控制输出格式的,但是通过一些特殊的构造可以用来往任意地址写入任意数据。如果改写了一些关键位置的数据,比如把GOT函数表的printf地址替换成system函数的地址,那么整个程序就变成了所有的输入都抛给system执行了。于是通过这种方式顺利拿到了执行权限并通过这题,同时也在这个过程中学了不少底层的东西。

学习一些软件逆向知识并加以尝试

学习软件逆向的过程,其实就是在学习很多底层知识,这包括汇编语言、操作系统如何加载应用程序、编译器优化原理。这些东西有什么用呢?举例来说,熟悉了编译器的优化原理,意味着你知道什么样的代码才能让编译器做更好的优化,即使是完全相同的逻辑和算法,也能写出比别人更加高效的代码。此外,在工作中相信大家都会遇到过一些莫名其妙的问题,很多问题在高级语言层面很难定位,这个时候直接gdb调出汇编看一下,再结合相关的寄存器内容很快就能知道问题大概出在哪里。

这里也说一下自己的经历。最初学习逆向知识的原动力其实只是为了方便自己玩游戏,能做出属于自己的游戏外挂,或者破解别人的收费外挂,也并没有想到会对自己产生更深远的影响。当时还是高二,自己买了本汇编语言的书,然后主要就是研究看雪论坛上的一些资料,然后就开始自己捣鼓起来。那时正好在玩一个网游,它的通信协议是通过动态代码来加密的,也就是说,客户端每次连接都会由服务器下发一段二进制代码,客户端通过CALL这段二进制代码完成协议加密,这样的好处是加密算法服务器可以随时修改,不过并没什么用,只要在该代码入口下个断点,就可以拿到未加密的明文数据来分析了。研究了一段时间就把登录、移动、打怪等各种通信协议给捣鼓出来了,然后干脆自己按着协议实现了一个客户端,就这么挂起机来了,没过两天等级就甩了同学远远一截。那个时候也做了属于自己的第一个远程控制的后门程序,由于词穷,取了个名字叫AngelShell发到网上,然后以阿长作为笔名发了两篇关于该后门程序的文章居然还被当时的黑客杂志《黑客防线》给刊登了,这些东西在现在看来虽然没什么,但在当时这些确实给了自己很强的成就感以及继续深入学习下去的动力。此后一直认为自己未来会从事安全相关的岗位。结果进到公司以后却阴差阳错的进入了后台开发的领域,然后发现这个领域也非常适合自己,之前与安全相关的所有知识一点都没有浪费,反而成了最宝贵的积累。

了解操作系统特性

一般来说,操作系统对开发者屏蔽了很多细节,如果不了解这些细节会很容易造成一些不当的设计。这里也举个例子,比如你想测试一下内存的写入速度能有多快,于是你申请了一个16G的数组来做测试,main函数里几句话:申请内存、开始计时、memset、结束计时、输出时间差,搞定。但事实上,如果你了解操作系统的内存管理,就会知道这种方法测试出来的根本不是内存的写入速度,而是把操作系统的一系列内存管理的运行时间通通算进去了。首先,你需要知道,操作系统管理内存是以页为单位,当你向操作系统申请一块内存之后,系统并不会立刻给该地址分配物理内存页,而是当对该页进行首次读写操作时才会有实际的物理内存分配动作。了解了这一点,你就会明白为啥刚才的做法是不对的了,同时你写代码的时候也应该会注意去节省物理内存,不会再动不动就对静态内存先来个内存清零操作了。

了解硬件的工作特性

对于一个非硬件专业的工程师来说,去了解他们的工作特性依然是非常有必要的。首先需要了解的是这些硬件的性能参数,比如,当前我们的服务器的CPU的计算能力是多少,内存的吞吐能力、网卡带宽、磁盘的IOPS,顺序读写吞吐量等等。掌握了这些数据,一个设计方案行还是不行,瓶颈是在哪里,设计之初就能看出来。而不至于一个系统到上线以后才发现设计不合理,扛不住线上压力。另外,如果你能掌握CPU访问内存的机制,一定能写出比别人更高效的代码。比如,你会下意识的让整数变量地址是自然对齐的,来避免CPU对内存的多次访问。再比如,如果你知道Intel的Cache线是64字节,以及Cache机制的运作,那么在结构体、类的设计的过程中,你应该会尽量让最常用的成员紧挨在一起摆放,而不是随意的摆放,因为这样可以尽可能的降低Cache容量开销与数据交换次数。再比如,大家都知道多线程/进程之间的加锁是一件很高成本的事情,但如果你知道CPU对自然对齐的整数读写本身就是原子的,那么你一定可以以无锁的方式写出高效的多线程/进程安全代码。如果你知道CPU除了常规的指令集以外,还有很多黑科技指令,比如mmx,sse,avx等指令集,那么在遇到一些大批量数据的计算时,你也一定会用这些指令来做指令集的加速计算,而不是简单的for循环计算。很多硬件特性,其实不去了解也能写代码,但是如果你了解了,绝对能写出更高质量的代码。

一定的算法与数据结构基础

大学时,大家学习算法和数据结构的时候一定会疑惑,这玩意学了到底有啥用?即使是工作以后,很多人也会认为这玩意并没什么作用,毕竟就写业务逻辑而言,用到高深算法的场景确实不多,就算用到了,也是搬出各种库直接套进去用,很少需要自己实现的。但就个人感受而言,算法与数据结构的精髓在于追求“更好的解决问题”,这是一种追求极致的体现。这种追求极致的精神恰恰是工作中所需要的,“解决问题”和“更好的解决问题”这显然是不同的层次。

其次,算法与数据结构在实际工作中的作用其实并不小,特别是在腾讯这样的大公司里,各种海量数据的处理都对性能有着极高的要求,这个时候,一个好的算法实现其运行成本会远低于一个糟糕的算法实现。举一个常见场景为例,文本中的关键词匹配,一个刚入门程序员很可能做一个关键词列表,然后循环匹配。稍微有经验一点的,可能会对关键词做个排序,然后二分查找。而更有经验的程序员,则会使用trie树、AC自动机这样的方法来实现的。这三种方法的运行效率显然不可相提并论,运用于实际系统中,方法一可能需要一百台机器才能解决的问题,方法三一台机器就够了。

很多时候,算法和数据结构只是为我们提供了一套问题解决的思路,就像是武侠小说中的“招式”,招式是死的,但人是活的,如果单纯的按照招式套路出牌,一定被打的很惨,关键在于招式的灵活运用。早期的时候我们有一个项目,涉及大数据存储的快速索引,我们通过对问题做了一些变换,最后用了一种原本看起来毫无关联的算法有效解决了这个问题,具体可以参考另一篇KM平台文章: 《颠覆索引性能极限:单机实现每秒千万级索引写入》。

三 结语

很庆幸自己能来到腾讯这样一家公司,这里有足够大的挑战让我去尝试,也给了我足够大的平台去学习钻研。经过这些年的磨练,个人感受最深的就是,扎实的技术功底并非一蹴而就,只能稳扎稳打,一步步的积累。如果之前忽视了对底层原理的学习,那么现在起步也不算晚。如果某一天,你写的每一行代码,你都能自然的想到编译器会如何翻译优化,操作系统会如何处理,硬件上又是怎么执行的。任何一个架构设计,你能马上想到缺点是什么,优点是什么,在什么条件下会触发什么问题,这个问题是否是在当前业务条件可接受的,那么恭喜你,你已经真正做到 “知根知底,游刃有余”了。

原文发布于微信公众号 - TEG云端专业号(TEGYunduan)

原文发表时间:2016-12-13

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Golang语言社区

改变未来IT世界的十种编程语言:Go语言

这里要说的都是革新,说这些的目的就是要保持关注最新技术。如果你是一个程序员,想要探寻未来技术,那这篇文章就是你的必读之选。我们这里列出了10种编程语言,10种将...

3065
来自专栏牛客网

2019届秋招JAVA开发面经 - 杭州【有赞】

一面电话面,二面线下面试,去公司看了看,技术氛围很好环境也很好,是一家很不错的公司,坐标杭州,规模千人,值得考虑。

821
来自专栏牛客网

分享一下面试题

阿里一面: 自我介绍; JVM内存模型; 你所知道的JVM几种gc算法; HashMap内部数据结构; 单例模式; 自己去实现线程池; 做过什么项目; 做项目时...

35311
来自专栏北京马哥教育

面试分享系列:从现在开始,准备加入BAT!

豌豆贴心提醒,本文阅读时间5分钟 程序员是一项技术工种,个人的技术水平决定薪资。 程序员需要在面试的过程中展示自己的技术水平,通过有说服力的表现拿到自己理...

3767
来自专栏牛客网

金蝶java岗 技术面+hr面 面经(略水)

看了那么多大佬的面经,感觉都是可望而不可及,问的题目都挺难。今天我就发份简单点的面经,让大家参考一下这种公司的水平大概都是会问些什么,有所了解。  一面 : ...

3736
来自专栏程序员互动联盟

为什么这么多人学不会C语言?

很多人觉得用C语言作为入门语言觉得太难了,里面还有指针,回调,递归之类的操作太难了。为什么这么多人觉得C语言难?笔者根据从业十几年的经验尝试着分析一下。 ? 第...

3476
来自专栏牛客网

秋招时间规划,知识点汇总,以及面试总结一、知识储备二、面试问题三、心态变化四、总结

秋招已结束,作为一个平时潜水的牛友,很感激牛客网和广大牛友们。在我无知时,给与我知识;在我烦恼时,给与我慰藉;现在自己也拿到了心仪的offer,就简单写写这段时...

35611
来自专栏余林丰

4.从AbstractQueuedSynchronizer(AQS)说起(3)——AQS结语

  前两节的内容《2.从AbstractQueuedSynchronizer(AQS)说起(1)——独占模式的锁获取与释放》 、《3.从AbstractQueu...

1879
来自专栏java一日一条

不愿看到Java开发者再做的10件事

编者注:Andy是OSI(开发系统集成者)的CEO,同时也是位思想先锋及优秀博客作者。

142
来自专栏程序人生

永恒不变的魅力

一个程序员,无论你人生的第一个hello world是从basic开始,c开始,抑或javascript开始,接下来了解的一个概念一定是「变量」(variabl...

34312

扫描关注云+社区