每次到这里就不动,这个代码耳机推不动是什么意思思?

程序总出故障,这10个容易犯的编程错误你避免了吗?
【51CTO.com快译】为什么程序出故障?虽然自世界上第一位女程序员艾达&洛夫莱斯(Ada Lovelace)在上世纪第一次看到通用计算的潜力以来我们已取得了很大进展,但是我们编写的软件还是错误百出。这些年来,尽管我们开发出许多高级方法来确保代码的成功,但是程序还是不断的出故障。
原因何在?
虽然这个问题的答案多种多样,但我们还是决定提供一个务实的答案。程序员难免犯错。他们有时马虎了事。他们并不总是使用最佳工具或最佳实践。
我在加州大学伯克利分校教面向对象编程这门课,我在学校教优秀编程实践所花的时间与帮助学生理解代码本身所花的时间相比只多不少。我在课堂上看到许多常犯的错误,本文就介绍其中几个常见错误。
我还联系上了西北理工大学工程学院的詹姆斯&A&康纳(James A. Connor)教授,请他介绍其学生常犯的一些错误。
我先来说几个。
第一个错误:糟糕的注释方法。
注释是程序里面计算机并不执行的那部分文本。它们被程序员写成附注的形式,用来解释代码里面发生的情况。
我的好多学生避免给代码添加注释,也想不明白为何要占用实际编码的时间去编写一些注释。我最实用的例子来自我自己的生活。
早在世纪之交前,我编写了版本1.0的ZENPRESS,这是最古老的内容管理系统之一。我预计它会带来好几年的文章。14年过后,它仍在管理许多文章,准备好了75000篇文章和26亿页的内容。
最后,它运行所依赖的那个平台过时了。我不得不回过头去研究代码。2009年,我把代码从原始平台移植到现代平台。我最近不得不再次改动,因为PHP一个关键的语言特性在版本升级后完全消失了。
19年过后,我根本记不起所有这些代码是怎么运行的,但是由于我对代码作了详细的注释,所以可以说有了一份路线图。我可以查看代码,查看嵌入在代码里面的注释,然后进行修改。
你在团队工作时,或者你的软件不归你监管时,注释也很重要。你的职业生涯可能发生变化,别人可能需要过来了解你的代码。注释将大有帮助。
第二个错误:糟糕的变量命名。
我会继续探讨这个主题:通过语言让代码一目了然。我会用一个例子来表明这点。假设你驾驶一辆每加仑汽油跑20英里的汽车,开了100英里。请问你用掉了多少汽油?
这是个简单的例子,但是适用于我们的用途。假设你遇到了a = b/c这一行。a指什么?b和c又指什么?它们与你的其余代码有何关系?在编写程序十分钟后,你会记得一干二净。更不用说别人过来修改代码或编写更新版了。
现在看看这个表达式:加仑 = 英里/每加仑行驶英里数。每一个变量的具体用途就一目了然。一个代表加仑,一个代表英里,另一个代表每加仑行驶英里数。很清楚。
想一想为变量赋予清楚的英语(或者其他任何母语)名称与注释之间的关系。假设你从别人那里接过了一段代码,看到a = b/c。这代码用来干嘛?你有何头绪吗?
务必以一种代表其功能的方式来命名变量。那样可以节省大量的时间,减少许多头痛的问题。
第三个错误:没有实验笔记(lab notes)。
我在1997年年中开始编写ZENPRESS,它在1998年1月份上线。遗憾的是,我当时匆忙完成了项目,没有花时间为这第一个版本编写实验笔记。此后我懊悔多次。从1999年6月份开始(当时我开始编写版本2),我就经常做实验笔记了。
实验笔记是代码注释之外的记录。科学家一直在使用实验笔记,作为其研发过程的日志或对话内容。实验笔记用来证明科学发现归谁所有,因为研究过程常常记录在科学家用来记录进度的每天日志中。
实验笔记对程序员来说同样是一种有效的工具。我为ZENPRESS编写的上一份实验笔记是在今年3月份编写的,当时我不得不把ZATZ归档从一家主机托管提供商迁移到另一家。我还经常对自己的其他项目做实验笔记,由于能够回过头去查阅笔记,好多次帮了大忙。
第四个错误:不用一种人类语言编写。
我的学生要考试过关,不仅仅要编程,他们还要编写讨论区帖子,证明他们熟知某些编程概念。
我们提出这个要求出于两个原因。当然,首先是为了证明熟悉概念。但是更为重要的是需要所有专业人员都有编写能力。
我在这方面遭到学生们的反对。每学期都有学生高喊:&我想成为程序员,而不是编写者。&但是编程、工程、IT和几乎所有专业工作都不可能在真空状态下存在。
你需要通过编写来解释概念、推销想法、获得资金、要求澄清、准备提议,或者甚至为拿到更好的分数据理力争。开源项目的参与者在非常庞大的团队协同工作,他们保持同步的唯一手段就是编写清楚的、易于理解的信息。
结论很简单:如果你想要从事专业工作或从事任何重要的项目,就需要用一种人类语言(比如英语)来编写,而不仅仅是用一种编程语言来编写。
第五个错误:糟糕的代码格式。
毫无疑问,这里的一个主题就是:让代码易于了解。代码维护起来极耗费时间和财力。坦率地说,这也不是很有趣。最好还是能够把宝贵的工作时间用来添加功能,而不是花几周来钻研旧代码,试图搞清楚你(或者交给你代码的那个人)想要完成什么任务。
本人就遇到过这种事,不仅仅是来自我的旧代码,还来自从别人手里接过的代码。我接手被丢弃的WordPress 开源插件作为一个副带项目。据我所知,我接手的插件比其他任何人都要多。每个插件都是由别人开发的,为了保持插件可以正常使用,我不得不钻研陌生人的代码。
幸好,那些开发人员都是高手,深谙编程之道。要不然,我也就无法接手这些项目了。但即便如此,要尽快上手还是困难重重。你能想象要是他们编写的代码结构很糟糕,那会有多难吗?
我所说的结构是指代码的布局方式。我为学生制作了这方面的一段视频。有兴趣的话,大家可以上YouTube观看(https://youtu.be/0u-I016Hxlw)。
想一想你在网上读到的文章。一些文章格式优美,每一个段落之间有一行,一切都保持一致。可是有些文章都用一个大大的blob来排列,没法看清。
每个程序员(或项目)都往往有一种编程风格。你的风格是什么样不是同样重要,只要保持一致就行。你需要让代码格式来帮助引导。
比如说,在我的代码中,我坚持代码段之间的空行不得超过一行。如果我看到一段更大的空白区,我立即就知道这一点:哪里出现了异常,这个空白区里面可能有错误。
你在深入研究代码时,要关注贵企业有没有编程风格。考虑为你的所有程序员定义一种编程风格,坚持采用清楚、易于维护的那一种风格。
第六个错误:糟糕的错误检查。
某位著名的将军曾经说过,遇到敌人时,计划根本不管用。我在此基础上改动一下,遇到用户时,你的代码根本不管用。尽管你认为自己知道用户会如何使用代码,但你其实并不知道,相信我。
用户会搞坏你的代码。
正确的处理方式就是借助测试和错误检查。错误检查是指这种做法:检查代码中每一次操作的结果。确保它符合你的预期,或者确保你的代码可处理意外的结果。
比如说,我的学生经常有一项任务:阅读文件。几乎所有的学生编写代码时会调用文件读取例程。他们检查用户是否取消对话框,但是很少查看文件是否实际上被读入,或者是否存在某种类型的系统错误。要是他们试图编写文件,那就更糟糕了。他们几乎从不真正查看文件是不是实际上保存起来。真是要命。
不难发现这会有多糟糕。为了对付这种情形,你总是要考虑能不能绝对预测行为,然后认识到你不能绝对预测行为。你需要测试。测试并不是仅仅指你自己运行代码。测试意味着让实际用户(即行为可能无法预测的那些人)运行你的代码。
你会发现这会提供大量的信息。
第七个错误:使用打印输出语句,而不是真正的调试器。
这些年来我发现,使用不同语言的程序员往往有不同的文化。总的来说,那是由于他们构建不同种类的解决方案、使用不同的工具。
这方面的一个例子就是我的C#编程学生和与我一起开发一些项目的开源PHP开发人员之间的区别。几乎没有一个C#程序员会考虑不使用一种符号调试器来调试代码。那是由于, C#本身是使用Visual Studio作为编程环境来编写的,调试器内置在里面。
相比之下,我见到许多PHP开发人员认为只要插入echo语句或var_dump就足以帮助自己调试代码了。这一方面是由于,大多数PHP程序员往往在编辑器里面编程,而不是在开发环境里面编程。两者之间的一大区别就是调试器。
那么,调试器是什么东东?简而言之,这种工具让你可以在代码运行时查看代码内部的情况。它就好比是代码的X光、超声波或MRI。可以指令调试器在某些点停止,检查所有变量的状态。还可以指令调试器在某些条件下停止。你可以更改值,可以查看和分析值(不过分析有时是另一种工具)。
工作效率方面的差异很大。如果你想更快速、极其准确地完成工作,就要确保使用一种真正的符号调试器。
以上就是我所介绍的几个常见错误,下面看看詹姆斯&康纳教授介绍的几个。
第八个错误:使用魔数(magical number)。
许多程序员认为,他们只要编写一次代码,代码就能完美无缺。然而,为了优化企业软件和工业软件的长期生命周期成本,有必要编写能够抵御不断变化的条件的代码。
这方面的一个典例就是魔数这个想法。我所说的魔数是指程序员认为总能经受得住时间考验的数字。
以可能基于客户的采购数量的佣金计算为例。截至截稿时,佣金比例可能是三个百分点,即0.03。
现在,设想一下会如何编写这段代码:佣金= .03 *销售额。在这个上下文中,这个魔数就是0.03。由于程序员认为这会是永远神奇地有效,他将0.03这个数字硬编码到代码中。
这一切很好,但是每年的佣金往往发生变化。如果下一年佣金涨了0.5%,涨到0.035,那么就很难在成千上万行代码中找到它。
切忌使用魔数,而是在一个地方定义变量或常量,让代码使用那些变量。如果你预先定义commission_rate,那么commission = commission_rate * sale之类的代码就不需要改动。
要考虑的另一个方面是,无论你在何处找到魔数,都应该找到想要提供给用户的选项,以便用户可以在偏好设置部分可以设置。
第九个错误:马虎对待的日期和时间。
这里有个难题:一年有几天?365天也许是平常的回答,但今年当然有366天。一天会有365.25天吗?这不可能。
但我的一些学生认为,既然闰年每四年就出现一次,所以每年平均下来因此是365.25天。在进行日期计算时,他们使用这个平均值;因而,结果根本不正确。
常常更好的办法是使用系统库来计算日期,因为你计算的日期可能不是西方日历日期。
不妨看一下时间方面的类似问题。每几年,由于地球转速减慢,有一天会多出一秒来,通常是在6月30日或12月31日那天。这叫作闰秒,因而,时钟可能从11:59:59走到11:59:60再走到12:00:00。
这是第二个时间挑战。在使用夏令时的地方,交易的进行有可能乱套。比如说,先置入交易A,但是随后时间被向后重置1小时,那么就置入交易B。然而,如果你在时间序列方面很马虎,它就会记录为交易B先发生。这种类型的时间错误会导致产生不必要的罚款,还会导致其他各种各样的混乱。
再次,有许多好的语言和系统库可以处理这两种时间问题。常常更好的办法是使用现有的库,而不是编写自己的时间计算代码。
第十个错误:没有选择合适的数据结构。
数据结构是表示程序中数据的一种机制。许多人听说过链表、树和数组之类的术语。这每一个术语都是数据的逻辑表示,对应于你试图表示的数据的一些架构结构。
我看到程序员(无论编程高手还是菜鸟)最常犯的错误之一就是,很少注意数据结构方面的选择。由于你的几乎所有代码都建立在选择的那种数据表示方法上,一旦选错数据结构,会在将来带来严重的影响。
下面这个例子可以表明这种设计错误:选择一个简单的堆栈或队列,而不是循环队列。堆栈就好比是一堆盘子。你取下底部盘子,然后放上另一只盘子,再取下另一只盘子,依次类推。
如果你想拿走一只盘子,你从最上面拿走。这就叫后进先出。可问题,如果你需要拿走早些时候放的东西,就很麻烦了。假设一堆栈盘子有10只盘子。想找到第一只盘子,你就得先拿走其他所有的盘子。
现在,不妨想象一下队列。如果你在银行排队,就处于队列中。第一个进去的也是第一个出来的。一旦第一个人得到了服务,下一个人跟上,然后该人得到服务。出现的另一种情况是,每个人都向前迈一步,在队列中向前移动位置。
要是许多人来排队,会出现什么情况?要么他们被拒之门外,要么队伍排到门外面。第一个人叫到后,所有这些人都要往前移动。
如果你有大量数据,这种队列就会极其低效。每当从队列的开头获取数据,所有数据都要移动。我们置身于大数据时代,有源源不断的数据从我们的系统通过。
在这种环境下,一种更好的办法也许是实施循环队列。在这种情况下,数据根本不动。相反,设置的指针指向队列的开头和末尾;在内部,队列首尾相连,那样数据以圆环、而不是队列的方式来加以组织。当数据元素使用、从圆环中移除后,就不需要移动圆环中的所有数据。发生的只是第一个元素的指针指向圆环中的新元素。
有许多例子可以表明选择正确的数据结构会给你代码的效率带来重大影响,这仅仅是其中一个。
但愿你在看完本文后,会成为一名更高效的程序员,避免其中一些严重错误。
原文标题:Software bugs? Avoid these 10 costly programming mistakes,作者:David Gewirtz
【51CTO译稿,合作站点转载请注明原文译者和出处为51CTO.com】
官方微博/微信
每日头条、业界资讯、热点资讯、八卦爆料,全天跟踪微博播报。各种爆料、内幕、花边、资讯一网打尽。百万互联网粉丝互动参与,TechWeb官方微博期待您的关注。
↑扫描二维码
想在手机上看科技资讯和科技八卦吗?想第一时间看独家爆料和深度报道吗?请关注TechWeb官方微信公众帐号:1.用手机扫左侧二维码;2.在添加朋友里,搜索关注TechWeb。
Copyright (C)
All rights reserved. 京ICP证060517号/京ICP备号 京公网安备76号
TechWeb公众号
机情秀公众号扫一扫关注官方微信
后使用快捷导航没有帐号?
主题帖子里程
主题帖子里程
本帖最后由 geniexxf 于
13:44 编辑
招商银行/*M*USER003
基本上1-2个月会看一次,出现的是这个代码,是什么意思呢?
我的招行卡是经典白,额度不高76K,每个月出账单后全部还款
主题帖子里程
主题帖子里程
招行好贱啊啊& &&&。。。。
主题帖子里程
主题帖子里程
确实。。。。。。。。。。。。。。。。。。。。。。。。每个月查
主题帖子里程
主题帖子里程
那是操作员的代码好像。
主题帖子里程
主题帖子里程
这是查你信报的人的账号。。。。
主题帖子里程
主题帖子里程
主题帖子里程
主题帖子里程
有病吗,每个月都看一次个人信报?难道是进入了什么监控账户
主题帖子里程
主题帖子里程
CMBUSER003.。。。。。。。。。。。
主题帖子里程
主题帖子里程
你们经常看信报吗?不是说一年只能免费两次?
主题帖子里程
主题帖子里程
有病吗,每个月都看一次个人信报?难道是进入了什么监控账户
表吓我好不好。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。
论坛达到10000用户活动奖励
社区QQ达人
使用QQ帐号登录论坛的用户
常客园公众号
奢玩手机客户端
积友之家公众号
积友之家手机客户端
|||官方QQ交流群:程序由0到1的过程 - 简书
程序由0到1的过程
进程的一生
我们中有许多程序员打码几年还没有搞清楚一个程序从源代码 -& 可执行程序 -& 执行 -& 死亡, 经历了什么变化. 他们只知道, 编译, 链接, 运行...由于强大的IDE已经帮我们把这些过程屏蔽掉了, 我们不知道底层他们干了什么. 但是我们只有明白这些运行机制和机理, 才能解决一些莫名其妙的错误, 提升性能瓶颈.
笔者在看了&&程序员的自我修养&&这本书后决定把这些过程用比较简单易懂的文字叙述出来, 如有不对的地方还请各位指出, 谢谢~
可执行文件
动态链接(需要的话)
编译又分为 预处理(Preprocessing), 编译(Compilation)和汇编(Assembly).
预编译过程主要处理源代码文件那些#开头的预编译指令
1. 删除所有#define, 展开所有宏定义
2. 处理所有预编译指令, 如#if, #ifdef, #elif, #else, #endif
3. 递归处理#include
4. 删除所有注释, // 和 /**/
5. 添加行号和文件名标识
6. 保留所有#pragma编译器指令
因此如果我们无法判断宏定义是否正确, 头文件包含是否正确时 -& 查看预编译后的文件来确定问题
编译过程可分为6部 : 扫描, 语法分析, 语义分析, 源代码优化, 代码生成和目标代码优化.
1. 扫描 : 扫描器运用一种类似于有限状态机的算法把源代码分割成一些列的记号(Token)
2. 语法分析 : 语法分析器采用上下文无关语法(Context-free Grammar)将Token进行语法分析, 生成语法树(Syntax Tree).该语法树就是以表达式为节点的树
3. 语义分析 : 语法分析只是对表达式的语法进行层面的分析, 它并不知道该语句是否真正有意义. 在这里, 语义分析器能够进行静态语义分析, 分析过后整个语法树的表达式都被标识了类型
静态语义 : 在编译器可以确定的语义, 通常包括声明和类型的匹配, 转换.
动态语义 : 在运行时才能确定的语义, 比如0作为除数则在这里报错
4. 源代码优化 : 源码级优化器(Source Code Optimizer)在源代码级别进行优化, 把一些类似于(2+6)这些在编译器就能确定的表达式优化成值, 从而把整个语法树转换成中间代码(Intermediate Code)
中间代码使得编译器可以被分为前端和后端, 前端负责产生机器无关的中间代码, 后端将中间代码转换成目标机器代码
5. 代码生成与优化 : 代码生成器(Code Generator)将中间代码转换成目标机器代码(该过程十分依赖于目标机器), 最后目标代码优化器(Target Code Optimizer)将上述的代码进行优化, 例如选择合适的寻址方式, 使用位移来代替乘法运算, 删除多余的指令等.
汇编器将汇编代码转换成机器可以执行的指令, 输出目标文件. 该过程比较简单, 就是翻译代码.
经过上述多个步骤, 源代码终于被编译成了目标文件. 这个目标文件肚子里又卖的是什么药呢? 我们接着看~
由于不同的操作系统下, 目标文件, 可执行文件等都有些出入. 本文是用Linux系统下的ELF文件作为例子
编译之后生成的目标文件内容肯定少不了机器指令代码, 数据等. 不过除了这些之外, 目标文件还包括了链接时所需的一些信息, 而目标文件将这些信息按照不同的属性, 以段(Section)来存储.
程序源代码编译后的机器指令 -& 代码段(Code Section), ".code"或".text"
全局变量和局部静态变量数据 -& 数据段(Data Section), ".data"或".bss"
Question :
为什么要把数据和指令分开呢? 经典的冯诺依曼体系不是不分指令还是数据的吗?
1 : 当程序被装载后, 数据和指令被映射到两个虚存区域. 数据区域对于进程而言, 是可读写的, 而指令区域则只可读. 这样方便分别设置他们的权限, 防止程序指令被恶意修改
2 : 把指令和数据分开有利于提高程序的局部性, 对于提高CPU缓存命中率有帮助
3 : 最重要的原因, 当系统中运行着多个该程序的副本时, 他们的指令都是一样的, 所以进程之间能共享指令和其他只读数据, 而数据区域则为进程私有. 如果系统中运行了数百个进程, 可以想象共享为我们节省了多少空间
这里插一句 : 其实不是可执行文件才才按照执行文件的格式存储. 什么意思呢? 除了可执行文件之外, 目标对象, 动态链接库, 静态链接库也按照可执行文件的格式存储. 某种程度上他们也是可执行文件. 所以我们可以把他们视为同一类文件
目标文件有什么
ELF文件头(ELF Header)
包含了整个文件的基本属性
段表(Section Header Table)
描述了ELF文件包含的所有段的信息
链接器在处理目标文件时, 要对目标文件中某些符号进行重定位, 即代码段和数据段那些对绝对地址引用的符号. 这些重定位信息就记录在重定位表中.
在链接中, 我们将函数和变量统称为符号(Symbol), 函数名和变量名称为符号名(Symbol Name). 符号表记录着该目标文件所用到的所有符号, 每个符号都有一个对应的值, 符号值(Symbol Value), 对于函数和变量来说, 符号值就是他们的地址.
强符号与弱符号, 强引用与弱引用
如果在目标文件A和目标文件B都定义了一个全局变量global, 并将他们都初始化. 那么链接的时候就会报multiple definition of 'global'的错误. 这种符号就是强符号. 默认所有符号都是强符号, 可以使用GCC的__attribute__ ((weak))定义一个弱符号.
强符号与弱符号的规则 :
1. 不允许多次定义强符号
2. 如果一个符号在某个目标文件中是强符号, 在其他目标文件中是弱符号, 那么链接时选择强符号
3. 如果一个符号在全部目标文件中都是弱符号, 那么选择占用空间最大的一个.
符号引用被最终链接的时候必须要被正确决议, 如果没有找到该符号的定义, 就会报符号未定义错误undefined symbol of xxx, 这种称为强引用(Strong Reference). 而弱引用(Weak Reference)则被处理的时候如果未定义, 不报错, 链接器会默认其为0或者是一个特殊值. 默认都是强引用, 可以使用GCC的__attribute__ ((weakref))定义一个弱引用.
弱符号和弱引用的作用 :
对于库来说十分有用, 库中定义的弱符号可以被用户定义的强符号覆盖, 程序则可以使用自定义的库函数
或者程序可以对某些扩展功能模块的引用定义为弱引用,
当我们将扩展模块与程序链接在一起时, 功能模块可以正常使用;
如果我们去掉了功能模块, 程序也可以正常链接, 只是扩展模块的功能将不起作用.
符号修饰和函数签名
很久之前, 编译器编译源代码产生目标文件时, 符号名与相应的变量和函数的名字是一样的, 例如函数foo, 经过编译后对应的符号名也是foo, 那么久会产生冲突, 例如要使用Fortran语言编写的目标文件, 一链接就会报错. 为了解决这种冲突, 规定C语言的全局变量和函数经编译后, 符号名前加上_, 此时foo编译后符号名为_foo. 但是还是不能完全解决C语言源文件之间链接产生的问题, 因为大家都有下划线啊! 于是C++开始设计的时候就考虑到了这个问题, 衍生出了命名空间(Name Space).
在C++中, int func()和int func(int)和int func(float)是三个不一样的函数, 这里我们引用一个术语函数签名(Function Signature), 函数签名包括一个函数的信息, 包括函数名, 参数类型, 所在的类和命名空间等其他信息. 于是, 以上三个函数编译后各自的符号名均不一样但是有规律可循.
int func() -编译后-& _int_func_
int func(int) -编译后-& _int_func_int_
int func(float) -编译后-& _int_func_float_
// 这里只是举个栗子, 告诉大家他们的符号名不一致, 至于会变成什么样, 需要看是什么编译器
很久很久以前, 人们把所有代码写在一个文件中, 到后来, 人类已经没有能力维护这个程序了. 于是人们把代码根据功能或性质划分为不同的模块. 于是, 将这些模块拼接起来的过程就叫 : 链接
不知道大家看完上述的编译过程有没有这么一个疑问 : 如果编译的时候编译器不知道一个外部符号的地址, 怎么办? 答案就是不管, 先放一边, 等到链接的时候再把地址修正, 这就是重定位该做的事.
链接过程包括 : 地址和空间分配(Address and Storage Allocation), 符号决议(Symbol Resolution) 和 重定位(Relocation).
最基本的静态链接过程 : 把各个目标文件(.o文件)和库(Library)一起链接形成可执行文件.
那么他们每个文件中的段是怎么合并起来呢?
ELF用的就是相似段合并 : a的.text和b的.text合并, a的.data与b的.data合并, 其他段类似.
符号决议和重定位
符号地址的确定
符号地址的确定.png
每个需要被重定位的段都有一个与之相对应的重定位表, 如.text段对应.rel.text
根据重定位表中每个符号的信息, 找到每个符号对应的目标对象文件, 再根据偏移(offset)确定其绝对地址(或相对地址).
为什么有了静态链接还需要动态链接?
1. 为了节省内存和磁盘空间
例如 : 由于我们操作系统中总是多进程并发的, 如果程序A和B都用到了lib.o, 那么由于是静态链接, 每个程序都有lib.o的一份副本, 当我们同时运行A和B时, lib.o在磁盘和内存中便存在两个副本. 这多耗费内存空间.
2. 静态链接对程序的更新, 部署和发布带来许多麻烦
例如 : 程序A用到了一个第三方厂商提供的lib.o, 当厂商更新了lib.o, 例如修复了其中一个bug. 那么程序A就必须先拿到最新的lib, 再将其链接, 发布. 缺点非常明显, 只要有任何一个模块更新, 整个程序就必须重头链接, 发布给用户.
动态链接怎么解决以上静态链接的短板?
1. 假设我们要运行程序A, 系统会首先加载programA.o, 当系统发现其用到了lib.o, 就会接着加载lib.o, 如果还依赖其他文件, 就会继续按照这种方法逐个加载进内存. 当我们接着运行程序B的时候, 就只加载programB.o而不需加载lib.o, 因为此时系统内存中已经有一份lib.o的副本了, 系统只需要把他们两链接起来即可.
2. 当有新的模块更新时, 只需要将旧的目标文件更新覆盖掉, 不需要将所有程序重新链接一遍. 当程序下次运行时, 新版本的目标文件会被自动装载到内存并且链接起来, 程序就完成了升级了.
程序可扩展性和兼容性
程序可扩展性 : 动态链接还有一个特点就是程序可以在运行的时候选择加载各种程序模块, 这就是我们熟知的`插件`.
兼容性 : 一个程序在不同的平台运行时可以动态链接该操作系统的动态链接库, 这就消除了程序对操作系统的依赖性.
例如 : 操作系统A和操作系统B对于printf的实现机制不同, 程序A如果是采用静态链接, 那么就必须分别针对操作系统A和B分布两个不同的版本, 如果是采取动态链接就减少了这种麻烦.
动态链接是否完美无缺
答案肯定是否. 否则早就把静态链接淘汰掉了. 由于程序所依赖的某个模块更新后有可能与旧模块之间`接口不兼容`, 导致程序无法运行, 崩溃, 这种问题成为`DLL Hell`
动态链接的基本实现
动态链接是不是直接使用目标文件(.o文件)进行链接呢? 理论上可行, 但实际有区别. 由于动态链接的情况下, 进程的虚拟地址空间的分布会比静态链接更为复杂, 还有一些存储管理啊, 内存共享, 进程线程等机制也会有变化.
Linux系统下, ELF动态链接文件为动态共享对象(DSO, Dynamic Shared Objects), 简称共享对象, 一般以.so为后缀
Windows系统下, 称为动态链接库(Dynamical Linking Library), 就是我们常见的.dll为后缀的文件.
也就是说, 动态链接在这个阶段, 实际上是把目标文件和.so文件(或.dll文件)进行链接.
What? 为什么不是把全部.o文件链接起来? 实际上动态链接主要工作并不是在链接这个阶段做的, 否则跟静态链接有什么区别, 何来的动态? 对吧. 其主要工作是在程序被装载进内存的时候. 那么这个阶段的链接有啥用?
还记得链接要完成的三件事情吗?
地址和空间分配, 符号决议和重定位.
对的没错说的就是你 -& 符号决议. 这个.so动态库的作用就在于此. 它是用来告诉链接器 : "哥们, 这个符号采取的是动态链接, 在这里你就别管它地址是多少了, 等程序被装载进内存的时候自然有人负责的啦."
于是, 链接就这么结束了, 可执行文件就这么被生成了咯. 剩下的动态链接工作在下面装载的时候由动态链接器完成.
可执行文件
我们前面说过, 可执行文件也就是目标文件, 其实没什么不一样. 略过
程序想要运行起来, 就必须被装载进内存中才能被CPU调度到.
早期程序装载 : 把整个程序一次性加载到内存中, 然后执行.
// 现在的游戏动不动机会几十G的, 哪来那么多内存资源啊
覆盖装入 : 在覆盖管理器(Overlay Manager)的辅助下, 进程使用到什么模块就把该模块载入到内存中替换掉不需要使用的模块
// 随着虚拟存储机制的发明而诞生出一种技术 -& 页映射
页映射 : 将内存和磁盘中的数据和指令按照"页(Page)"为单位划分, 以后所有的装载, 操作的单位就是页. x86下页的大小为4096字节
页映射和页装载.png
事实上, 可执行文件并不是直接与物理内存直接映射的, 否则也没有虚拟内存什么事了对吧. 而且程序直接访问物理内存有几个坏处 : 地址空间不隔离, 内存使用效率低, 程序运行的地址不确定等.. 实际上, CPU发出的Virtual Address经过MMU(Memory Management Unit)转换成physical Address之后才能访问物理内存
从操作系统的角度看可执行文件的装载
创建一个独立的虚拟地址空间
这一步所做的是虚拟空间与物理内存的映射关系. 分配一个页目录(Page Directory), 页映射关系可以等到后面程序发生页错误再设置
读取可执行文件, 建立可执行文件与虚拟空间的映射关系
这一步做的是虚拟空间与可执行文件的映射关系. 当发生页错误时, 操作系统从物理内存中分配一个物理页, 然后将该"缺页"从磁盘中读到内存中, 再设置虚拟页和物理页的内存映射关系. 操作系统捕捉到页错误时, 它应该知道程序当前所需要的页在可执行文件中的哪一个位置, 这就是可执行文件和虚拟空间的映射关系.
将CPU的指令寄存器设置成可执行文件的入口地址, 启动运行!
这一步操作系统执行一条跳转指令跳到可执行文件的入口地址( 不是main函数, 不是main函数, 不是main函数, 重要的事情说三遍!!! ). 实际上并没有那么简单, 到程序能成功运行还差许多步骤. 这里只是将这些过程屏蔽了.
虚拟内存中分为许多个段(Segment), 每一个段就成为VMA(Virtual Memory Area). 还记得目标文件我们说过的段(Section)吗, 此段非彼段, 这里我们就说英文吧. 目标文件中的多个操作权限相同的Section在这里要被合并成一个个Segment, 再装载进程序的虚拟内存中. 如图所示 :
ELF可执行文件与进程虚拟空间映射关系.png
进程运行在内存中的的VMA布局就如图所示 :
常见进程的虚拟空间.png
执行完上面那些步骤, 实际上可执行文件的真正指令和数据都还没被装入到内存中. 可执行文件只是与虚拟内存建立了映射关系. 当真正执行指令的时候, 会发现虚拟内存中的页面为空, 这时候就产生页错误(Page Fault). 进程将控制权交给操作系统, 操作系统由上面所说Page Directory, 找到空页面所在的VMA, 计算出相应的页面在可执行文件中的偏移(Offset), 然后在物理内存中分配一个物理页面, 将进程中该虚拟页与物理页之间建立映射关系, 再把控制权交回给进程, 进程从刚才页错误的位置重新开始执行.
动态链接器
还记得之前链接的时候如果是动态链接的话, 那么我们会把符号决议和重定位推迟到加载时进行吗? 如果该程序采取的是动态链接, 那么可执行文件装载完之后, 动态链接器就要闪亮登场了!!!
启动动态链接器本身 -& 装载所有需要的动态库 -& 重定位和初始化
启动动态链接器本身
动态链接器本身也是一个动态库, 其他普通动态库的重定位工作由动态链接器来完成, 那么动态链接器的重定位又由谁来完成? 它可否依赖于其他动态库?
这是一个先有鸡还是先有蛋的问题, 为了解决该问题, 动态链接器必须有些特殊 :
1. 动态链接器本身不能依赖于其他任何动态库
2. 动态链接器本身所需要的全局和静态变量的重定位工作由它本身完成
这样, 动态链接器必须在启动时有一段精巧的代码完成这项工作而又不能用到全局和静态变量, 这就是自举(Bootstrap).
装载所有需要的动态库
完成bootstrap后, 动态链接器把ELF文件和链接器本身的符号表都合并到一个表中, 称为全局符号表(Global Symbol Table)中. 然后链接器开始寻找ELF文件所依赖的动态库, 一个个遍历下去, 直到所有动态库都被加载进来.
重定位和初始化
装载完所有需要的动态库后, 动态链接器开始重新遍历ELF文件和每个动态库的重定位表, 把每个需要被重定位的位置进行修正.
完成这些工作后, 链接器就可以松一口气, 把进程的控制权交还给程序的入口并且开始运行了.
特殊的动态链接
我们知道, 有一些函数或者一些用户比较少用的功能模块, 也许到程序结束运行都不会用到, 那么如果程序运行的时候也把这些一并链接的话, 这实际是一种浪费, 无用功. 所以才有了这个延迟绑定 : 当函数第一次被用到时才进行绑定(符号查找, 重定位等), 没用到则不绑定.
显式运行时链接(Explicit Run-time Linking)
也叫运行时加载. 也就是让程序自己在运行时控制加载指定的模块, 并且可以在不需要该模块的时候将其卸载. 这种动态库往往被叫做动态装载库(Dynamic Loading Library).
这种加载方式对于需要长期运行的程序来说具有很大的优势, 最常见的便是Web服务器程序.
上面我们说到, 动态链接器的任务完成之后就会把控制权交回给程序的入口, 那么这个所谓的程序入口, 是一个什么家伙呢? 我们创建一个命令行项目看看
#include &stdio.h&
int main(int argc, const char * argv[]) {
// insert code here...
printf("Hello, World!\n");
首先, 这个程序入口肯定不是main函数, 你看他还有参数啊哥们!! 那就是别的函数传给他的玩意!
argc : 保存的是命令行参数数量
argv : 保存的是命令行参数字符串数组
在执行main函数以前, 程序需要初始化运行环境, 初始化堆栈, I/O, 线程等等. 这通通在一个我们称之为入口函数或入口点(Entry Point)的地方完成. 等初始化之后, 才轮到main函数出场. main函数结束后, 回到入口函数, 进行清理工作, 然后进行系统调用结束进程.
// 伪代码如下
Enrty Point()
if (程序执行){
init run- // 存在于系统中的一些公用数据, 任何程序都可以访问, 如系统搜索路径, 当前OS版本等.
init heap,
main(argc, argv); // 初始化完毕, 执行main函数
} else if (程序退出)
/* 做一些清理工作 */
exit(); // 调用系统接口结束进程
终于讲到内存了, 关于内存那就是程序永恒的话题, 各种内存管理, 泄露问题让程序员头疼不已啊...
每个进程内存空间内都有以下默认的区域 :
栈 : 用于维护函数调用的上下文, 离开了栈函数调用就无法实现. 栈通常在用户空间的最高地址处分配.
堆 : malloc或new分配的内存就在这里. 堆通常在栈的下方(低地址方向).
可执行文件映像 : 存储可执行文件在内存的映像.
保留区 : 对内存中受到保护而禁止访问的内存区域的总称.
于是有了以下这个经典的进程内存布局图 :
进程内存布局图.png
栈是程序中最重要的概念之一, 没有栈就没有函数, 没有局部变量, 栈遵循FILO(First In Last Out)规则.
栈保存了一个函数调用所需要的维护信息, 称为堆栈帧(Stack Frame). 堆栈帧一般包括如下几个方面的内容 :
1. 函数的返回地址和参数
2. 临时变量 : 包括函数的非静态局部变量以及编译器自动生成的其他临时变量
3. 保存的上下文 : 包括在函数调用前后需要保持不变的寄存器
一个堆栈帧用两个寄存器划定范围 : ebp和esp.
esp寄存器 : 始终指向栈的顶部
ebp寄存器 : 指向堆栈帧的一个固定位置, 又称帧指针(Frame Pointer).
堆栈帧.png
函数的返回地址 : ebp-4
压入栈中的参数地址 : 分别是 ebp-8, ebp-12等示参数的数量和大小而定.
ebp所直接指向的数据是调用该函数前ebp的值, 这样在函数返回的时候, ebp可以通过读取这个值恢复到调用前的值.
i386下的函数调用流程
把所有或一部分参数压入栈中, 如果有其他参数没有入栈, 那么使用某些特定的寄存器传递
把当前指令的下一条指令的地址压入栈中
跳转到函数体执行
其中第2, 3步由指令call一起执行. 伪代码如下
// 把ebp压入栈中(称为old ebp)
mov ebp, // ebp = esp(这时ebp指向栈顶, 此时栈顶就是old ebp)
[可选] sub esp, XXX; // 在栈上分配XXX字节的临时空间
[可选] push XXX; // 如有必要, 保存名为XXX寄存器(可重复多个)
把ebp压入栈中, 是为了在函数返回的时候便于恢复以前的ebp值. 那为什么保存一些寄存器呢, 有一些编译器可能要求某些寄存器在调用前后保持不变. 于是在函数返回时, 代码就恰好相反
[可选] pop XXX; // 如有必要, 恢复保存过的寄存器(可重复多个)
mov esp, // 恢复esp同时回收局部变量空间
// 从栈中恢复保存的ebp的值
// 从栈中取得返回地址, 并跳转到该位置.
毫无疑问, 函数的调用方与函数被调用方对函数如何调用必须有着相同的理解, 否则将会出现错乱. 如
Mike : hello, john!
二狗蛋 : 黑龙江?
这种对函数的约定称为调用惯例(Calling Convention) , 内容如下 :
- 函数参数的传递顺序和方式
1. 通过栈传输, 压栈顺序是从左往右还是从右往左?
2. 通过寄存器传输, 提高性能.
- 栈的维护方式
函数参数pop是由函数调用方来完成还是函数本身来完成?
- 名字修饰(Name-mangling)的策略
对函数名进行修饰(链接的时候曾讲到这个问题, 如foo() -& _foo() )
在C语言中默认的调用惯例是cdecl
参数传递 : 从右往左的顺序压参数入栈
出栈方 : 函数调用方
名字修饰 : 直接在函数名称前加1个下划线
下面我们用一个例子来形容这个调用惯例.
void func(int x, int y)
int main()
func(1, 3);
流程如下 :
调用惯例实例.png
相对于栈而言, 堆更加复杂, 程序随时可能发出申请内存和释放内存的指令, 而申请的内存的大小也大小不一. 下面介绍堆的工作原理.
为什么需要堆? 什么是堆?
如果只有栈, 那么函数返回的时候栈上的数据就会全部被pop掉, 无法将数据传给函数外部. 这样的话全局变量则无法动态地产生与销毁
相对于栈, 堆是一块巨大的空间, 占用了程序大多数的虚拟空间. 在这里, 程序可以自由地申请和释放内存空间. 如 :
char *p = (char *)malloc(1000); // 申请1000个字节的内存空间
free(p); // 释放1000个字节的内存空间
既然是申请内存空间, 那么这个过程完全可以丢给操作系统去做. "喂! 操作系统, 我这里需要xxx字节的内存, 快给我分配一下..", 想象下我们操作系统多进程并发的情况, 这显然非常低效. 所以应该一次性向操作系统申请一块适当大小的堆空间. 就像你爸一次性给你一个月的零花钱而不用你每天张手跟他要零花钱一样.
怎么向堆申请空间呢? 我们知道用malloc函数, 却不知道其背后做了什么.
程序的确是通过malloc向堆申请空间, 而我们清楚的知道, 如果每次malloc都向操作系统申请的话很影响效率, Linux下通过mmap()函数向操作系统申请一块堆空间(Windows下为VitualAlloc() ), 以后malloc时就会从这里索取需要的空间, 只有这里的堆空间又不足了, 堆才会向操作系统再申请多一块堆空间.
假设程序员总是向堆申请内存空间, 使用完后又不及时释放(free)掉, 这就会造成内存泄漏. 操作系统不会自动回收堆空间, 因为它不知道这一块内存到底是不是有人在用啊. 于是久而久之, 操作系统能用的内存空间就会越来越少, 我们就会感到越来越卡. 这个时候往往重启一下电脑(手机), 这种情况就会改善. 就是这个原因啦.
线程相对于进程而言, 其访问权限就没那么多约束, 一般来说线程与线程共享整个进程内存的所有数据, 线程甚至可以访问其他线程的堆栈(比较少见)
函数的参数
线程局部存储(TLS, Thread Local Storage)数据
线程之间共享(进程所有)
堆上的数据
函数里的静态变量
程序代码, 任何线程都有权利读取并执行任何代码
打开文件, A线程打开的文件可以有B线程读写
用户线程和内核线程之间的关系 :
好处 : 线程之间真正的并发, 一个线程阻塞不会影响到其他线程
坏处 : 由于操作系统限制了内核线程的数量, 所以用户线程的数量也会受到影响, 内核线程调度时, 上下文切换开销大导致用户线程执行效率低
好处 : 高效的上下文切换和几乎无限制的用户线程数量
坏处 : 由于多个用户线程对应一个内核线程, 所以其中一个用户线程阻塞会导致其他线程也随之阻塞
多对多 : 结合了一对一和多对一的优缺点, 折中
进程的生命终究走到了尽头, 从main函数return之后就回到入口函数处, 回到梦开始的地方, 把所有资源一一释放掉, 然后转身走开, 不带走一片云彩... 有缘再见~
相同点 : 都是应用程序接口
& API是源代码层面的接口, 而ABI是二进制层面的接口
& API相同不等于ABI相同 (例如, 同样一个printf函数, 在不同的系统中的底层实现有可能不一样)
ps : 二进制兼容(ABI兼容)实现相当困难
编译器优化 和 CPU的动态调度换序
编译器优化
即便是短短的一条x++;的代码, 翻译成汇编语言之后也是需要几条来执行. 那么编译器为了优化代码, 有时候会将一个变量缓存到寄存器而不立即写回(时间局部性原理), 又或者调整这些指令的顺序... 所以多线程下没有绝对的安全(上锁也不例外)
CPU的动态调度换序
CPU的为了优化有时也会乱序执行代码, 也就是说不是一行接着一行执行代码, 那么在一些情况下也会有安全漏洞, 例如单例模式下, 有可能取到的是一个未初始化的对象.
解决办法 :
编译器优化 : 使用volatile关键字
CPU的动态调度换序 : 使用barrier
我们最熟悉不过的带变长参数的函数就是int printf(const char *format, ...); 我们知道, 除了第一个参数外, 还可以追加任意数量, 任意类型的参数.
我们用一个简单的函数来说明这种变长参数的实现原理. 如 : int sum(int num, ...);
当我们调用 int n = sum(1, 3, 5, 7);时, 按道理我们只能用num来访问1这个参数, 其他参数访问不了. 但是多亏了C语言默认的cdecl调用惯例的自右向左压栈的传递方式. 此时函数内部的堆栈如下 :
变长参数.png
由于其他的几个参数在num的高地址方向, 所以我们可以间接利用num来访问其他的那几个参数. 而printf函数接收的参数变量类型不一致, 所以比这个要复杂得多得多
对于一些人来说, 对硬盘格式化就是硬盘的数据全部都没啦..
这种观点完全错误!!! 硬盘上装的是什么? 0和1的序列.. 实际上操作系统是根据一张表, 在表中找到你要找的文件在硬盘中的具体位置, 然后再到硬盘中访问.
格式化的本质就是把这张表的内容全擦除掉, 这样操作系统就忘记了你的文件放在哪里了. 尽管文件还是在原来的地方, 他也找不到了.
现在有种方法就是格式化就把这些区域全部用0或者用1填充.
所以大家的SD卡啊, 硬盘啊, 宁愿破坏再扔掉也不要轻易交给别人啊!!!
广工大-大三生, iOS Dev
2016年国庆假期终于把此书过完,整理笔记和体会于此。 关于书名 书名源于俄罗斯的演员斯坦尼斯拉夫斯基创作的《演员的自我修养》,作者为了写这本书前前后后修改了三十年之久,临终前才同意不在修改,拿去出版。使用这个书名一方面书单内容的确不是介绍一门新的编程语言或是展示一些实用的...
一、温故而知新 1. 内存不够怎么办 内存简单分配策略的问题地址空间不隔离内存使用效率低程序运行的地址不确定 关于隔离 : 分为 虚拟地址空间 和 物理地址空间 分段 : 把一段程序所需要的内存空间大小映射到某个地址空间 分页 : 把地址空间人为地等分成固定大小的页,每一页...
总结自书籍《程序员的自我修养—链接、装载与库》 1. Hello World运行中被隐藏的过程 HoelloWorld在编译运行过程中可以分为4个步骤,预处理、编译、汇编和链接。 预处理:主要处理源代码文件中以#开头的预编译指令。比如#include
命令告诉预处理器读取...
bestswifter: 我推荐的iOS开发书单《程序员的自我修养》读书总结** 落影loyinglin研读《程序员的自我修养—链接、装载与库》编译与链接过程的思考静态库与动态库的思考 ** 说真的,有些人,真的是优秀到让你,不自觉就感到自卑了。** 一. 从源码到程序 程...
Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智能路由,微代理,控制总线)。分布式系统的协调导致了样板模式, 使用Spring Cloud开发人员可以快速地支持实现这些模式的服务和应用程序。他们将在任何分布式...
男儿东北有参酒, 不食京师鲍翅粥。 等闲跨马踏天下, 呼风唤雨醉不休。
今天五点半就起床了,这一定是我这辈子起的最早的一次。以前在妈妈家我都睡到七八点,娃啥时候醒把我叫醒我才起床。离开妈妈家后因为要准备早饭从最开始的七点半到后来的六点半,为了娃醒了之后能全心陪伴又提前到六点起床准备早饭和老公的午餐。我家娃起床太早了,没办法我也必须的更早起不然...
忘了她,再也不见她
撕掉所有的来信
扔掉记载美好的相片
连房间里她的一缕淡淡的香味也打扫干净
忘了她,再也不见她
不听她的软语呢喃
躲开她温柔的眼神
连那天彼此许下的承诺都不再想起
可是,我为什么还想念
为什么微风起时 还渴望风中有她的讯息
四点 闹钟在不屈不挠的叫着 朱戬翻了个身 挠了挠自己的菠萝头 起身 走到查杰的床边 “啪!啪!啪!”对着查杰的屁股就一通狂拍 “查杰,起床啦,查杰!查杰!查杰……” “嗯嗯~~贱猪,走开!” “快起,要晚啦!小心昊哥扣你工资啊!” “不管!” 查杰翻了个身,...
“ 这小子……又一屁股睡到天亮!”我软磨硬泡地要求他让闹钟叫起床宣告失败后,无赖得继续每日人工闹铃工作。
“妈妈,就喜欢你的声音开启我的一天,这么的话整天心情都会不错滴”小Z糖衣炮弹的攻击力依然那么强劲,我又一次败下阵来……
“妈妈,其实我已经很伤心了...

我要回帖

更多关于 idea滚动鼠标代码不动 的文章

 

随机推荐