第一篇:书籍简介与设计
书脊设计,精装书还有护封设计;版式设计,包括扉页、环衬、字体、开本、装订方式等;插图,包括题头、尾花和插图创作等。
一、封面设计
假如书籍装帧犹如一组建筑,那么书籍封面无疑是这些建筑的外观。不管是西方哥特式的教堂,还是中国古典式的皇宫寺院,建筑外观都能体现出建筑的精神。而封面也是如此,将集中地体现书籍的主题精神,它是书籍装帧设计的一个重点。
封面的形式要素同样包括了文字和图形两大类,封面设计也同样需要突出主体形象。但从构思到表现都讲究一种写意美。表现在以文字为主和以图形为主的设计上,都是如此。
1.文字
封面上简练的文字,主要是书名(包括丛书名、副书名)、作者名和出版社名,这些留在封面上的文字信息,在设计中起着举足轻重的作用。
在设计过程中,为了丰富画面,可重复书名、加上拼音或外文书名,或目录和适量的广告语。有时为了画面的需要,在封面上也可以不安排作者名有出版社名,让它们出现在书脊和扉页上,封面只留下不可缺少的书名。说明文(出版意图、丛书的目录、作者简介)责任编辑、装帧设计者名、书号定价等,则根据设计需要安排在勒口、封底和内页上。充满活力的字体何尝不是根据书籍的体 裁、风格、特点而定,字体的排列同样象广告设 计构图中所讲述的,把它们视为点、线、面来进行设计,有机地融入画面结构中,参与各种排列组合和分割,产生趣味新颖的形式,让人感到言有尽而意无穷。
2.图形
封面上的图形,包括了了摄影、插图和图案,有写实的、有抽象的、还有写意的。具体的写实手法应用在少儿的知识读物、通俗读物和某些文艺、科技读物的封面设计中较多。因为少年儿童和文化程度低的读者对于具体的形象更容易理解。而科技读物和一些建筑、生活 用品之类一画册封面运用具象图片,就具备了科学性、准确性和感人的说明力。
有些科技、政治、教育等方面的书籍封面设计,有时很难用具体的形象去提炼表现,可以运用 抽象的形式表现,使读者能够意会到其中的含义,得到精神感受。
在文学的封面上大量使用“写意”的手法,不只是象具象和抽象形式那样提炼原著内容的“写意”。而是似象非象的形式去表现。中国画中有写意的手法,着重于抓住形和神的表现,以简练的手法获得具有气韵的情调和感人的联想。有人把自然图案的变化方法也称为 “写意变化”,在简练的自然形式基础上,发挥想象力,追求形式美的表现。进行夸张、变化和 组合。而运用写意手法作为封面的形象,会使封面的表现更具象征意义和艺术的趣味性。如《少年文艺》封面“少”字的大特写,好似飘扬的红领巾,给人一种联想,从构思到形式上都体现了《少年文艺》的个性。而变形的儿童读物封面,更能引起孩子们的兴趣,从中能找到童话、神话和寓言故事中自己的知心朋友。那些具有写意的中外古今图案,在体现民族风格和时代特点上也起着很大的作用。
3.封面整体
封面其实并不只是正面,诚然,人们关心的主要是正面。但是出于审美的高要求,不应当将反面弃之不顾,再看书脊在书架上也同样 发挥着广告和美观的作用。因此,封面的正反面和书脊都应纳入封面设计的范围。整个封面是书籍装帧大整体中的一个小整体,正反和书脊的相互关系有着统一的构思和表现,这种关系处理得成败,同样影响着书籍装帧设计的整体效果。
我们可以总结出几种类型:
A.正反面设计完全相同。或大体设计相同,但文字有所变动,正面出现书名,反面采用拼音、外语,或极小的责任编辑、装帧设计人员名字。正反两面色彩、设汁有所变化。
B.以一张完整的设计画面分成封面、封 底和书脊,分别装饰文字。
C.封底以封面缩小的画面,或小标志、图案与正面形成呼应。
D.书脊应该是封面设计的体现,尤其在厚厚的书籍上,表现尤为如此,而不应满足于排列书名、作者名和出版社名。通过与正面书名相同的字体,在狭长的面积内,安排好大小、疏密关系,有些运用几何的点、线、面和图形进行分割和与正反面形成呼应,并与之形成节奏变化。
E.书脊的设计可以独居一面,可以用文字压在跨面的设计上。在精装的书籍上,常常还有护封,既能起一种保护作用,同时也是一种重要的宣传手段,是一种小型广告。护封设计纸张、印刷精彩,表现力丰富。有护封的封面可以简洁些,达到变化的效果,同时又因为封面运用了亚麻布、漆布、皮革等装帧材料和印刷工艺的制约,故采用简洁的表现式。护封的勒口也需精心设计,成为封面整体的一部分,并可利用其刊登内容提要、作者介绍、出版信息,丛书目录等。护封分全护封和半护封,半护封的高度只占封面的一半,包在封面的腰部,故称为腰带,用来刊登书籍广告和有关书的一些补充各项,也起着装饰作用。
二、版面设计
所谓版面设计,就是指在一定的新闻出版物幅面内,确定版面各要素的合理布局,即确定版心的尺寸及其在版面上所占的空间位置、文字的编排形式、文字的字体字号、文字的行间距、图片与表格的位置及排版形式、版面装饰物的使用等,使新闻出版物的版面具有美感并有独特的风格。
在书籍的目录或前言的前面设有扉页。扉页包括扩页、空白页、像页、卷首插页或丛书名、正扉页(书额)、版权页、赠献题词或感谢、空白页等。太多的扉页显得喧宾夺主,因此它的数量不能机械地规定,必须根据书的特点和装帧的需要而定。目前国内外的书籍,往往比较简练,多采用护页、正扉页而直接直进入目录或前言,而版权页的安排则根据具体情况而正。
正扉页上印有书名、作者名、出版者名和简练的图案。由于人们的阅读习惯。正扉页的方向总是和封面一致。当我们打开封面、翻过环衬和空白页;文字就出现在右边版心的中间或右上方。除此也有利用左右两面作为正扉页的设汁,称为两扉页。扉页上的字体不宜太大,主要采用美术字与封面的字体保持一致。扉页的设汁非常简练,并留出大量空白,好似在进入正文之前有块放松的空间。
无论打开正反面封面,总有一张连接封面和内页的版面,叫做环衬,目的在于封面和内心的牢固不脱离。精装书的环衬设计也很讲究,采用抽象的肌理效果、插图、图案,也有用照片表现,其风格内容与书装整体保持一致。但色彩相对于封面要有所变化。一般需要淡雅些,图形的对比相对弱一些,有些可以运用四方连续纹样装饰,产生统觉效果,在视觉上产生由封面到内心的过渡。
版面设计的目的一方面是为了使形式最好地表现版面内容,另一方面是为了建立版面的风格。一般而言,不同的新闻出版物的版面风格都应有所不同。但同一种新闻出版物的总体风格应保持一致。
怎样利用版面设计的手段达到设计版面的风格目标呢?一般认为有如下几种方法:
1.从字体、字号上来体现。如有的版面不用黑体字以保持淡雅的风格,有的版面则大量采用新型字体以体现其新颖、活泼的风格;
2.从正文行距行宽来体现。如有的版面加大行距以使版面明朗悦目,有的版面行宽很长使版面庄重典雅;
3.从图片、表格的形式上来体现。图片表格的大小、形状、数量的多少以及位置的安排都能给版面带来不同的特色;
4.从版面装饰来体现。版面装饰主要指花边、花线、题花、网底、加框等装饰性符号。版面装饰符号如何安排也是体现版面风格的重要手段;
5.从版面空间来体现。有的版面空白多,以体现明朗清秀的风格;有的版面空白少,以容纳更多的信息量。
应该说,版面设计工作是一项具有较强的创造性的工作。版面设计人员起码应具备以下素质:
1.应了解新闻出版工作的各环节,懂得整个编辑排版流程;
2.有较强的新闻理论业务知识,有敏感的“新闻鼻”,也就是说要“识货”,知道那些新闻是最重要的,能基本判断稿件新闻价值和宣传价值的大小;
3.应熟悉一些印刷知识并掌握排版和制版的知识。就现代报纸版面设计人员而言,应掌握激光照排技术;
4.应熟悉本社激光照排系统中字体、字号、版面装饰符号的种类并能熟练应用之;
5.应了解各种制版方法对原稿的要求;
6.有一定的美术知识修养;
7.此外,还应该研究版面的构图、版面的结构模式、版面的风格、版面的阅读适应性以及版面的美化等,使版面设计给人以美感并有利于阅读。
三、插图
插图是一种绘画,但是不同于一般独立欣赏性的绘画,它具有相对的独立性又具有必 要的从属性。插图必须具备一定的绘画条件,不依靠文字。也能从它的形象本身.表现一定 的主题,同时又必须服从原著,成为辅助者,这 就是插图的含义。
插图可以分为两类。一类是文艺性的插图。画者通过选择书中有意义的人物、场景和 情节,用绘画形象表现出来,可以增加读者阅读书籍的兴趣,使可读性和可视性结合起来· 加深对原著的理解,同时又得到不同程度的美 的享受。另一类是科技及史地书籍.这类插图 以帮助读者进一步理解知识内容,以达到文字 难以表达的作用。它的形象语言应力求准确、实际,并能说明问题。一个苹果的照片能帮助 我们看到非常客观的形状、颜色、结构和质感。一粒种子的说明图,不仅能再现它的形状、结 构,而且能把它在土壤中发芽的过程体现出来。
文学插图可以说是文艺性插图的典型.包括了题头、尾饰、单页插图和文间插图。其表现 形式多种多样:有水墨画、白描、油画、素描、版画(木刻、石版画、铜版画、丝网画),水粉、水彩、漫画等等。有写实的.也有装饰性的。插图创作的第一步,在于对原著的理解。不但要了解具体内容和要求,了解原作的主题精神,还要通过深入阅读原著,搞清原著是中国文学,还是外国文学?是古典文学还是儿童文学?是小说、散文、诗歌.还是童话、寓言、笑话?原著风格是粗犷豪放、细腻严谨,还是热情活泼、纯朴深沉?原著中所描写的历史时代、人物形象、服饰道具、日常习俗、建筑环境等。并且通过视觉形象资料加深理解,因为文学是语言的艺术,而 美术是视觉的艺术,没有文学中所描写的生活体验,很难在画面上体现文学内容。只有查阅有关资料,如其民族、时代相近的绘画、雕塑、建筑、工艺品.以及各种文物资料进行分析,将各种感受联系起来,加以综合研究,找出规律,以此为依据,按原著要求确定作品的基调,贯串于全部画幅中。这样书中的形象、资料中的形象、加上自己的想象,做到心中有底,才能表 现深入,而不至于概念,同时也是一种个人的积累不断提高的方法,为以后的创作打好基础有了这一步,则要对原著进行提炼,找到有戏可唱的情节内容。文学原著的篇幅很长,插图又不是连环画式的以图为主、一本书只安排几幅插图,这就需要我们通过一幅插图抓住 一段文字的情节内容的主题,将最具有典型意 义的文字内容,并适合于绘画表现的情节表现出来。这种插图不是停留在看文识图上,而要 经过再创作,使其具有艺术个性的感染力.同 时也要深入具体、刻划入微,让读者从中既能得到艺术的享受。又能感觉到具体的生活形象。陈老莲的《窥简》一图,择取营驾躲在屏风一端看信,发自内心深处的喜悦,红娘偷偷从屏风另一端察看,她那手指点在唇边,轻是机灵的神态,活现出少女聪敏活泼的形象。画面处理极为简洁,以一扇精彩的屏风展现了闺房的环境,上面的花鸟画,无论是飞翔交朴的蝴 蝶,还是切切私语的鸟儿,都巧妙着营营的美好愿望。极为生动形象地说明了这个道理。
正因为一张插图要表现众多的文字内容,有的插图采用了将不同时间、地点纳入一幅画的方法;写实性的插图运用了蒙太奇的手法;装饰性的插图不受透视、比例等客观条件的限制,更多地根据立意组织在画面之中,中国的传统绘画和民间绘画都采用了这种方法。
插图的形式表现丰富,同样按写实和装饰两大类来分,各个国家的插图都有自己风格,即使同一国家、同一类文学作品,也会因为各个插图家的艺术修养、对原著的理解不同,采 用不同的角度、不同的内容、不同的画面形式来表现。而作为一个插图画家,面对着不同内容题材的书籍,要认真研究思索,找到恰当的手法去表现,而不是千篇一律地去对待。黄永玉的插图《阿诗玛》不仅集中而真实地表现了主人翁的欢乐与幸福,劳动与英勇,困难与遭遇,而且对诗的节奏、情绪和风格的表达,也有着周密的匠心经营。整套插图富有韵律感的构图、流畅的线条、朴素淡雅的色彩、变化丰富的 刀法和装饰味。但他为《叶圣陶童话选》、《冯雪峰寓言》作的插图,根据原著不同风格,又采用了不同的表现手法。在《叶圣陶童话选》的插图中,出于画家对童话的理解和儿童欣赏要求的熟悉,更加发挥艺术的夸张和想象,满足儿童对新奇、强烈、稚气和拟人化的兴趣。一个个充满人格化的动物生动活泼地出现 在小读者的眼前。
一本书的插图,不会仅仅一张,因此插图与插图之间也是一个整体的关系问题。作为文章开幕用的题头、闭幕用的尾饰和文本中间的插图,要处理好与字和空间的关系。整套的插图在构思时就要有个整体设计安排,封面、环衬、扉页 到尾声上的插图,以及装饰图案,要贯穿在书籍的主题中,并在书籍装帧这组“建筑”中达到和谐完美,让读者在游览过程中得到充分的精神享受。
第二篇:核电书籍简介
核电书籍简介
一、900 MW压水堆核电站系统与设备(上下):2005年,611页,岭澳培训教材
二、大亚湾核电站系统及运行(上中下):1994年,2381页,大亚湾核电站工程全面介绍 四
大亚湾核电站建设经验汇编01: 1992年,489页,可以参考.五
大亚湾核电站建设经验汇编02: 1994年,546页,可以参考.六
大亚湾核电站建设经验汇编05: 1992年,559页,可以参考.七
秦山三期水堆核电站建设经验汇编01 综合管理 2003,589.八
秦山三期水堆核电站建设经验汇编03 工程建造 2003,615.九
庆祝秦山三期重水堆核电站工程全面建成学术报告会论文集 2003,249.十
核电站建设的项目管理 1997, 416, 核工业集团组织编制,可参考.
第三篇:书籍简介——排版、设计、印刷、纸张
书籍是人类用来纪录一切成就的主要工具,也是人类交融感情,取得知识,传承经验的重要媒介,对人类文明的开展,贡献至钜。
书籍装帧是指塑造书的“体””和“貌”。“体”就是为书籍制作其盛纳内容的容器,“貌”则是将内容传达给读者的外表。书的内容就是通过装帧将“体”和“貌”构成完美的统一体,形成了可供阅读的书籍。印刷和造纸是都是中国古代四大发明之一。它们对人类文明的贡献是不可估量的。
将文字、图画、照片等原稿经制版、施墨、加压等工序,使油墨转移到纸张、织品、皮革等材料表面上,批量复制原稿内容的技术。称之为印刷。
常规印刷必须具备有原稿(original)、印版(plate)、承印物、印刷油墨、印刷机械(printing machinery)五大要素,才能进行印刷。
纸张(Papers)种类:
道林纸:材料为木浆
模造纸:模仿道林纸, 纸浆为旧纸, 破布等
铜版纸:是以模造纸为纸芯, 在其表面加上一层涂料并轧光, 使纸面的纤维缝隙填满, 而印刷效果更佳, 此种纸通常用来印制彩色印刷品。
雪面铜版纸:铜版纸之纸面再加涂料使之不反光 圣经纸:全木纤维纸浆所制之薄纸, 常见用来印制圣经
再生纸:使用过的废纸加以回收处理,制成再生纸浆, 以其取代原木纸浆所产出来的纸类 书籍装帧包含3大部分
(1)书皮:封面设计,包括封面、封底、书脊设计,精装书还有护封设计;
详细包括:封面、封底、书脊/护封、腰封/环衬,前环衬、后环衬,护页/扉页, 副封面/勒口、前勒口、后勒口,折口/函套、书函、书套,半包式、全包式/纸张/厚度/工艺
(2)书心:版式设计,包括扉页、环衬、字体、开本、装订方式等;插图,包括题头、尾花和插图创作等。纸张/厚度/字体、大小、间距,行距、字距/开本/装订方式/插图/版面,天头、底脚、书眉、内口、外口/
印刷常用纸张
凸版印刷纸、新闻纸、胶版印刷纸、铜板纸、书皮纸、字典纸、拷贝纸、板纸。
厚度:50,60,70,80,100,105,115,120,128,150,157,180,200,210,220,240,250,280,300,350,400(g/m2)105,115,128,157(g/m2)进口纸规格较多
平板纸规格:787×1092,850×1168,880×1230 卷筒纸规格:宽度787,1092,850: 铜板纸规格:648×953,787×970,787×1092(目前国内尚无卷筒纸)。889×1194为进口铜版纸规格。
图书开本:通常分三种类型:大型开本、中型开本和小型开本。以787×1092的纸来说,12开以上为大型开本,16~36开为中型开本,40开以下为小型开本,但以文字为主的书籍一般为中型开本。开本形状除6开、12开、20开、24开、40开近似正方形外,其余均为比例不等的长方形,分别适用于性质和用途不同的各类书籍。
787×1092mm 开数(正)尺寸单位(mm)2开 540×780 3开 360×780 4开 390×543 6开 360×390 8开 270×390 16开 195×270 32开 195×135 64开 135×95 注:成品尺寸=纸张尺寸-修边尺寸
大度纸张:889*1194mm 开数(大)尺寸单位(mm)
2开 590×880 3开 395×880 4开 440×590 6开 395×440 8开 295×440 16开 220×295 32开 220×145 64开 110×145 注:成品尺寸=纸张尺寸-修边尺寸 装订方式:平装、精装、线装、散页装和软精装。详细:龟册装、简策装、卷轴装、旋风装、经折装、蝴蝶装、和合装、包背装、线装书、平装、骑马订、精装、特装、活页装订
工艺:烫金、烫银、起鼓、覆膜、UV上光,压纹、磨砂、模切、包边 封面设计的原则
封面是书的外貌,她既体现书的内容、性质,同时又给读者以美的享受,并且还起了保护书的作用。封面设计包括书名、编著者名、出版社名等文字和装饰形象、色彩及构图。如何使封面体现的的内容、性质、体裁,如何使封面能起着感应人的心理、启迪人的思维作用,是封面设计中最重要的一环。
(1)封面设计的造型:造型要带有明显的阅读者年龄、文化层次等特征。如对少年儿童读物形象要具体、真实、准确,构图要生动活拨,尤其要突出知识性和趣味性;对中青年到老年人的读物,形象可以由具象渐渐向于抽象,宜采用象征性手法,构图也可由生动活拨的形式转向于严肃、庄重的形式。
(2)封面设计的色彩:色彩是由书的内容与阅读对象的年龄、文化层次等特征所决定的。对于读者来说,因文化素养、民族、职业的不同,对于书籍的色彩也有不同的偏好。
(3)封面的形式:封面的形式包括了文字和图形两大类,封面设计也同样需要突出主体形象。
封面形式设计法 1.文字
封面上的文字主要包括书名(包括丛书名、副书名)、编著者名和出版社名。
在设计过程中为了丰富画面。可增加适量的广告语。文字的排列可以根据点、线、面的构成方法来进行设计,有机地融入画面中,参与各种排列组合和分割。2.图形
封面上的图形包括了摄影、插图等图案,可为写实的、有抽象的或写意的。具体的写实手法应用在少儿读物、通俗读物的封面设计中较多。因为少年儿童和文化程度低的读者对于具体的形象更容易理解。而科技读物的封面和一些建筑、生活用品之类画册封面运用具象图片,就具备了科学性、准确性和感人的说服力。有些科技、政治、教育等方面的书籍封面设计。有时很难用具体的形象去提炼表现,可以运用抽象的形式表现使读者能够意会到其中的含义,得到精神享受。
在文学的封面上大量使用“写意”的手法,不只是像具象和抽象形式那样提炼原著内容的“写意”。而是似像非像的形式去表现。在简练的自然形式基础上发挥想象力,追求形式美的表现。进行夸张、变化和组合。而运用写意手法作为封面的形象,会使封面的表现更具象征意义和艺术的趣味性。
装帧设计的技巧
封面、封底的设计和书脊的相互关系有着统一的构思和表现,这种关系处理,同样影响着书籍的整体效果。
关于设计技巧,大致可以终结出以下几种类型。1.封面、封底设计
(1)可采用完全相同或大体相同的设计方法,但文学将有所变动。封面出现书名;封底采用拼音、外语,或极小的责任编辑、装帧设计人员名字。正反两面色彩、设计有所变化。
(2)可以用一张完整的设计画面分成封面、封底和书脊,然后分别加入文字。(3)封底以缩小的画面、标志、图案与封面形成呼应。
2.书脊
书脊是封面设计的体现,尤其在厚厚的书籍上表现尤为如此。而不应满足于排列出版信息,可以通过与封面书名相同的字体在狭长的面积内安排大小、疏密关系。有些运用几何的点、线、面和图形进行分割与封面形成呼应,并与之形成节奏变化。
另外,有的书脊设计可以独居一面,可以用文字压在跨面的设计上。
3.在精装的书籍外部,常常还有护封,既起保护用,同时也是一种重要的宣传手段。有护封的封面可以简洁一些。护封的勒口也需精心设计,这是成为封面整体设计的一部分。
护封分全护封和半护封,半护封的高度约占封面的一半,包在封面的腰部,故也称为腰封,用来刊登书籍广告和有关书的一些补充信息,也起着装饰作用。内页版式的设计
版式设计,是指书籍正文部分的格式设计。一般,除封面、环衬和扉页之外,前言也包括在其中。另外,在正文版中,还包括版心的设计。
版心是指正文版式中文字和图形所占的总面积,版心以上的空间叫天头,版心以下的空间叫底脚,左右称为内口和外口。大部分的图书会在版心外围设计页眉、页脚或添加图文标题等。
版心的大小根据书籍的类型而定。一般,画册、影集为了扩大图画效果,宜取大版心,乃至出血处理(画面四周不留空白);文献资料类参考书以文字为主,则可以扩大版心缩小边口;图文并茂的书,图可根据图需要安排,较大的图甚至可以跨页排列或出血处理,使展开的两面相呼应和均衡,让版面更加生动活拨,给人的视线带来舒展感。
在现代的图书版式的设计者,应该紧紧围绕任何烘托标志设计图书内容这一实际功利的需要,运用形式美的法则,进行版式设计的操作。实用与审美并重,构思、设计出无穷无尽的版面变化,使自己所经手的图书生动活泼,多姿多彩。探讨图书版式设计的创新,是具有理论意义和实践意义的,以下有几种图书版式: 1.主题形象强化
在进行版式设计构思时,突出、强化主题形象的措施是,多次、多角度地展示这一主题。如封面、封底、前后环衬、目录、译序、题词、护封都要有主题形象出现,每形象应该有不同的变化。
2.版块分疆缺损
按黄金分割比例,留出相当于黄金分割律画出的一个方块,即顶上顶右排文字作为一块,另顶上通栏留白一块,顶下顶左留白一块。这种版式设计,布局比较合理,标题与排文字的版块左右呼应,高低顾盼,文图分布疏密有致。版面既呈三方块的分疆,又于分疆中有缺损,使之变化。
3.书眉交叉倒错
采用书眉交叉倒错的方法,根据人的视线一般从幅左下方朝右上移动的规律,双码书眉排在地脚,单码书眉排在天头,一天一地,左右交错,全书书眉间隔倒错,耐人寻味。上下左右间隔交错的这种书眉,打破常规的对称之均衡,在形式上呈现令人惊讶的新意,有独特的审美价值。
4.订口为轴对称
将书面摊开后左边与右边两面,即双码与单码两面当成标志设计面看,据此设计版式,常会有一种大气魄的整体感,对视觉会带来新鲜的刺激。以订口为轴的对称版式,外分内合,张敛有致,或造成版面、开本的扩张,或加强向心力的聚敛,有衡稳之效。
5.大胆留出空白
版面空白,是标志设计版面注入生机的一种有效手段。大胆地留出大片,空白,是现代书籍版式设计意识的体现。恰当、合理地留出空白,能传达出设计者高雅的审美趣味,打破死板的常规惯例,使版面通透、开朗标志设计、跳跃、清新,给读者在视觉上造成轻快、愉悦的刺激,目力因之得到松弛、小憩。当然,标志设计片空白不可乱用,一旦空白,必须有呼应,有过渡,以免造成版面空泛。6.图案化的书眉
书眉除具有方便检索查阅的功能外,还具有装饰的作用。一般书眉只占一行,并且只是由横线及文字构成。而用图案做书眉,虽十分夸张,却仍然得体,使被表达对象的标志设计特征更加鲜明、突出,产生一种令人惊奇叫绝的美感
总的来说,在图书排版的时候多进行些设计后的对比,就能看出不一样的效果,但要注意有些小细节的地方,如果设计没有考虑到的,就会导至整本图书的面板视觉。设计年鉴图书封面设计
图书的装帧设计一般包括书的版式、字体、扉页、目次、插画、衬页、封面、纸张、印刷等。其中,封面设计一般是书籍装帧设计中最重要的内容.优秀的封面设计会直接影响读者的购买欲,同时,也是图书内在精神的体现.经典漫画杂志封面设计
杂志属于期刊类书籍,形式设计上相对固定一些,封面通常依据杂志风格来设计,因为杂志具有延续性,封面与标志的风格一旦确立,很少更新版式,因此,杂志封面的版式设计要求也 更高.杂志版式设计
优秀的杂志是非常有个性和感染力的,它的核心是代表杂志的精华内容和版式风格。一个具有简洁之美的版面,同时也具备更丰富的内涵,包括节奏、韵律、流畅等审美需要。这样的版面要求设计人员具备更高层次的人文知识修养。
附图:
附录
一、图书整体设计的概念和内容? 答:图书的整体设计,包括图书外部装帧设计和内文版式设计。图书外部装帧设计,包括图书开本选择,结构样式的确定,以及图书封面、护封、环衬、主书名页、插页等美术设计。图书内文版式设计,包括图书书芯部分的版式设计、用字的选择,版心的确定、文字的排式及图文在版面上的编排。
二、图书整体设计的原则是什么?
答:图书的整体设计,兼具艺术性、技术性、实用性和经济性的统一。必须遵循以下一些原则:
1、整体性原则,要求与图书出版过程中的其他环节相配合(如材料、工艺、技术等);图书内容与形式相统一,使用价值与审美价值相统一;设计的艺术化与图书主题的内涵相统一;
2、艺术性原则,要求充分体现艺术特点和独特创意,体现时代特色和民族特色,同时又体现图书不同性质和门类的特点;
3、实用性原则,要求充分考虑不同层次读者使用不同类别图书的便利,充分考虑读者经济上的承受能力和审美需求,充分考虑审美需要对提高读者阅读兴趣的导向作用。
4、效益比差原则,又称“适度原则”,要求兼顾两方面比差:(1)所需资金投入带来实际经济效益的比差;(2)设计方案导致的图书定价与读者承受能力的比差。
三、什么是开本?图书开本受哪些因素制约?
答:开本是表示图书幅面大小(规格尺寸)的行业用语。开本以全张纸开切的数量(开数)来表示。
同一开数的开本,由于全张纸幅面尺寸大小不同,因此开本相同的图书,其规格尺寸也会有所不同。图书开本的制约因素有3种:
1、图书性质种类的制约。不同种类的图书,对开本有不同要求,如画册、图集、多采用大型开本;学术著作、高等教材、刊物,多采用较大的中型开本;通俗读物、中小学课本,多采用较小的中型开本;少儿读物,多采用小型开本;
2、图书篇幅的制约。较大的图书多采用大中型开本,篇幅较小的图书,多采用中小型开本;
3、图书用途的制约。鉴赏类图书,多采用大中型开本;阅读类图书多采用中型开本;便携类图书,多采用小型开本;
四、图书的结构部件有哪些?
答:
1、书芯。图书的主体内容,由若干“帖”组成(包括正文及辅文)
2、封面。图书的封面包括前勒口;前封面(也称面封)、书脊;底封面(也称封底);后勒口五个部分构成。(也有不带前后勒口的封面)(也有带书壳的封面,称“封壳”)
3、环衬。封面与书芯之间连接起来的衬纸,(前环衬、后环衬)
4、主书名页。位于环衬后、书芯前,习称“扉页”或“内封”,印有书名、著作者和出版者的名称。位于单数页码。反面印有版权说明、在版编目(CIP)数据,版本记录等。
5、插页。一般在主书名页后,印有与图书相关的图版,有“集合型插页”,也有“分散型插页”。
6、护封。(包封)包在硬质封面外的包纸,前后勒口勒住封面封底。
7、函套。有二种“书函”及“书套”,“书函”为我国传统书籍护装物; “书套”为硬质书盒
五、图书有哪些装订样式?
答:平装、精装、线装、散页装和软精装。平装分为普通平装、勒口平装、封面贴膜平装。
精装分为全纸面精装,纸面布脊精装、全面料精装;三种精装样式都有园脊和平脊两种形态。精装书一般采用锁线订,胶背订;
线装:如传统古籍类图书(在右侧适当宽度用线穿订)
散页装:单页装在纸袋或折盒内。如教学挂图、艺术图片的装订。
软精装:在勒口平装的前后封面内添加厚卡纸,使封面挺括度超过一般平装图书。
六、图书的美术设计有哪些手段?
答:图书装帧的美术设计运用的手段主要有:艺术手段,材料手段和技术手段。
1、艺术手段:运用形象、图案、色彩、文字、纹饰等要素,以写真或写意手法,充分表现图书的主题内涵。
2、材料手段:使用适宜的特种纸张、纸板、织物、塑料、皮革、电化铝箔、粉箔、丝带等材料,以裱糊、烫印、火印、覆压等手法。
3、技术手段:借助电子桌面的出版系统对文字、图形、图像进行变形,对色彩进行增减,使设计对象产生特殊艺术效果。
七、图书的版式设计形式有几种? 答:图书的版式设计形式常见的有三种。
1、古典版式设计:以订口为轴心左右两面严格对称。字距、行距统一尺寸的保护性框子,具有均衡、典雅的特点。
2、网格版式设计:运用固定的格子设计版面,把版心分为一栏、二栏、三栏等,具有紧密连贯、结构严谨等特点。
3、自由版式设计:特点版心无疆界、字图一体化,运用计算机技术自由编排。[next]
八、版式设计有哪些艺术规律?
答:
1、对称与均衡。既要有整齐的平衡、又要有变化的平衡。
2、比例与尺度。各要素间合适的比例与尺度,和谐完美。
3、对比与调和。既要有差异性,如大小、黑白对比、又要讲究调和统一。
4、节奏与韵律。既有条理和规律性的重复,又有节奏高级形态的韵律。
5、变化与统一。既有充满智慧与想象力的变化,又统一在有机品整体中。
九、什么是字级?什么是字体?基本字体有哪几种
答:字级是指版面所排文字的大小级别。
铅字用号数制:(即初号、一号、二号-------七号等号数表示字级大小)
之后采用点数制:每点的单位长度为0.35毫米(5号字为10.5点,公制为3.675毫米)照排级数制:每一级为0.25毫米(15级的照相字,为3.75毫米)现代计算机照排字为“无定级”(可任意大小)。字体是指版面所排文字的形体种类。
基本字体有四种:宋体、黑体、楷体、仿宋体
十、图书的正文有哪些排式?
图书正文排式有横排和直排两种基本样式。
所谓横排,是每行文字自左向右排列,每面各行从上到下排列的排式。所谓直排,是每行文字从上到下排列,每面各行自右向左排列的排式。横排样式中,有单栏、双栏、三栏及多栏排式。
十一、图书标题排式要注意什么?
答:
1、标题字级、字体的选择,依据图书类别,开本、标题等级及标题用字的原则(即字级大小有序,字体轻重相间的原则)。
2、标题位置,有居中、居左、居右三种样式,依据开本、标题文字多少、及版面整体布局。
3、标题字空。标题字数少的,适当增加字空。
4、标题占行。与标题各等级相关,一级标题可多占行,二、三级标题逐减少占行,全书前后一致。
5、标题转行。不害文意。虚字不转为下一行的第一字。
十二、版面图文合成有哪几种样式?
答:
1、以文为主。图起辅助作用。合成原则“图随文走”“文先图后”。
2、图文并重。不违反图文在同一版面(或和合面)上合成的原则。
3、以图为主。文字起辅助作用,图注文字,以文字靠近图为宜,(或图注文字集中于某一角,以序号与图版相对应)。
第四篇:[电脑书籍]语言简介与入门
我不想夸大或者贬低汇编语言。但我想说,汇编语言改变了20世纪的历史。与前辈相比,我们这一代编程人员足够的幸福,因为我们有各式各样的编程语言,我们可以操作键盘、坐在显示器面前,甚至使用鼠标、语音识别。我们可以使用键盘、鼠标来驾驭“个人计算机”,而不是和一群人共享一台使用笨重的继电器、开关去操作的巨型机。相比之下,我们的前辈不得不使用机器语言编写程序,他们甚至没有最简单的汇编程序来把助记符翻译成机器语言,而我们可以从上千种计算机语言中选择我们喜欢的一种,而汇编,虽然不是一种“常用”的具有“快速原型开发”能力的语言,却也是我们可以选择的语言中的一种。
每种计算机都有自己的汇编语言——没必要指望汇编语言的可移植性,选择汇编,意味着选择性能而不是可移植或便于调试。这份文档中讲述的是x86汇编语言,此后的“汇编语言”一词,如果不明示则表示ia32上的x86汇编语言。
汇编语言是一种易学,却很难精通的语言。回想当年,我从初学汇编到写出第一个可运行的程序,只用了不到4个小时;然而直到今天,我仍然不敢说自己精通它。编写快速、高效、并且能够让处理器“很舒服地执行”的程序是一件很困难的事情,如果利用业余时间学习,通常需要2-3年的时间才能做到。这份教材并不期待能够教给你大量的汇编语言技巧。对于读者来说,x86汇编语言“就在这里”。然而,不要僵化地局限于这份教材讲述的内容,因为它只能告诉你汇编语言是“这样一回事”。学好汇编语言,更多的要靠一个人的创造力于悟性,我可以告诉你我所知道的技巧,但肯定这是不够的。一位对我的编程生涯产生过重要影响的人曾经对我说过这么一句话:
写汇编语言程序不是汇编语言最难的部分,创新才是。
我想,愿意看这份文档的人恐怕不会问我“为什么要学习汇编语言”这样的问题;不过,我还是想说几句:首先,汇编语言非常有用,我个人主张把它作为C语言的先修课程,因为通过学习汇编语言,你可以了解到如何有效地设计数据结构,让计算机处理得更快,并使用更少的存储空间;同时,学习汇编语言可以让你熟悉计算机内部运行机制,并且,有效地提高调试能力。就我个人的经验而言,调试一个非结构化的程序的困难程度,要比调试一个结构化的程序的难度高很多,因为“结构化”是以牺牲运行效率来提高可读性与可调试性,这对于完成一般软件工程的编码阶段是非常必要的。然而,在一些地方,比如,硬件驱动程序、操作系统底层,或者程序中经常需要执行的代码,结构化程序设计的这些优点有时就会被它的低效率所抹煞。另外,如果你想真正地控制自己的程序,只知道源代码级的调试是远远不够的。
浮躁的人喜欢说,用C++写程序足够了,甚至说,他不仅仅掌握C++,而且精通STL、MFC。我不赞成这个观点,掌握上面的那些是每一个编程人员都应该做到的,然而C++只是我们“常用”的一种语言,它不是编程的全部。低层次的开发者喜欢说,嘿,C++是多么的强大,它可以做任何事情——这不是事实。便于维护、调试,这些确实是我们的追求目标,但是,写程序不能仅仅追求这个目标,因为我们最终的目的是满足设计需求,而不是个人非理性的理想。这份教材适合已经学习过某种结构化程序设计语言的读者。其内容基于我在1995年给别人讲述汇编语言时所写的讲义。当然,如大家所希望的,它包含了最新的处理器所支持的特性,以及相应的内容。我假定读者已经知道了程序设计的一些基本概念,因为没有这些是无法理解汇编语言程序设计的;此外,我希望读者已经有了比较良好的程序设计基础,因为如果你缺乏对于结构化程序设计的认识,编写汇编语言程序很可能很快就破坏了你的结构化编程习惯,大大降低程序的可读性、可维护性,最终让你的程序陷于不得不废弃的代码堆之中。基本上,这份文档撰写的目标是尽可能地便于自学。不过,它对你也有一些要求,尽管不是很高,但我还是强调一下。学习汇编语言,你需要
胆量。不要害怕去接触那些计算机的内部工作机制。知识。了解计算机常用的数制,特别是二进制、十六进制、八进制,以及计算机保存数据的方法。
开放。接受汇编语言与高级语言的差异,而不是去指责它如何的不好读。经验。要求你拥有任意其他编程语言的一点点编程经验。头脑。
祝您编程愉快!
先说一点和实际编程关系不太大的东西。当然,如果你迫切的想看到更实质的内容,完全可以先跳过这一章。
那么,我想可能有一个问题对于初学汇编的人来说非常重要,那就是: 汇编语言到底是什么?
汇编语言是一种最接近计算机核心的编码语言。不同于任何高级语言,汇编语言几乎可以完全和机器语言一一对应。不错,我们可以用机器语言写程序,但现在除了没有汇编程序的那些电脑之外,直接用机器语言写超过1000条以上指令的人大概只能算作那些被我们成为“圣人”的牺牲者一类了。毕竟,记忆一些短小的助记符、由机器去考虑那些琐碎的配位过程和检查错误,比记忆大量的随计算机而改变的十六进制代码、可能弄错而没有任何提示要强的多。熟练的汇编语言编码员甚至可以直接从十六进制代码中读出汇编语言的大致意思。当然,我们有更好的工具——汇编器和反汇编器。
简单地说,汇编语言就是机器语言的一种可以被人读懂的形式,只不过它更容易记忆。至于宏汇编,则是包含了宏支持的汇编语言,这可以让你编程的时候更专注于程序本身,而不是忙于计算和重写代码。
汇编语言除了机器语言之外最接近计算机硬件的编程语言。由于它如此的接近计算机硬件,因此,它可以最大限度地发挥计算机硬件的性能。用汇编语言编写的程序的速度通常要比高级语言和C/C++快很多--几倍,几十倍,甚至成百上千倍。当然,解释语言,如解释型LISP,没有采用JIT技术的Java虚机中运行的Java等等,其程序速度更无法与汇编语言程序同日而语。
永远不要忽视汇编语言的高速。实际的应用系统中,我们往往会用汇编彻底重写某些经常调用的部分以期获得更高的性能。应用汇编也许不能提高你的程序的稳定性,但至少,如果你非常小心的话,它也不会降低稳定性;与此同时,它可以大大地提高程序的运行速度。我强烈建议所有的软件产品在最后Release之前对整个代码进行Profile,并适当地用汇编取代部分高级语言代码。至少,汇编语言的知识可以告诉你一些有用的东西,比如,你有多少个寄存器可以用。有时,手工的优化比编译器的优化更为有效,而且,你可以完全控制程序的实际行为。
我想我在罗嗦了。总之,在我们结束这一章之前,我想说,不要在优化的时候把希望完全寄托在编译器上——现实一些,再好的编译器也不可能总是产生最优的代码。
当时我学过BASIC, Fortran和Pascal,写的是一个
对一个包含100个32bit整数的数组进行快速排序,并且输出出来的小程序。实际上用汇编器写出的机器码与在调试器中用它附带的汇编程序写出的机器码还是有一些细微差别的,前者更大,然而却可能更高效,因为汇编器能够将代码放置到适合处理器的地方这句话假定两个程序进行了同等程度的优化,一个写的不好的汇编程序和一个写的很好的C程序相比,汇编程序不一定更快。
中央处理器(CPU)在微机系统处于“领导核心”的地位。汇编语言被编译成机器语言之后,将由处理器来执行。那么,首先让我们来了解一下处理器的主要作用,这将帮助你更好地驾驭它。
典型的处理器的主要任务包括 从内存中获取机器语言指令,译码,执行
根据指令代码管理它自己的寄存器
根据指令或自己的的需要修改内存的内容 响应其他硬件的中断请求
一般说来,处理器拥有对整个系统的所有总线的控制权。对于Intel平台而言,处理器拥有对数据、内存和控制总线的控制权,根据指令控制整个计算机的运行。在以后的章节中,我们还将讨论系统中同时存在多个处理器的情况。
处理器中有一些寄存器,这些寄存器可以保存特定长度的数据。某些寄存器中保存的数据对于系统的运行有特殊的意义。
新的处理器往往拥有更多、具有更大字长的寄存器,提供更灵活的取指、寻址方式。寄存器
如前所述,处理器中有一些可以保存数据的地方被称作寄存器。
寄存器可以被装入数据,你也可以在不同的寄存器之间移动这些数据,或者做类似的事情。基本上,像四则运算、位运算等这些计算操作,都主要是针对寄存器进行的。
首先让我来介绍一下80386上最常用的4个通用寄存器。先瞧瞧下面的图形,试着理解一下: 上图中,数字表示的是位。我们可以看出,EAX是一个32-bit寄存器。同时,它的低16-bit又可以通过AX这个名字来访问;AX又被分为高、低8bit两部分,分别由AH和AL来表示。对于EAX、AX、AH、AL的改变同时也会影响与被修改的那些寄存器的值。从而事实上只存在一个32-bit的寄存器EAX,而它可以通过4种不同的途径访问。
也许通过名字能够更容易地理解这些寄存器之间的关系。EAX中的E的意思是“扩展的”,整个EAX的意思是扩展的AX。X的意思Intel没有明示,我个人认为表示它是一个可变的量。而AH、AL中的H和L分别代表高和低。
为什么要这么做呢?主要由于历史原因。早期的计算机是8位的,8086是第一个16位处理器,其通用寄存器的名字是AX,BX等等;80386是Intel推出的第一款IA-32系列处理器,所有的寄存器都被扩充为32位。为了能够兼容以前的16位应用程序,80386不能将这些寄存器依旧命名为AX、BX,并且简单地将他们扩充为32位——这将增加处理器在处理指令方面的成本。
Intel微处理器的寄存器列表(在本章先只介绍80386的寄存器,MMX寄存器以及其他新一代处理器的新寄存器将在以后的章节介绍)通用寄存器
下面介绍通用寄存器及其习惯用法。顾名思义,通用寄存器是那些你可以根据自己的意愿使用的寄存器,修改他们的值通常不会对计算机的运行造成很大的影响。通用寄存器最多的用途是计算。EAX 32-bit宽
通用寄存器。相对其他寄存器,在进行运算方面比较常用。在保护模式中,也可以作为内存偏移指针(此时,DS作为段 寄存器或选择器)EBX 32-bit宽
通用寄存器。通常作为内存偏移指针使用(相对于EAX、ECX、EDX),DS是默认的段寄存器或选择器。在保护模式中,同样可以起这个作用。
ECX 32-bit宽
通用寄存器。通常用于特定指令的计数。在保护模式中,也可以作为内存偏移指针(此时,DS作为 寄存器或段选择器)。
EDX 32-bit宽
通用寄存器。在某些运算中作为EAX的溢出寄存器(例如乘、除)。在保护模式中,也可以作为内存偏移指针(此时,DS作为段 寄存器或选择器)。上述寄存器同EAX一样包括对应的16-bit和8-bit分组。用作内存指针的特殊寄存器 ESI 32-bit宽
通常在内存操作指令中作为“源地址指针”使用。当然,ESI可以被装入任意的数值,但通常没有人把它当作通用寄存器来用。DS是默认段寄存器或选择器。EDI 32-bit宽
通常在内存操作指令中作为“目的地址指针”使用。当然,EDI也可以被装入任意的数值,但通常没有人把它当作通用寄存器来用。DS是默认段寄存器或选择器。EBP 32-bit宽
这也是一个作为指针的寄存器。通常,它被高级语言编译器用以建造‘堆栈帧’来保存函数或过程的局部变量,不过,还是那句话,你可以在其中保存你希望的任何数据。SS是它的默认段寄存器或选择器。
注意,这三个寄存器没有对应的8-bit分组。换言之,你可以通过SI、DI、BP作为别名访问他们的低16位,却没有办法直接访问他们的低8位。段寄存器和选择器
实模式下的段寄存器到保护模式下摇身一变就成了选择器。不同的是,实模式下的“段寄存器”是16-bit的,而保护模式下的选择器是32-bit的。
CS 代码段,或代码选择器。同IP寄存器(稍后介绍)一同指向当前正在执行的那个地址。处理器执行时从这个寄存器指向的段(实模式)或内存(保护模式)中获取指令。除了跳转或其他分支指令之外,你无法修改这个寄存器的内容。
DS 数据段,或数据选择器。这个寄存器的低16 bit连同ESI一同指向的指令将要处理的内存。同时,所有的内存操作指令 默认情况下都用它指定操作段(实模式)或内存(作为选择器,在保护模式。这个寄存器可以被装入任意数值,然而在这么做的时候需要小心一些。方法是,首先把数据送给AX,然后再把它从AX传送给DS(当然,也可以通过堆栈来做).ES 附加段,或附加选择器。这个寄存器的低16 bit连同EDI一同指向的指令将要处理的内存。同样的,这个寄存器可以被装入任意数值,方法和DS类似。
FS F段或F选择器(推测F可能是Free?)。可以用这个寄存器作为默认段寄存器或选择器的一个替代品。它可以被装入任何数值,方法和DS类似。
GS G段或G选择器(G的意义和F一样,没有在Intel的文档中解释)。它和FS几乎完全一样。SS 堆栈段或堆栈选择器。这个寄存器的低16 bit连同ESP一同指向下一次堆栈操作(push和pop)所要使用的堆栈地址。这个寄存器也可以被装入任意数值,你可以通过入栈和出栈操作来给他赋值,不过由于堆栈对于很多操作有很重要的意义,因此,不正确的修改有可能造成对堆栈的破坏。
* 注意 一定不要在初学汇编的阶段把这些寄存器弄混。他们非常重要,而一旦你掌握了他们,你就可以对他们做任意的操作了。段寄存器,或选择器,在没有指定的情况下都是使用默认的那个。这句话在现在看来可能有点稀里糊涂,不过你很快就会在后面知道如何去做。特殊寄存器(指向到特定段或内存的偏移量):
EIP 这个寄存器非常的重要。这是一个32位宽的寄存器,同CS一同指向即将执行的那条指令的地址。不能够直接修改这个寄存器的值,修改它的唯一方法是跳转或分支指令。(CS是默认的段或选择器)ESP 这个32位寄存器指向堆栈中即将被操作的那个地址。尽管可以修改它的值,然而并不提倡这样做,因为如果你不是非常明白自己在做什么,那么你可能造成堆栈的破坏。对于绝大多数情况而言,这对程序是致命的。(SS是默认的段或选择器)IP: Instruction Pointer, 指令指针 SP: Stack Pointer, 堆栈指针
好了,上面是最基本的寄存器。下面是一些其他的寄存器,你甚至可能没有听说过它们。(都是32位宽):
CR0, CR2, CR3(控制寄存器)。举一个例子,CR0的作用是切换实模式和保护模式。还有其他一些寄存器,D0, D1, D2, D3, D6和D7(调试寄存器)。他们可以作为调试器的硬件支持来设置条件断点。
TR3, TR4, TR5, TR6 和 TR? 寄存器(测试寄存器)用于某些条件测试。
最后我们要说的是一个在程序设计中起着非常关键的作用的寄存器:标志寄存器。本节中部份表格来自David Jurgens的HelpPC 2.10快速参考手册。在此谨表谢意。先说一点和实际编程关系不太大的东西。当然,如果你迫切的想看到更实质的内容,完全可以先跳过这一章。
那么,我想可能有一个问题对于初学汇编的人来说非常重要,那就是: 汇编语言到底是什么?
汇编语言是一种最接近计算机核心的编码语言。不同于任何高级语言,汇编语言几乎可以完全和机器语言一一对应。不错,我们可以用机器语言写程序,但现在除了没有汇编程序的那些电脑之外,直接用机器语言写超过1000条以上指令的人大概只能算作那些被我们成为“圣人”的牺牲者一类了。毕竟,记忆一些短小的助记符、由机器去考虑那些琐碎的配位过程和检查错误,比记忆大量的随计算机而改变的十六进制代码、可能弄错而没有任何提示要强的多。熟练的汇编语言编码员甚至可以直接从十六进制代码中读出汇编语言的大致意思。当然,我们有更好的工具——汇编器和反汇编器。
简单地说,汇编语言就是机器语言的一种可以被人读懂的形式,只不过它更容易记忆。至于宏汇编,则是包含了宏支持的汇编语言,这可以让你编程的时候更专注于程序本身,而不是忙于计算和重写代码。
汇编语言除了机器语言之外最接近计算机硬件的编程语言。由于它如此的接近计算机硬件,因此,它可以最大限度地发挥计算机硬件的性能。用汇编语言编写的程序的速度通常要比高级语言和C/C++快很多--几倍,几十倍,甚至成百上千倍。当然,解释语言,如解释型LISP,没有采用JIT技术的Java虚机中运行的Java等等,其程序速度更无法与汇编语言程序同日而语。
永远不要忽视汇编语言的高速。实际的应用系统中,我们往往会用汇编彻底重写某些经常调用的部分以期获得更高的性能。应用汇编也许不能提高你的程序的稳定性,但至少,如果你非常小心的话,它也不会降低稳定性;与此同时,它可以大大地提高程序的运行速度。我强烈建议所有的软件产品在最后Release之前对整个代码进行Profile,并适当地用汇编取代部分高级语言代码。至少,汇编语言的知识可以告诉你一些有用的东西,比如,你有多少个寄存器可以用。有时,手工的优化比编译器的优化更为有效,而且,你可以完全控制程序的实际行为。
我想我在罗嗦了。总之,在我们结束这一章之前,我想说,不要在优化的时候把希望完全寄托在编译器上——现实一些,再好的编译器也不可能总是产生最优的代码。
当时我学过BASIC, Fortran和Pascal,写的是一个
对一个包含100个32bit整数的数组进行快速排序,并且输出出来的小程序。实际上用汇编器写出的机器码与在调试器中用它附带的汇编程序写出的机器码还是有一些细微差别的,前者更大,然而却可能更高效,因为汇编器能够将代码放置到适合处理器的地方这句话假定两个程序进行了同等程度的优化,一个写的不好的汇编程序和一个写的很好的C程序相比,汇编程序不一定更快。
在前一节中的x86基本寄存器的介绍,对于一个汇编语言编程人员来说是不可或缺的。现在你知道,寄存器是处理器内部的一些保存数据的存储单元。仅仅了解这些是不足以写出一个可用的汇编语言程序的,但你已经可以大致读懂一般汇编语言程序了(不必惊讶,因为汇编语言的祝记符和英文单词非常接近),因为你已经了解了关于基本寄存器的绝大多数知识。在正式引入第一个汇编语言程序之前,我粗略地介绍一下汇编语言中不同进制整数的表示方法。如果你不了解十进制以外的其他进制,请把鼠标移动到这里。
汇编语言中的整数常量表示
十进制整数
这是汇编器默认的数制。直接用我们熟悉的表示方式表示即可。例如,1234表示十进制的1234。不过,如果你指定了使用其他数制,或者有凡事都进行完整定义的小爱好,也可以写成[十进制数]d或[十进制数]D的形式。十六进制数
这是汇编程序中最常用的数制,我个人比较偏爱使用十六进制表示数据,至于为什么,以后我会作说明。十六进制数表示为0[十六进制数]h或0[十六进制数]H,其中,如果十六进制数的第一位是数字,则开头的0可以省略。例如,7fffh, 0ffffh,等等。二进制数
这也是一种常用的数制。二进制数表示为[二进制数]b或[二进制数]B。一般程序中用二进制数表示掩码(mask code)等数据非常的直观,但需要些很长的数据(4位二进制数相当于一位十六进制数)。例如,1010110b。八进制数
八进制数现在已经不是很常用了(确实还在用,一个典型的例子是Unix的文件属性)。八进制数的形式是[八进制数]q、[八进制数]Q、[八进制数]o、[八进制数]O。例如,777Q。需要说明的是,这些方法是针对宏汇编器(例如,MASM、TASM、NASM)说的,调试器默认使用十六进制表示整数,并且不需要特别的声明(例如,在调试器中直接用FFFF表示十进制的65535,用10表示十进制的16)。
现在我们来写一小段汇编程序,修改EAX、EBX、ECX、EDX的数值。我们假定程序执行之前,寄存器中的数值是全0:
? X H L EAX 0000 00 00 EBX 0000 00 00 ECX 0000 00 00 EDX 0000 00 00
正如前面提到的,EAX的高16bit是没有办法直接访问的,而AX对应它的低16bit,AH、AL分别对应AX的高、低8bit。
mov eax, 012345678h mov ebx, 0abcdeffeh mov ecx, 1 mov edx, 2;将012345678h送入eax;将0abcdeffeh送入ebx;将000000001h送入ecx;将000000002h送入edx
则执行上述程序段之后,寄存器的内容变为:
? X H L EAX 1234 56 78 EBX abcd ef fe ECX 0000 00 01 EDX 0000 00 02
那么,你已经了解了mov这个指令(mov是move的缩写)的一种用法。它可以将数送到寄存器中。我们来看看下面的代码:
mov eax, ebx mov ecx, edx;ebx内容送入eax;edx内容送入ecx
则寄存器内容变为:
? X H L EAX abcd ef fe EBX abcd ef fe ECX 0000 00 02 EDX 0000 00 02
我们可以看到,“move”之后,数据依然保存在原来的寄存器中。不妨把mov指令理解为“送入”,或“装入”。
练习题
把寄存器恢复成都为全0的状态,然后执行下面的代码:
mov eax, 0a1234h mov bx, ax mov ah, bl mov al, bh;将0a1234h送入eax;将ax的内容送入bx;将bl内容送入ah;将bh内容送入al
思考:此时,EAX的内容将是多少?[答案]
下面我们将介绍一些指令。在介绍指令之前,我们约定:
使用Intel文档中的寄存器表示方式
reg32 32-bit寄存器(表示EAX、EBX等)
reg16 16-bit寄存器(在32位处理器中,这AX、BX等)reg8 8-bit寄存器(表示AL、BH等)imm32 32-bit立即数(可以理解为常数)imm16 16-bit立即数 imm8 8-bit立即数
在寄存器中载入另一寄存器,或立即数的值:
mov reg32,(reg32 | imm8 | imm16 | imm32)mov reg32,(reg16 | imm8 | imm16)mov reg8,(reg8 | imm8)
例如,mov eax, 010h表示,在eax中载入00000010h。需要注意的是,如果你希望在寄存器中装入0,则有一种更快的方法,在后面我们将提到。
交换寄存器的内容:
xchg reg32, reg32 xchg reg16, reg16 xchg reg8, reg8
例如,xchg ebx, ecx,则ebx与ecx的数值将被交换。由于系统提供了这个指令,因此,采用其他方法交换时,速度将会较慢,并需要占用更多的存储空间,编程时要避免这种情况,即,尽量利用系统提供的指令,因为多数情况下,这意味着更小、更快的代码,同时也杜绝了错误(如果说Intel的CPU在交换寄存器内容的时候也会出错,那么它就不用卖CPU了。而对于你来说,检查一行代码的正确性也显然比检查更多代码的正确性要容易)刚才的习题的程序用下面的代码将更有效: mov eax, 0a1234h mov bx, ax xchg ah, al;将0a1234h送入eax;将ax内容送入bx;交换ah, al的内容
递增或递减寄存器的值:
inc reg(8,16,32)dec reg(8,16,32)
这两个指令往往用于循环中对指针的操作。需要说明的是,某些时候我们有更好的方法来处理循环,例如使用loop指令,或rep前缀。这些将在后面的章节中介绍。
将寄存器的数值与另一寄存器,或立即数的值相加,并存回此寄存器:
add reg32, reg32 / imm(8,16,32)add reg16, reg16 / imm(8,16)add reg8, reg8 / imm(8)
例如,add eax, edx,将eax+edx的值存入eax。减法指令和加法类似,只是将add换成sub。
需要说明的是,与高级语言不同,汇编语言中,如果要计算两数之和(差、积、商,或一般地说,运算结果),那么必然有一个寄存器被用来保存结果。在PASCAL中,我们可以用nA := nB + nC来让nA保存nB+nC的结果,然而,汇编语言并不提供这种方法。如果你希望保持寄存器中的结果,需要用另外的指令。这也从另一个侧面反映了“寄存器”这个名字的意义。数据只是“寄存”在那里。如果你需要保存数据,那么需要将它放到内存或其他地方。
类似的指令还有and、or、xor(与,或,异或)等等。它们进行的是逻辑运算。
我们称add、mov、sub、and等称为为指令助记符(这么叫是因为它比机器语言容易记忆,而起作用就是方便人记忆,某些资料中也称为指令、操作码、opcode[operation code]等);后面的参数成为操作数,一个指令可以没有操作数,也可以有一两个操作数,通常有一个操作数的指令,这个操作数就是它的操作对象;而两个参数的指令,前一个操作数一般是保存操作结果的地方,而后一个是附加的参数。
我不打算在这份教程中用大量的篇幅介绍指令——很多人做得比我更好,而且指令本身并不是重点,如果你学会了如何组织语句,那么只要稍加学习就能轻易掌握其他指令。更多的指令可以参考Intel提供的资料。编写程序的时候,也可以参考一些在线参考手册。Tech!Help和HelpPC 2.10尽管已经很旧,但足以应付绝大多数需要。
聪明的读者也许已经发现,使用sub eax, eax,或者xor eax, eax,可以得到与mov eax, 0类似的效果。在高级语言中,你大概不会选择用a=a-a来给a赋值,因为测试会告诉你这么做更慢,简直就是在自找麻烦,然而在汇编语言中,你会得到相反的结论,多数情况下,以由快到慢的速度排列,这三条指令将是xor eax, eax、sub eax, eax和mov eax, 0。
为什么呢?处理器在执行指令时,需要经过几个不同的阶段:取指、译码、取数、执行。
我们反复强调,寄存器是CPU的一部分。从寄存器取数,其速度很显然要比从内存中取数快。那么,不难理解,xor eax, eax要比mov eax, 0更快一些。
那么,为什么a=a-a通常要比a=0慢一些呢?这和编译器的优化有一定关系。多数编译器会把a=a-a翻译成类似下面的代码(通常,高级语言通过ebp和偏移量来访问局部变量;程序中,x为a相对于本地堆的偏移量,在只包含一个32-bit整形变量的程序中,这个值通常是4):
mov eax, dword ptr [ebp-x] sub eax, dword ptr [ebp-x] mov dword ptr [ebp-x],eax
而把a=0翻译成
mov dword ptr [ebp-x], 0
上面的翻译只是示意性的,略去了很多必要的步骤,如保护寄存器内容、恢复等等。如果你对与编译程序的实现过程感兴趣,可以参考相应的书籍。多数编译器(特别是C/C++编译器,如Microsoft Visual C++)都提供了从源代码到宏汇编语言程序的附加编译输出选项。这种情况下,你可以很方便地了解编译程序执行的输出结果;如果编译程序没有提供这样的功能也没有关系,调试器会让你看到编译器的编译结果。
如果你明确地知道编译器编译出的结果不是最优的,那就可以着手用汇编语言来重写那段代码了。怎么确认是否应该用汇编语言重写呢?
使用汇编语言重写代码之前需要确认的几件事情
首先,这种优化最好有明显的效果。比如,一段循环中的计算,等等。一条语句的执行时间是很短的,现在新的CPU的指令周期都在0.000000001s以下,Intel甚至已经做出了4GHz主频(主频的倒数是时钟周期)的CPU,如果你的代码自始至终只执行一次,并且你只是减少了几个时钟周期的执行时间,那么改变将是无法让人察觉的;很多情况下,这种“优化”并不被提倡,尽管它确实减少了执行时间,但为此需要付出大量的时间、人力,多数情况下得不偿失(极端情况,比如你的设备内存价格非常昂贵的时候,这种优化也许会有意义)。其次,确认你已经使用了最好的算法,并且,你优化的程序的实现是正确的。汇编语言能够提供同样算法的最快实现,然而,它并不是万金油,更不是解决一切的灵丹妙药。用高级语言实现一种好的算法,不一定会比汇编语言实现一种差的算法更慢。不过需要注意的是,时间、空间复杂度最小的算法不一定就是解决某一特定问题的最佳算法。举例说,快速排序在完全逆序的情况下等价于冒泡排序,这时其他方法就比它快。同时,用汇编语言优化一个不正确的算法实现,将给调试带来很大的麻烦。
最后,确认你已经将高级语言编译器的性能发挥到极致。Microsoft的编译器在RELEASE模式和DEBUG模式会有差异相当大的输出,而对于GNU系列的编译器而言,不同级别的优化也会生成几乎完全不同的代码。此外,在编程时对于问题的严格定义,可以极大地帮助编译器的优化过程。如何优化高级语言代码,使其编译结果最优超出了本教程的范围,但如果你不能确认已经发挥了编译器的最大效能,用汇编语言往往是一种更为费力的方法。
还有一点非常重要,那就是你明白自己做的是什么。好的高级语言编译器有时会有一些让人难以理解的行为,比如,重新排列指令顺序,等等。如果你发现这种情况,那么优化的时候就应该小心——编译器很可能比你拥有更多的关于处理器的知识,例如,对于一个超标量处理器,编译器会对指令序列进行“封包”,使他们尽可能的并行执行;此外,宏汇编器有时会自动插入一些nop指令,其作用是将指令凑成整数字长(32-bit,对于16-bit处理器,是16-bit)。这些都是提高代码性能的必要措施,如果你不了解处理器,那么最好不要改动编译器生成的代码,因为这种情况下,盲目的修改往往不会得到预期的效果。
曾经在一份杂志上看到过有人用纯机器语言编写程序。不清楚到底这是不是编辑的失误,因为一个头脑正常的人恐怕不会这么做程序,即使它不长、也不复杂。首先,汇编器能够完成某些封包操作,即使不行,也可以用db伪指令来写指令;用汇编语言写程序可以防止很多错误的发生,同时,它还减轻了人的负担,很显然,“完全用机器语言写程序”是完全没有必要的,因为汇编语言可以做出完全一样的事情,并且你可以依赖它,因为计算机不会出错,而人总有出错的时候。此外,如前面所言,如果用高级语言实现程序的代价不大(例如,这段代码在程序的整个执行过程中只执行一遍,并且,这一遍的执行时间也小于一秒),那么,为什么不用高级语言实现呢?
一些比较狂热的编程爱好者可能不太喜欢我的这种观点。比方说,他们可能希望精益求精地优化每一字节的代码。但多数情况下我们有更重要的事情,例如,你的算法是最优的吗?你已经把程序在高级语言许可的范围内优化到尽头了吗?并不是所有的人都有资格这样说。汇编语言是这样一件东西,它足够的强大,能够控制计算机,完成它能够实现的任何功能;同时,因为它的强大,也会提高开发成本,并且,难于维护。因此,我个人的建议是,如果在软件开发中使用汇编语言,则应在软件接近完成的时候使用,这样可以减少很多不必要的投入。
第二章中,我介绍了x86系列处理器的基本寄存器。这些寄存器对于x86兼容处理器仍然是有效的,如果你偏爱AMD的CPU,那么使用这些寄存器的程序同样也可以正常运行。
不过现在说用汇编语言进行优化还为时尚早——不可能写程序,而只操作这些寄存器,因为这样只能完成非常简单的操作,既然是简单的操作,那可能就会让人觉得乏味,甚至找一台足够快的机器穷举它的所有结果(如果可以穷举的话),并直接写程序调用,因为这样通常会更快。但话说回来,看完接下来的两章——内存和堆栈操作,你就可以独立完成几乎所有的任务了,配合第五章中断、第六章子程序的知识,你将知道如何驾驭处理器,并让它为你工作。
数字计算机内部只支持二进制数,因为这样计算机 只需要表示两种(某些情况是3种,这一内容超过了 这份教程的范围,如果您感兴趣,可以参考数字逻 辑电路的相关书籍)状态.对于电路而言,这表现 为高、低电平,或者开、关,分别非常明显,因而 工作比较稳定;另一方面,由于只有两种状态,设 计起来也比较简单。这样,使用二进制意味着低成 本、稳定,多数情况下,这也意味着快速。
与十进制类似,我们可以用下面的式子来换算出一 个任意形如am-1„„a3a2a1a0 的m位r进制数对应的 数值n:
程序设计中常用十六进制和八进制数字代替二进制 数,其原因在于,16和8是2的整次方幂,这样,一 位十六或八进制数可以表示整数个二进制位。十六 进制中,使用字母A、B、C、D、E、F表示10-15,而十六进制或八进制数制表示的的数字比二进制数 更短一些。
EAX的内容为000A3412h.在前面的章节中,我们已经了解了寄存器的基本使用方法。而正如结尾提到的那样,仅仅使用寄存器做一点运算是没有什么太大意义的,毕竟它们不能保存太多的数据,因此,对编程人员而言,他肯定迫切地希望访问内存,以保存更多的数据。
我将分别介绍如何在保护模式和实模式操作内存,然而在此之前,我们先熟悉一下这两种模式中内存的结构。3.1 实模式
事实上,在实模式中,内存比保护模式中的结构更令人困惑。内存被分割成段,并且,操作内存时,需要指定段和偏移量。不过,理解这些概念是非常容易的事情。请看下面的图:
段寄存器这种格局是早期硬件电路限制留下的一个伤疤。地址总线在当时有20-bit。
然而20-bit的地址不能放到16-bit的寄存器里,这意味着有4-bit必须放到别的地方。因此,为了访问所有的内存,必须使用两个16-bit寄存器。
这一设计上的折衷方案导致了今天的段-偏移量格局。最初的设计中,其中一个寄存器只有4-bit有效,然而为了简化程序,两个寄存器都是16-bit有效,并在执行时求出加权和来标识20-bit地址。
偏移量是16-bit的,因此,一个段是64KB。下面的图可以帮助你理解20-bit地址是如何形成的:
段-偏移量标识的地址通常记做 段:偏移量 的形式。
由于这样的结构,一个内存有多个对应的地址。例如,0000:0010和0001:0000指的是同一内存地址。又如,0000:1234 = 0123:0004 = 0120:0034 = 0100:0234 0001:1234 = 0124:0004 = 0120:0044 = 0100:0244
作为负面影响之一,在段上加1相当于在偏移量上加16,而不是一个“全新”的段。反之,在偏移量上加16也和在段上加1等价。某些时候,据此认为段的“粒度”是16字节。
练习题
尝试一下将下面的地址转化为20bit的地址:
2EA8:D678 26CF:8D5F 453A:CFAD 2933:31A6 5924:DCCF 694E:175A 2B3C:D218 728F:6578 68E1:A7DC 57EC:AEEA
稍高一些的要求是,写一个程序将段为AX、偏移量为BX的地址转换为20bit的地址,并保存于EAX中。
[上面习题的答案]
我们现在可以写一个真正的程序了。
经典程序:Hello, world
;;;应该得到一个29字节的.com文件
.MODEL TINY.CODE
CR equ 13 LF equ 10 TERMINATOR equ '$'
ORG 100h
Main PROC mov dx,offset sMessage mov ah,9 int 21h mov ax,4c00h int 21h Main ENDP
sMessage: DB 'Hello, World!' DB CR,LF,TERMINATOR
END Main;.COM文件的内存模型是‘TINY’;代码段开始
;回车;换行
;DOS字符串结束符
;代码起始地址为CS:0100h
;令DS:DX指向Message;int 21h(DOS中断)功能9;终止程序并返回AL的错误代码
;程序结束的同时指定入口点为Main
那么,我们需要解释很多东西。
首先,作为汇编语言的抽象,C语言拥有“指针”这个数据类型。在汇编语言中,几乎所有对内存的操作都是由对给定地址的内存进行访问来完成的。这样,在汇编语言中,绝大多数操作都要和指针产生或多或少的联系。
这里我想强调的是,由于这一特性,汇编语言中同样会出现C程序中常见的缓冲区溢出问题。如果你正在设计一个与安全有关的系统,那么最好是仔细检查你用到的每一个串,例如,它们是否一定能够以你预期的方式结束,以及(如果使用的话)你的缓冲区是否能保证实际可能输入的数据不被写入到它以外的地方。作为一个汇编语言程序员,你有义务检查每一行代码的可用性。
程序中的equ伪指令是宏汇编特有的,它的意思接近于C或Pascal中的const(常量)。多数情况下,equ伪指令并不为符号分配空间。
此外,汇编程序执行一项操作是非常繁琐的,通常,在对与效率要求不高的地方,我们习惯使用系统提供的中断服务来完成任务。例如本例中的中断21h,它是DOS时代的中断服务,在Windows中,它也被认为是Windows API的一部分(这一点可以在Microsoft的文档中查到)。中断可以被理解为高级语言中的子程序,但又不完全一样——中断使用系统栈来保存当前的机器状态,可以由硬件发起,通过修改机器状态字来反馈信息,等等。
那么,最后一段通过DB存放的数据到底保存在哪里了呢?答案是紧挨着代码存放。在汇编语言中,DB和普通的指令的地位是相同的。如果你的汇编程序并不知道新的助记符(例如,新的处理器上的CPUID指令),而你很清楚,那么可以用DB 机器码的方式强行写下指令。这意味着,你可以超越汇编器的能力撰写汇编程序,然而,直接用机器码编程是几乎肯定是一件费力不讨好的事——汇编器厂商会经常更新它所支持的指令集以适应市场需要,而且,你可以期待你的汇编其能够产生正确的代码,因为机器查表是不会出错的。既然机器能够帮我们做将程序转换为代码这件事情,那么为什么不让它来做呢?
细心的读者不难发现,在程序中我们没有对DS进行赋值。那么,这是否意味着程序的结果将是不可预测的呢?答案是否定的。DOS(或Windows中的MS-DOS VM)在加载.com文件的时候,会对寄存器进行很多初始化。.com文件被限制为小于64KB,这样,它的代码段、数据段都被装入同样的数值(即,初始状态下DS=CS)。
也许会有人说,“嘿,这听起来不太好,一个64KB的程序能做得了什么呢?还有,你吹得天花乱坠的堆栈段在什么地方?”那么,我们来看看下面这个新的Hello world程序,它是一个EXE文件,在DOS实模式下运行。
;;;应该得到一个561 字节的EXE文件
.MODEL SMALL.STACK 200h
CR equ 13 LF equ 10 TERMINATOR equ '$'.DATA
Message DB 'Hello, World!' DB CR,LF,TERMINATOR.CODE
Main PROC mov ax, DGROUP mov ds, ax
mov dx, offset Message mov ah, 9 int 21h
mov ax, 4c00h int 21h Main ENDP END main
;采用“SMALL”内存模型;堆栈段
;回车
;换行
;DOS字符串结束符
;定义数据段
;定义显示串
;定义代码段
;将数据段;加载到DS寄存器
;设置DX;显示
;终止程序
561字节?实现相同功能的程序大了这么多!为什么呢?我们看到,程序拥有了完整的堆栈段、数据段、代码段,其中堆栈段足足占掉了512字节,其余的基本上没什么变化。
分成多个段有什么好处呢?首先,它让程序显得更加清晰——你肯定更愿意看一个结构清楚的程序,代码中hard-coded的字符串、数据让人觉得费解。比如,mov dx, 0152h肯定不如mov dx, offset Message来的亲切。此外,通过分段你可以使用更多的内存,比如,代码段腾出的空间可以做更多的事情。exe文件另一个吸引人的地方是它能够实现“重定位”。现在你不需要指定程序入口点的地址了,因为系统会找到你的程序入口点,而不是死板的100h。
程序中的符号也会在系统加载的时候重新赋予新的地址。exe程序能够保证你的设计容易地被实现,不需要考虑太多的细节。
当然,我们的主要目的是将汇编语言作为高级语言的一个有用的补充。如我在开始提到的那样,真正完全用汇编语言实现的程序不一定就好,因为它不便于维护,而且,由于结构的原因,你也不太容易确保它是正确的;汇编语言是一种非结构化的语言,调试一个精心设计的汇编语言程序,即使对于一个老手来说也不啻是一场恶梦,因为你很可能掉到别人预设的“陷阱”中——这些技巧确实提高了代码性能,然而你很可能不理解它,于是你把它改掉,接着就发现程序彻底败掉了。使用汇编语言加强高级语言程序时,你要做的通常只是使用汇编指令,而不必搭建完整的汇编程序。绝大多数(也是目前我遇到的全部)C/C++编译器都支持内嵌汇编,即在程序中使用汇编语言,而不必撰写单独的汇编语言程序——这可以节省你的不少精力,因为前面讲述的那些伪指令,如equ等,都可以用你熟悉的高级语言方式来编写,编译器会把它转换为适当的形式。
需要说明的是,在高级语言中一定要注意编译结果。编译器会对你的汇编程序做一些修改,这不一定符合你的要求(附带说一句,有时编译器会很聪明地调整指令顺序来提高性能,这种情况下最好测试一下哪种写法的效果更好),此时需要做一些更深入的修改,或者用db来强制编码。
3.2 保护模式
实模式的东西说得太多了,尽管我已经删掉了许多东西,并把一些原则性的问题拿到了这一节讨论。这样做不是没有理由的——保护模式才是现在的程序(除了操作系统的底层启动代码)最常用的CPU模式。保护模式提供了很多令人耳目一新的功能,包括内存保护(这是保护模式这个名字的来源)、进程支持、更大的内存支持,等等。
对于一个编程人员来说,能“偷懒”是一件令人愉快的事情。这里“偷懒”是说把“应该”由系统做的事情做的事情全都交给系统。为什么呢?这出自一个基本思想——人总有犯错误的时候,然而规则不会,正确地了解规则之后,你可以期待它像你所了解的那样执行。对于C程序来说,你自己用C语言写的实现相同功能的函数通常没有系统提供的函数性能好(除非你用了比函数库好很多的算法),因为系统的函数往往使用了更好的优化,甚至可能不是用C语言直接编写的。
当然,“偷懒”的意思是说,把那些应该让机器做的事情交给计算机来做,因为它做得更好。我们应该把精力集中到设计算法,而不是编写源代码本身上,因为编译器几乎只能做等价优化,而实现相同功能,但使用更好算法的程序实现,则几乎只能由人自己完成。
举个例子,这样一个函数:
int fun(){ int a=0;register int i;for(i=0;i<1000;i++)a+=i;return a;}
在某种编译模式[DEBUG]下被编译为
push ebp mov ebp,esp sub esp,48h push ebx push esi push edi lea edi,[ebp-48h] mov ecx,12h mov eax,0CCCCCCCCh rep stos dword ptr [edi] mov dword ptr [ebp-4],0 mov dword ptr [ebp-8],0 jmp fun+31h mov eax,dword ptr [ebp-8] add eax,1 mov dword ptr [ebp-8],eax cmp dword ptr [ebp-8],3E8h jge fun+45h mov ecx,dword ptr [ebp-4] add ecx,dword ptr [ebp-8] mov dword ptr [ebp-4],ecx jmp fun+28h mov eax,dword ptr [ebp-4] pop edi pop esi pop ebx mov esp,ebp pop ebp ret;子程序入口
;保护现场
;初始化变量-调试版本特有。
;本质是在堆中挖一块地儿,存CCCCCCCC。;用串操作进行,这将发挥Intel处理器优势;‘a=0’;‘i=0’
;走着;i++
;i<1000?
;a+=i;
;return a;
;恢复现场
;返回
而在另一种模式[RELEASE/MINSIZE]下却被编译为
xor eax,eax xor ecx,ecx add eax,ecx inc ecx cmp ecx,3E8h jl fun+4 ret;a=0;;i=0;;a+=i;;i++;;i<1000?;是->继续继续;return a
如果让我来写,多半会写成
mov eax, 079f2ch ret;return 499500
为什么这样写呢?我们看到,i是一个外界不能影响、也无法获知的内部状态量。作为这段程序来说,对它的计算对于结果并没有直接的影响——它的存在不过是方便算法描述而已。并且我们看到的,这段程序实际上无论执行多少次,其结果都不会发生变化,因此,直接返回计算结果就可以了,计算是多余的(如果说一定要算,那么应该是编译器在编译过程中完成它)。
更进一步,我们甚至希望编译器能够直接把这个函数变成一个符号常量,这样连操作堆栈的过程也省掉了。
第三种结果属于“等效”代码,而不是“等价”代码。作为用户,很多时候是希望编译器这样做的,然而由于目前的技术尚不成熟,有时这种做法会造成一些问题(gcc和g++的顶级优化可以造成编译出的FreeBSD内核行为异常,这是我在FreeBSD上遇到的唯一一次软件原因的kernel panic),因此,并不是所有的编译器都这样做(另一方面的原因是,如果编译器在这方面做的太过火,例如自动求解全部“固定”问题,那么如果你的程序是解决固定的问题“很大”,如求解迷宫,那么在编译过程中你就会找锤子来砸计算机了)。然而,作为编译器制造商,为了提高自己的产品的竞争力,往往会使用第三种代码来做函数库。正如前面所提到的那样,这种优化往往不是编译器本身的作用,尽管现代编译程序拥有编译执行、循环代码外提、无用代码去除等诸多优化功能,但它都不能保证程序最优。最后一种代码恐怕很少有编译器能够做到,不信你可以用自己常用的编译器加上各种优化选项试试:)
发现什么了吗?三种代码中,对于内存的访问一个比一个少。这样做的理由是,尽可能地利用寄存器并减少对内存的访问,可以提高代码性能。在某些情况下,使代码既小又快是可能的。
书归正传,我们来说说保护模式的内存模型。保护模式的内存和实模式有很多共同之处。
毫无疑问,以'protected mode'(保护模式), 'global descriptor table'(全局描述符表), 'local descriptor table'(本地描述符表)和'selector'(选择器)搜索,你会得到完整介绍它们的大量信息。
保护模式与实模式的内存类似,然而,它们之间最大的区别就是保护模式的内存是“线性”的。
新的计算机上,32-bit的寄存器已经不是什么新鲜事(如果你哪天听说你的CPU的寄存器不是32-bit的,那么它——简直可以肯定地说——的字长要比32-bit还要多。新的个人机上已经开始逐步采用64-bit的CPU了),换言之,实际上段/偏移量这一格局已经不再需要了。尽管如此,在继续看保护模式内存结构时,仍请记住段/偏移量的概念。不妨把段寄存器看作对于保护模式中的选择器的一个模拟。选择器是全局描述符表(Global Descriptor Table, GDT)或本地描述符表(Local Descriptor Table, LDT)的一个指针。
如图所示,GDT和LDT的每一个项目都描述一块内存。例如,一个项目中包含了某块被描述的内存的物理的基地址、长度,以及其他一些相关信息。
保护模式是一个非常重要的概念,同时也是目前撰写应用程序时,最常用的CPU模式(运行在新的计算机上的操作系统很少有在实模式下运行的)。
为什么叫保护模式呢?它“保护”了什么?答案是进程的内存。保护模式的主要目的在于允许多个进程同时运行,并保护它们的内存不受其他进程的侵犯。这有点类似于C++中的机制,然而它的强制力要大得多。如果你的进程在保护模式下以不恰当的方式访问了内存(例如,写了“只读”内存,或读了不可读的内存,等等),那么CPU就会产生一个异常。这个异常将交给操作系统处理,而这种处理,假如你的程序没有特别说明操作系统该如何处理的话,一般就是杀掉做错了事情的进程。
我像这样的对话框大家一定非常熟悉(临时写了一个程序故意造成的错误):
好的,只是一个程序崩溃了,而操作系统的其他进程照常运行(同样的程序在DOS中几乎是板上钉钉的死机,因为NULL指针的位置恰好是中断向量表),你甚至还可以调试它。
保护模式还有其他很多好处,在此就不一一赘述了。实模式和保护模式之间的切换问题我打算放在后面的“高级技巧”一章来讲,因为多数程序并不涉及这个。
了解了内存的格局,我们就可以进入下一节——操作内存了。
3.3 操作内存
前两节中,我们介绍了实模式和保护模式中使用的不同的内存格局。现在开始解释如何使用这些知识。
回忆一下前面我们说过的,寄存器可以用作内存指针。现在,是他们发挥作用的时候了。
可以将内存想象为一个顺序的字节流。使用指针,可以任意地操作(读写)内存。
现在我们需要一些其他的指令格式来描述对于内存的操作。操作内存时,首先需要的就是它的地址。
让我们来看看下面的代码:
mov ax,[0]
方括号表示,里面的表达式指定的不是立即数,而是偏移量。在实模式中,DS:0中的那个字(16-bit长)将被装入AX。
然而0是一个常数,如果需要在运行的时候加以改变,就需要一些特殊的技巧,比如程序自修改。汇编支持这个特性,然而我个人并不推荐这种方法——自修改大大降低程序的可读性,并且还降低稳定性,性能还不一定好。我们需要另外的技术。
mov bx,0 mov ax,[bx]
看起来舒服了一些,不是吗?BX寄存器的内容可以随时更改,而不需要用冗长的代码去修改自身,更不用担心由此带来的不稳定问题。
同样的,mov指令也可以把数据保存到内存中:
mov [0],ax
在存储器与寄存器之间交换数据应该足够清楚了。
有些时候我们会需要操作符来描述内存数据的宽度: 操作符 意义
byte ptr 一个字节(8-bit, 1 byte)word ptr 一个字(16-bit)dword ptr 一个双字(32-bit)
例如,在DS:100h处保存1234h,以字存放:
mov word ptr [100h],01234h
于是我们将mov指令扩展为:
mov reg(8,16,32), mem(8,16,32)mov mem(8,16,32), reg(8,16,32)mov mem(8,16,32), imm(8,16,32)
需要说明的是,加减同样也可以在[]中使用,例如:
mov ax,[bx+10] mov ax,[bx+si] mov ax,es:[di+bp]
等等。我们看到,对于内存的操作,即使使用MOV指令,也有许多种可能的方式。下一节中,我们将介绍如何操作串。
感谢 网友 水杉 指出此答案中的一处错误。感谢 Heallven 指出.COM程序实例编译失败的问题
我们前面已经提到,内存可以和寄存器交换数据,也可以被赋予立即数。问题是,如果我们需要把内存的某部分内容复制到另一个地址,又怎么做呢?
设想将DS:SI处的连续512字节内容复制到ES:DI(先不考虑可能的重叠)。也许会有人写出这样的代码:
NextByte: mov cx,512 mov al,ds:[si] mov es:[di],al inc si inc di loop NextByte;循环次数
我不喜欢上面的代码。它的确能达到作用,但是,效率不好。如果你是在做优化,那么写出这样的代码意味着赔了夫人又折兵。
Intel的CPU的强项是串操作。所谓串操作就是由CPU去完成某一数量的、重复的内存操作。需要说明的是,我们常用的KMP算法(用于匹配字符串中的模式)的改进——Boyer算法,由于没有利用串操作,因此在Intel的CPU上的效率并非最优。好的编译器往往可以利用Intel CPU的这一特性优化代码,然而,并非所有的时候它都能产生最好的代码。某些指令可以加上REP前缀(repeat, 反复之意),这些指令通常被叫做串操作指令。举例来说,STOSD指令将EAX的内容保存到ES:DI,同时在DI上加或减四。类似的,STOSB和STOSW分别作1字节或1字的上述操作,在DI上加或减的数是1或2。
计算机语言通常是不允许二义性的。为什么我要说“加或减”呢?没错,孤立地看STOS?指令,并不能知道到底是加还是减,因为这取决于“方向”标志(DF, Direction Flag)。如果DF被复位,则加;反之则减。
置位、复位的指令分别是STD和CLD。
当然,REP只是几种可用前缀之一。常用的还包括REPNE,这个前缀通常被用来比较两个串,或搜索某个特定字符(字、双字)。REPZ、REPE、REPNZ也是非常常用的指令前缀,分别代表ZF(Zero Flag)在不同状态时重复执行。下面说三个可以复制数据的指令: 助记符 意义
movsb 将DS:SI的一字节复制到ES:DI,之后SI++、DI++ movsw 将DS:SI的一字节复制到ES:DI,之后SI+=
2、DI+=2 movsd 将DS:SI的一字节复制到ES:DI,之后SI+=
4、DI+=4 于是上面的程序改写为 cld mov cx, 128 rep movsd;复位DF;512/4 = 128,共128个双字;行动!
第一句cld很多时候是多余的,因为实际写程序时,很少会出现置DF的情况。不过在正式决定删掉它之前,建议你仔细地调试自己的程序,并确认每一个能够走到这里的路径中都不会将DF置位。
错误(非预期的)的DF是危险的。它很可能断送掉你的程序,因为这直接造成缓冲区溢出问题。
什么是缓冲区溢出呢?缓冲区溢出分为两类,一类是写入缓冲区以外的内容,一类是读取缓冲区以外的内容。后一种往往更隐蔽,但随便哪一个都有可能断送掉你的程序。
缓冲区溢出对于一个网络服务来说很可能更加危险。怀有恶意的用户能够利用它执行自己希望的指令。服务通常拥有更高的特权,而这很可能会造成特权提升;即使不能提升攻击者拥有的特权,他也可以利用这种问题使服务崩溃,从而形成一次成功的DoS(拒绝服务)攻击。每年CERT的安全公告中,都有6成左右的问题是由于缓冲区溢出造成的。
在使用汇编语言,或C语言编写程序时,很容易在无意中引入缓冲区溢出。然而并不是所有的语言都会引入缓冲区溢出问题,Java和C#,由于没有指针,并且缓冲区采取动态分配的方式,有效地消除了造成缓冲区溢出的土壤。
汇编语言中,由于REP*前缀都用CX作为计数器,因此情况会好一些(当然,有时也会更糟糕,因为由于CX的限制,很可能使原本可能改变程序行为的缓冲区溢出的范围缩小,从而更为隐蔽)。避免缓冲区溢出的一个主要方法就是仔细检查,这包括两方面:设置合理的缓冲区大小,和根据大小编写程序。除此之外,非常重要的一点就是,在汇编语言这个级别写程序,你肯定希望去掉所有的无用指令,然而再去掉之前,一定要进行严格的测试;更进一步,如果能加上注释,并通过善用宏来做调试模式检查,往往能够达到更好的效果。
正如3.2节提到到的那样,保护模式中,你可以使用32位的线性地址,这意味着直接访问4GB的内存。由于这个原因,选择器不用像实模式中段寄存器那样频繁地修改。顺便提一句,这份教程中所说的保护模式指的是386以上的保护模式,或者,Microsoft通常称为“增强模式”的那种。
在为选择器装入数值的时候一定要非常小心。错误的数值往往会导致无效页面错误(在Windows中经常出现:)。同时,也不要忘记你的地址是32位的,这也是保护模式的主要优势之一。
现在假设存在一个描述符描述从物理的0:0开始的全部内存,并已经加载进DS(数据选择器),则我们可以通过下面的程序来操作VGA的VRAM: mov edi,0a0000h mov byte ptr [edi],0fh;VGA显存的偏移量;将第一字节改为0fh 很明显,这比实模式下的程序 mov ax,0a000h mov ds,ax mov di,0 mov [di],0fh;AX-> VGA段地址;将AX值载入DS;DI清零
;修改第一字节 看上去要舒服一些。
到目前为止,您已经了解了基本的寄存器以及内存的操作知识。事实上,您现在已经可以写出很多的底层数据处理程序了。
下面我来说说堆栈。堆栈实在不是一个让人陌生的数据结构,它是一个先进后出(FILO)的线性表,能够帮助你完成很多很好的工作。
先进后出(FILO)是这样一个概念:最后放进表中 的数据在取出时最先出来。先进后出(FILO)和先 进先出(FIFO, 和先进后出的规则相反),以及随 机存取是最主要的三种存储器访问方式。
对于堆栈而言,最后放入的数据在取出时最先出
现。对于子程序调用,特别是递归调用来说,这 是一个非常有用的特性。
一个铁杆的汇编语言程序员有时会发现系统提供的寄存器不够。很显然,你可以使用普通的内存操作来完成这个工作,就像C/C++中所做的那样。
没错,没错,可是,如果数据段(数据选择器)以及偏移量发生变化怎么办?更进一步,如果希望保存某些在这种操作中可能受到影响的寄存器的时候怎么办?确实,你可以把他们也存到自己的那片内存中,自己实现堆栈。
太麻烦了„„
既然系统提供了堆栈,并且性能比自己写一份更好,那么为什么不直接加以利用呢 系统堆栈不仅仅是一段内存。由于CPU对它实施管理,因此你不需要考虑堆栈指针的修正问题。可以把寄存器内容,甚至一个立即数直接放到堆栈里,并在需要的时候将其取出。同时,系统并不要求取出的数据仍然回到原来的位置。
除了显式地操作堆栈(使用PUSH和POP指令)之外,很多指令也需要使用堆栈,如INT、CALL、LEAVE、RET、RETF、IRET等等。配对使用上述指令并不会造成什么问题,然而,如果你打算使用LEAVE、RET、RETF、IRET这样的指令实现跳转(比JMP更为麻烦,然而有时,例如在加密软件中,或者需要修改调用者状态时,这是必要的)的话,那么我的建议是,先搞清楚它们做的到底是什么,并且,精确地了解自己要做什么。正如前面所说的,有两个显式地操作堆栈的指令:
助记符 功能
PUSH 将操作数存入堆栈,同时修正堆栈指针
POP 将栈顶内容取出并存到目的操作数中,同时修正堆栈指针 我们现在来看看堆栈的操作。执行之前 执行代码 mov ax,1234h mov bx,10 push ax push bx 之后,堆栈的状态为 之后,再执行 pop dx pop cx 堆栈的状态成为
当然,dx、cx中的内容将分别是000ah和1234h。
注意,最后这张图中,我没有抹去1234h和000ah,因为POP指令并不从内存中抹去数值。不过尽管如此,我个人仍然非常反对继续使用这两个数(你可以通过修改SP来再次POP它们),然而这很容易导致错误。
一定要保证堆栈段有足够的空间来执行中断,以及其他一些隐式的堆栈操作。仅仅统计PUSH的数量并据此计算堆栈所需的大小很可能造成问题。CALL指令将返回地址放到堆栈中。绝大多数C/C++编译器提供了“堆栈检查”这个编译选项,其作用在于保证C程序段中没有忘记对堆栈中多余的数据进行清理,从而保证返回地址有效。本章小结
本章中介绍了内存的操作的一些入门知识。限于篇幅,我不打算展开细讲指令,如cmps*,lods*,stos*,等等。这些指令的用法和前面介绍的movs*基本一样,只是有不同的作用而已。
4.0 利用子程序与中断
已经掌握了汇编语言?没错,你现在已经可以去破译别人代码中的秘密。然而,我们还有一件重要的东西没有提到,那就是自程序和中断。这两件东西是如此的重要,以至于你的程序几乎不可能离开它们。
4.1 子程序
在高级语言中我们经常要用到子程序。高级语言中,子程序是如此的神奇,我们能够定义和主程序,或其他子程序一样的变量名,而访问不同的变量,并且,还不和程序的其他部分相冲突。
然而遗憾的是,这种“优势”在汇编语言中是不存在的。
汇编语言并不注重如何减轻程序员的负担;相反,汇编语言依赖程序员的良好设计,以期发挥CPU的最佳性能。汇编语言不是结构化的语言,因此,它不提供直接的“局部变量”。如果需要“局部变量”,只能通过堆或栈自行实现。
从这个意义上讲,汇编语言的子程序更像GWBASIC中的GOSUB调用的那些“子程序”。所有的“变量”(本质上,属于进程的内存和寄存器)为整个程序所共享,高级语言编译器所做的,将局部变量放到堆或栈中的操作,只能自行实现。
参数的传递是靠寄存器和堆栈来完成的。高级语言中,子程序(函数、过程,或类似概念的东西)依赖于堆和栈来传递。
让我们来简单地分析一下一般高级语言的子程序的执行过程。无论C、C++、BASIC、Pascal,这一部分基本都是一致的。
调用者将子程序执行完成时应返回的地址、参数压入堆栈
子程序使用BP指针+偏移量对栈中的参数寻址,并取出、完成操作
子程序使用RET或RETF指令返回。此时,CPU将IP置为堆栈中保存的地址,并继续予以执行
毋庸置疑,堆栈在整个过程中发挥着非常重要的作用。不过,本质上对子程序最重要的还是返回地址。如果子程序不知道这个地址,那么系统将会崩溃。
调用子程序的指令是CALL,对应的返回指令是RET。此外,还有一组指令,即ENTER和LEAVE,它们可以帮助进行堆栈的维护。
CALL指令的参数是被调用子程序的地址。使用宏汇编的时候,这通常是一个标号。CALL和RET,以及ENTER和LEAVE配对,可以实现对于堆栈的自动操作,而不需要程序员进行PUSH/POP,以及跳转的操作,从而提高了效率。
作为一个编译器的实现实例,我用Visual C++编译了一段C++程序代码,这段汇编代码是使用特定的编译选项得到的结果,正常的RELEASE代码会比它精简得多。包含源代码的部分反汇编结果如下(取自Visual C++调试器的运行结果,我删除了10条int 3指令,并加上了一些注释,除此之外,没有做任何修改): 1: int myTransform(int nInput){ 00401000 push ebp;保护现场原先的EBP指针 00401001 mov ebp,esp 2: return(nInput*2 + 3)% 7;00401003 mov eax,dword ptr [nInput];取参数
00401006 lea eax,[eax+eax+3];LEA比ADD加法更快
0040100A cdq;DWORD->QWORD(扩展字长)0040100B mov ecx,7;除数 00401010 idiv eax,ecx;除
00401012 mov eax,edx;商->eax(eax中保存返回值)3: } 00401014 pop ebp;恢复现场的ebp指针
00401015 ret;返回
;此处删除10条int 3指令,它们是方便调试用的,并不影响程序行为。4: 5: int main(int argc, char* argv[])6: { 00401020 push ebp;保护现场原先的EBP指针 00401021 mov ebp,esp 00401023 sub esp,10h;为取argc, argv修正堆栈指针。7: int a[3];8: for(register int i=0;i<3;i++){ 00401026 mov dword ptr [i],0;0->i 0040102D jmp main+18h(00401038);判断循环条件 0040102F mov eax,dword ptr [i];i->eax 00401032 add eax,1;eax ++ 00401035 mov dword ptr [i],eax;eax->i 00401038 cmp dword ptr [i],3;循环条件: i与3比较
0040103C jge main+33h(00401053);如果不符合条件,则应结束循环 9: a[i] = myTransform(i);0040103E mov ecx,dword ptr [i];i->ecx 00401041 push ecx;ecx(i)-> 堆栈 00401042 call myTransform(00401000);调用myTransform 00401047 add esp,4;esp+=4: 在堆中的新单元;准备存放返回结果 0040104A mov edx,dword ptr [i];i->edx 0040104D mov dword ptr a[edx*4],eax;将eax(myTransform返回值);放回a[i] 10: } 00401051 jmp main+0Fh(0040102f);计算i++,并继续循环 11: return 0;00401053 xor eax,eax;返回值应该是0 12: } 00401055 mov esp,ebp;恢复堆栈指针 00401057 pop ebp;恢复BP 00401058 ret;返回调用者(C++运行环境)上述代码确实做了一些无用功,当然,这是因为编译器没有对这段代码进行优化。让我们来关注一下这段代码中,是如何调用子程序的。不考虑myTransform这个函数实际进行的数值运算,最让我感兴趣的是这一行代码:
00401003 mov eax,dword ptr [nInput];取参数 这里nInput是一个简简单单的变量符号吗?Visual C++的调试器显然不能告诉我们答案——它的设计目标是为了方便程序调试,而不是向你揭示编译器生成的代码的实际构造。我用另外一个反汇编器得到的结果是:
00401003 mov eax,dword ptr [ebp+8];取参数
这和我们在main()中看到的压栈顺序是完全吻合的(注意,程序运行到这个地方的时候,EBP=ESP)。main()最终将i的值通过堆栈传递给了myTransform()。
剖析上面的程序只是说明了我前面所提到的子程序的一部分用法。对于汇编语言来说,完全没有必要拘泥于结构化程序设计的框架(在今天,使用汇编的主要目的在于提高执行效率,而不是方便程序的维护和调试,因为汇编不可能在这一点上做得比C++更好)。考虑下面的程序:
void myTransform1(int nCount, char* sBytes){ for(register int i=1;i void myTransform2(int nCount, char* sBytes){ for(register int i=0;i 很容易看出,这两个函数包含了公共部分,即 for(i=0;i 目前,还没有编译器能够做到将这两部分合并。依然沿用刚才的编译选项,得到的反汇编结果是(同样地删除了int 3): 1: void myTransform1(int nCount, char* sBytes){ 00401000 push ebp 00401001 mov ebp,esp 00401003 push ecx 2: for(register int i=1;i 非常明显地,0040103d-0040106e和00401074-004010a5这两段代码存在少量的差别,但很显然只是对寄存器的偏好不同(编译器在优化时,这可能会减少堆栈操作,从而提高性能,但在这里只是使用了不同的寄存器而已) 对代码进行合并的好处是非常明显的。新的操作系统往往使用页式内存管理。当内存不足时,程序往往会频繁引发页面失效(Page faults),从而引发操作系统从磁盘中读取一些东西。磁盘的速度赶不上内存的速度,因此,这一行为将导致性能的下降。通过合并一部分代码,可以减少程序的大小,这意味着减少页面失效的可能性,从而软件的性能会有所提高 当然,这样做的代价也不算低——你的程序将变得难懂,并且难于维护。因此,再进行这样的优化之前,一定要注意: 优化前的程序必须是正确的。如果你不能确保这一点,那么这种优化必将给你的调试带来极大的麻烦。 优化前的程序实现最好是最优的。仔细检查你的设计,看看是否已经使用了最合适(即,对于此程序而言最优)的算法,并且已经在高级语言许可的范围内进行了最好的实现。优化最好能够非常有效地减少程序大小(例如,如果只是减少十几个字节,恐怕就没什么必要了),或非常有效地提高程序的运行速度(如果代码只是运行一次,并且只是节省几个时钟周期,那么在多数场合都没有意义)。否则,这种优化将得不偿失。4.2 中断 中断应该说是一个陈旧的话题。在新的系统中,它的作用正在逐渐被削弱,而变成操作系统专用的东西。并不是所有的计算机系统都提供中断,然而在x86系统中,它的作用是不可替代的。 中断实际上是一类特殊的子程序。它通常由系统调用,以响应突发事件。 例如,进行磁盘操作时,为了提高性能,可能会使用DMA方式进行操作。CPU向DMA控制器发出指令,要求外设和内存直接交换数据,而不通过CPU。然后,CPU转去进行起他的操作;当数据交换结束时,CPU可能需要进行一些后续操作,但此时它如何才能知道DMA已经完成了操作呢? 很显然不是依靠CPU去查询状态——这样DMA的优势就不明显了。为了尽可能地利用DMA的优势,在完成DMA操作的时候,DMA会告诉CPU“这事儿我办完了”,然后CPU会根据需要进行处理。 这种处理可能很复杂,需要若干条指令来完成。子程序是一个不错的主意,不过,CALL指令需要指定地址,让外设强迫CPU执行一条CALL指令也违背了CPU作为核心控制单元的设计初衷。考虑到这些,在x86系统中引入了中断向量的概念。 中断向量表是保存在系统数据区(实模式下,是0:0开始的一段区域)的一组指针。这组指针指向每一个中断服务程序的地址。整个中断向量表的结构是一个线性表。 每一个中断服务有自己的唯一的编号,我们通常称之为中断号。每一个中断号对应中断向量表中的一项,也就是一个中断向量。外设向CPU发出中断请求,而CPU自己将根据当前的程序状态决定是否中断当前程序并调用相应的中断服务。不难根据造成中断的原因将中断分为两类:硬件中断和软件中断。硬件中断有很多分类方法,如根据是否可以屏蔽分类、根据优先级高低分类,等等。考虑到这些分类并不一定科学,并且对于我们介绍中断的使用没有太大的帮助,因此我并不打算太详细地介绍它(在本教程的高级篇中,关于加密解密的部分会提到某些硬件中断的利用,但那是后话)。 在设计操作系统时,中断向量的概念曾经带来过很大的便利。操作系统随时可能升级,这样,通过CALL来调用操作系统的服务(如果说每个程序都包含对于文件系统、进程表这些应该由操作系统管理的数据的直接操作的话,不仅会造成程序的臃肿,而且不利于系统的安全)就显得不太合适了——没人能知道,以后的操作系统的服务程序入口点会不会是那儿。软件中断的存在为解决这个问题提供了方便。对于一台包含了BIOS的计算机来说,启动的时候系统已经提供了一部分服务,例如显示服务。无论你的BIOS、显示卡有多么的“个性”,只要他们和IBM PC兼容,那么此时你肯定可以通过调用16(10h)号中断来使用显示服务。调用中断的指令是 int 中断号 这将引发CPU去调用一个中断。CPU将保存当前的程序状态字,清除Trap和Interrupt两个标志,将即将执行的指令地址压入堆栈,并调用中断服务(根据中断向量表)。 编写中断服务程序不是一件容易的事情。很多时候,中断服务程序必须写成可重入代码(或纯代码,pure code)。所谓可重入代码是指,程序的运行过程中可以被打断,并由开始处再次执行,并且在合理的范围内(多次重入,而不造成堆栈溢出等其他问题),程序可以在被打断处继续执行,并且执行结果不受影响。 由于在多线程环境中等其他一些地方进行程序设计时也需要考虑这个因素,因此这里着重讲一下可重入代码的编写。 可重入代码最主要的要求就是,程序不应使用某个指定的内存地址的内存(对于高级语言来说,这通常是全局变量,或对象的成员)。如果可能的话,应使用寄存器,或其他方式来解决。如果不能做到这一点,则必须在开始、结束的时候分别禁止和启用中断,并且,运行时间不能太长。 下面用C语言分别举一个可重入函数,和两个非可重入函数的例子(注.这些例子应该是在某本多线程或操作系统的书上看到的,遗憾的是我想不起来是哪本书了,在这里先感谢那位作者提供的范例): 可重入函数: void strcpy(char* lpszDest, char* lpszSrc){ while(*dest++=*src++);*dest=0;} 非可重入函数 char cTemp;// 全局变量 void SwapChar(char* lpcX, char* lpcY){ cTemp = *lpcX;*lpcX = *lpcY;lpcY = cTemp;// 引用了全局变量,在分享内存的多个线程中可能造成问题 } 非可重入函数 void SwapChar2(char* lpcX, char* lpcY){ static char cTemp;// 静态变量 cTemp = *lpcX;*lpcX = *lpcY;lpcY = cTemp;// 引用了静态变量,在分享内存的多个线程中可能造成问题 } 中断利用的是系统的栈。栈操作是可重入的(因为栈可以保证“先进后出”),因此,我们并不需要考虑栈操作的重入问题。使用宏汇编器写出可重入的汇编代码需要注意一些问题。简单地说,干脆不要用标号作为变量是一个不错的主意。 使用高级语言编写可重入程序相对来讲轻松一些。把持住不访问那些全局(或当前对象的)变量,不使用静态局部变量,坚持只适用局部变量,写出的程序就将是可重入的。书归正传,调用软件中断时,通常都是通过寄存器传进、传出参数。这意味着你的int指令周围也许会存在一些“帮手”,比如下面的代码: mov ax, 4c00h int 21h 就是通过调用DOS中断服务返回父进程,并带回错误反馈码0。其中,ax中的数据4c00h就是传递给DOS中断服务的参数。 到这里,x86汇编语言的基础部分就基本上讲完了,《简明x86汇编语言教程》的初级篇——汇编语言基础也就到此告一段落。当然,目前为止,我只是蜻蜓点水一般提到了一些学习x86汇编语言中我认为需要注意的重要概念。许多东西,包括全部汇编语句的时序特性(指令执行周期数,以及指令周期中各个阶段的节拍数等)、功能、参数等等,限于个人水平和篇幅我都没有作详细介绍。如果您对这些内容感兴趣,请参考Intel和AMD两大CPU供应商网站上提供的开发人员参考。 在以后的简明x86汇编语言教程中级篇和高级篇中,我将着重介绍汇编语言的调试技术、优化,以及一些具体的应用技巧,包括反跟踪、反反跟踪、加密解密、病毒与反病毒等等。 5.0 编译优化概述 优化是一件非常重要的事情。作为一个程序设计者,你肯定希望自己的程序既小又快。DOS时代的许多书中都提到,“某某编译器能够生成非常紧凑的代码”,换言之,编译器会为你把代码尽可能地缩减,如果你能够正确地使用它提供的功能的话。目前,Intel x86体系上流行的C/C++编译器,包括Intel C/C++ Compiler, GNU C/C++ Compiler,以及最新的Microsoft和Borland编译器,都能够提供非常紧凑的代码。正确地使用这些编译器,则可以得到性能足够好的代码。 但是,机器目前还不能像人那样做富于创造性的事情。因而,有些时候我们可能会不得不手工来做一些事情。 使用汇编语言优化代码是一件困难,而且技巧性很强的工作。很多编译器能够生成为处理器进行过特殊优化处理的代码,一旦进行修改,这些特殊优化可能就会被破坏而失效。因此,在你决定使用自己的汇编代码之前,一定要测试一下,到底是编译器生成的那段代码更好,还是你的更好。 本章中将讨论一些编译器在某些时候会做的事情(从某种意义上说,本章内容更像是计算机专业的基础课中《编译程序设计原理》、《计算机组成原理》、《计算机体系结构》课程中的相关内容)。本章的许多内容和汇编语言程序设计本身关系并不是很紧密,它们多数是在为使用汇编语言进行优化做准备。编译器确实做这些优化,但它并不总是这么做;此外,就编译器的设计本质来说,它确实没有义务这么做——编译器做的是等义变换,而不是等效变换。考虑下面的代码: // 程序段1 int gaussianSum(){ int i, j=0;for(i=0;i<100;i++)j+=i; return j;} 好的,首先,绝大多数编译器恐怕不会自作主张地把它“篡改”为 // 程序段1(改进1)int gaussianSum(){ int i, j=0;for(i=1;i<100;i++)j+=i; return j;} 多数(但确实不是全部)编译器也不会把它改为 // 程序段1(改进2)inline int gaussianSum(){ return 5050;} 这两个修改版本都不同于原先程序的语义。首先我们看到,让i从0开始是没有必要的,因为j+=i时,i=0不会做任何有用的事情;然后是,实际上没有必要每一次都计算1+...+100的和——它可以被预先计算,并在需要的时候返回。 这个例子也许并不恰当(估计没人会写出最初版本那样的代码),但这种实践在程序设计中确实可能出现。我们把改进2称为编译时表达式预先计算,而把改进1成为循环强度削减。 然而,一些新的编译器的确会进行这两种优化。不过别慌,看看下面的代码: // 程序段2 int GetFactorial(int k){ int i, j=1;if((k<0)||(k>=10))return-1; if((k<=1))return 1 for(i=1;i return j;} 程序采用的是一个时间复杂度为O(n)的算法,不过,我们可以把他轻易地改为O(1)的算法: // 程序段2(非规范改进)int GetFactorial(int k){ int i, j=1;static const int FractorialTable[]={1, 1, 2, 6, 24, 120, 720, 5040, 40320, 362880, 3628800}; if((k<0)||(k>=10))return-1; return FractorialTable[k];} 这是一个典型的以空间换时间的做法。通用的编译器不会这么做——因为它没有办法在编译时确定你是不是要这么改。可以说,如果编译器真的这样做的话,那将是一件可怕的事情,因为那时候你将很难知道编译器生成的代码和自己想的到底有多大的差距。 当然,这类优化超出了本文的范围——基本上,我把它们归入“算法优化”,而不是“程序优化”一类。类似的优化过程需要程序设计人员对于程序逻辑非常深入地了解和全盘的掌握,同时,也需要有丰富的算法知识。 自然,如果你希望自己的程序性能有大幅度的提升,那么首先应该做的是算法优化。例如,把一个O(n2)的算法替换为一个O(n)的算法,则程序的性能提升将远远超过对于个别语句的修改。此外,一个已经改写为汇编语言的程序,如果要再在算法上作大幅度的修改,其工作量将和重写相当。因此,在决定使用汇编语言进行优化之前,必须首先考虑算法优化。但假如已经是最优的算法,程序运行速度还是不够快怎么办呢? 好的,现在,假定你已经使用了已知最好的算法,决定把它交给编译器,让我们来看看编译器会为我们做什么,以及我们是否有机会插手此事,做得更好。 5.1 循环优化:强度削减和代码外提 比较新的编译器在编译时会自动把下面的代码: for(i=0;i<10;i++){ j = i;k = j + i;} 至少变换为 for(i=0;i<10;i++);j=i;k=j+i; 甚至 j=i=10;k=20; 当然,真正的编译器实际上是在中间代码层次作这件事情。 原理 如果数据项的某个中间值(程序执行过程中的计算结果)在使用之前被另一中间值覆盖,则相关计算不必进行。 也许有人会问,编译器不是都给咱们做了吗,管它做什么?注意,这里说的只是编译系统中优化部分的基本设计。不仅在从源代码到中间代码的过程中存在优化问题,而且编译器生成的最终的机器语言(汇编)代码同样存在类似的问题。目前,几乎所有的编译器在最终生成代码的过程中都有或多或少的瑕疵,这些瑕疵目前只能依靠手工修改代码来解决。 5.2 局部优化:表达式预计算和子表达式提取 表达式预先计算非常简单,就是在编译时尽可能地计算程序中需要计算的东西。例如,你可以毫不犹豫地写出下面的代码: const unsigned long nGiga = 1024L * 1024L * 1024L; 而不必担心程序每次执行这个语句时作两遍乘法,因为编译器会自动地把它改为 const unsigned long nGiga = 1073741824L; 而不是傻乎乎地让计算机在执行到这个初始化赋值语句的时候才计算。当然,如果你愿意在上面的代码中掺上一些变量的话,编译器同样会把常数部分先行计算,并拿到结果。 表达式预计算并不会让程序性能有飞跃性的提升,但确实减少了运行时的计算强度。除此之外,绝大多数编译器会把下面的代码: // [假设此时b, c, d, e, f, g, h都有一个确定的非零整数值,并且,// a[]为一个包括5个整数元素的数组,其下标为0到4] a[0] = b*c;a[1] = b+c;a[2] = d*e;a[3] = b*d + c*d;a[4] = b*d*e + c*d*e; 优化为(再次强调,编译器实际上是在中间代码的层次,而不是源代码层次做这件事情!): // [假设此时b, c, d, e, f, g, h都有一个确定的非零整数值,并且,// a[]为一个包括5个整数元素的数组,其下标为0到4] a[0] = b*c;a[1] = b+c;a[2] = d*e;a[3] = a[1] * d;a[4] = a[3] * e; 更进一步,在实际代码生成过程中,一些编译器还会对上述语句的次序进行调整,以使其运行效率更高。例如,将语句调整为下面的次序: // [假设此时b, c, d, e, f, g, h都有一个确定的非零整数值,并且,// a[]为一个包括5个整数元素的数组,其下标为0到4] a[0] = b*c;a[1] = b+c;a[3] = a[1] * d;a[4] = a[3] * e;a[2] = d*e; 在某些体系结构中,刚刚计算完的a[1]可以放到寄存器中,以提高实际的计算性能。上述5个计算任务之间,只有1, 3, 4三个计算任务必须串行地执行,因此,在新的处理器上,这样做甚至能够提高程序的并行度,从而使程序效率变得更高。 5.3 全局寄存器优化 [待修订内容] 本章中,从这一节开始的所有优化都是在微观层面上的优化了。换言之,这些优化是不能使用高级语言中的对应设施进行解释的。这一部分内容将进行较大规模的修订。 通常,此类优化是由编译器自动完成的。我个人并不推荐真的由人来完成这些工作——这些工作多半是枯燥而重复性的,编译器通常会比人做得更好(没说的,肯定也更快)。但话说回来,使用汇编语言的程序设计人员有责任了解这些内容,因为只有这样才能更好地驾驭处理器。 在前面的几章中我已经提到过,寄存器的速度要比内存快。因此,在使用寄存器方面,编译器一般会做一种称为全局寄存器优化的优化。 例如,在我们的程序中使用了4个变量:i, j, k, l。它们都作为循环变量使用: for(i=0;i<1000;i++){ for(j=0;j<1000;j++){ for(k=0;k<1000;k++){ for(l=0;l<1000;l++)do_something(i, j, k, l);} } } 这段程序的优化就不那么简单了。显然,按照通常的压栈方法,i, j, k, l应该按照某个顺序被压进堆栈,然后调用do_something(),然后函数做了一些事情之后返回。问题在于,无论如何压栈,这些东西大概都得进内存(不可否认某些机器可以用CPU的Cache做这件事情,但Cache是写通式的和回写式的又会造成一些性能上的差异)。 聪明的读者马上就会指出,我们不是可以在定义do_something()的时候加上inline修饰符,让它在本地展开吗?没错,本地展开以增加代码量为代价换取性能,但这只是问题的一半。编译器尽管完成了本地展开,但它仍然需要做许多额外的工作。因为寄存器只有那么有限的几个,而我们却有这么多的循环变量。 把四个变量按照它们在循环中使用的频率排序,并决定在do_something()块中的优先顺序(放入寄存器中的优先顺序)是一个解决方案。很明显,我们可以按照l, k, j, i的顺序(从高到低,因为l将被进行1000*1000*1000*1000次运算!)来排列,但在实际的问题中,事情往往没有这么简单,因为你不知道do_something()中做的到底是什么。而且,凭什么就以for(l=0;l<1000;l++)作为优化的分界点呢?如果do_something()中还有循环怎么办? 如此复杂的计算问题交给计算机来做通常会有比较满意的结果。一般说来,编译器能够对程序中变量的使用进行更全面地估计,因此,它分配寄存器的结果有时虽然让人费解,但却是最优的(因为计算机能够进行大量的重复计算,并找到最好的方法;而人做这件事相对来讲比较困难)。 编译器在许多时候能够作出相当让人满意的结果。考虑以下的代码: int a=0;for(int i=1;i<10;i++)for(int j=1;j<100;j++){ a +=(i*j);} 让我们把它变为某种形式的中间代码: 00: 0-> a 01: 1-> i 02: 1-> j 03: i*j-> t 04: a+t-> a 05: j+1-> j 06: evaluate j < 100 07: TRUE? goto 03 08: i+1-> i 09: evaluate i < 10 10: TRUE? goto 02 11: [继续执行程序的其余部分] 程序中执行强度最大的无疑是03到05这一段,涉及的需要写入的变量包括a, j;需要读出的变量是i。不过,最终的编译结果大大出乎我们的意料。下面是某种优化模式下Visual C++ 6.0编译器生成的代码(我做了一些修改): xor eax, eax;a=0(eax: a)mov edx, 1;i=1(edx: i)push esi;保存esi(最后要恢复,esi作为代替j的那个循环变量)nexti: mov ecx, edx;[t=i] mov esi, 999;esi=999: 此处修改了原程序的语义,但仍为1000次循环。nextj: add eax, ecx;[a+=t] add ecx, edx;[t+=i] dec esi;j--jne SHORT nextj;jne 等价于 jnz.[如果还需要,则再次循环] inc edx;i++ cmp edx, 10;i与10比较 jl SHORT nexti;i < 10, 再次循环 pop esi;恢复esi 这段代码可能有些令人费解。主要是因为它不仅使用了大量寄存器,而且还包括了5.2节中曾提到的子表达式提取技术。表面上看,多引入的那个变量(t)增加了计算时间,但要注意,这个t不仅不会降低程序的执行效率,相反还会让它变得更快!因为同样得到了计算结果(本质上,i*j即是第j次累加i的值),但这个结果不仅用到了上次运算的结果,而且还省去了乘法(很显然计算机计算加法要比计算乘法快)。 这里可能会有人问,为什么要从999循环到0,而不是按照程序中写的那样从0循环到999呢?这个问题和汇编语言中的取址有关。在下两节中我将提到这方面的内容。 5.4 x86体系结构上的并行最大化和指令封包 考虑这样的问题,我和两个同伴现在在山里,远处有一口井,我们带着一口锅,身边是树林;身上的饮用水已经喝光了,此处允许砍柴和使用明火(当然我们不想引起火灾:),需要烧一锅水,应该怎么样呢? 一种方案是,三个人一起搭灶,一起砍柴,一起打水,一起把水烧开。 另一种方案是,一个人搭灶,此时另一个人去砍柴,第三个人打水,然后把水烧开。 这两种方案画出图来是这样: 仅仅这样很难说明两个方案孰优孰劣,因为我们并不明确三个人一起打水、一起砍柴、一起搭灶的效率更高,还是分别作效率更高(通常的想法,一起做也许效率会更高)。但假如说,三个人一个只会搭灶,一个只会砍柴,一个只会打水(当然是说这三件事情),那么,方案2的效率就会搞一些了。 在现实生活中,某个人拥有专长是比较普遍的情况;在设计计算机硬件的时候则更是如此。你不可能指望加法器不做任何改动就能去做移位甚至整数乘法,然而我们注意到,串行执行的程序不可能在同一时刻同时用到处理器的所有功能,因此,我们(很自然地)会希望有一些指令并行地执行,以充分利用CPU的计算资源。 CPU执行一条指令的过程基本上可以分为下面几个阶段:取指令、取数据、计算、保存数据。假设这4个阶段各需要1个时钟周期,那么,只要资源够用,并且4条指令之间不存在串行关系(换言之这些指令的执行先后次序不影响最终结果,或者,更严格地说,没有任何一条指令依赖其他指令的运算结果)指令也可以像下面这样执行: 指令1 取指令 取数据 计 算 存数据 指令2 取指令 取数据 计 算 存数据 指令3 取指令 取数据 计 算 存数据 指令4 取指令 取数据 计 算 存数据 这样,原本需要16个时钟周期才能够完成的任务就可以在7个时钟周期内完成,时间缩短了一半还多。如果考虑灰色的那些方格(这些方格可以被4条指令以外的其他指令使用,只要没有串行关系或冲突),那么,如此执行对于性能的提升将是相当可观的(此时,CPU的所有部件都得到了充分利用)。 当然,作为程序来说,真正做到这样是相当理想化的情况。实际的程序中很难做到彻底的并行化。假设CPU能够支持4条指令同时执行,并且,每条指令都是等周期长度的4周期指令,那么,程序需要保证同一时刻先后发射的4条指令都能够并行执行,相互之间没有关联,这通常是不太可能的。 最新的Intel Pentium 4-XEON处理器,以及Intel Northwood Pentium 4都提供了一种被称为超线程(Hyper-Threading TM)的技术。该技术通过在一个处理器中封装两组执行机构来提高指令并行度,并依靠操作系统的调度来进一步提升系统的整体效率。 由于线程机制是与操作系统密切相关的,因此,在本文的这一部分中不可能做更为深入地探讨。在后续的章节中,我将介绍Win32、FreeBSD 5.x以及Linux中提供的内核级线程机制(这三种操作系统都支持SMP及超线程技术,并且以线程作为调度单位)在汇编语言中的使用方法。 关于线程的讨论就此打住,因为它更多地依赖于操作系统,并且,无论如何,操作系统的线程调度需要更大的开销并且,到目前为止,真正使用支持超线程的CPU,并且使用相应操作系统的人是非常少的。因此,我们需要关心的实际上还是同一执行序列中的并发执行和指令封包。不过,令人遗憾的是,实际上在这方面编译器做的几乎是肯定要比人好,因此,你需要做的只是开启相应的优化;如果你的编译器不支持这样的特性,那么就把它扔掉„„据我所知,目前在Intel平台上指令封包方面做的最好的是Intel的C++编译器,经过Intel编译器编译的代码的性能令人惊异地高,甚至在AMD公司推出的兼容处理器上也是如此。5.5 存储优化 从前一节的图中我们不难看出,方案2中,如果谁的动作慢,那么他就会成为性能的瓶颈。实际上,CPU也不会像我描述的那样四平八稳地运行,指令执行的不同阶段需要的时间(时钟周期数)是不同的,因此,缩短关键步骤(即,造成瓶颈的那个步骤)是缩短执行时间的关键。 至少对于使用Intel系列的CPU来说,取数据这个步骤需要消耗比较多的时间。此外,假如数据跨越了某种边界(如4或8字节,与CPU的字长有关),则CPU需要启动两次甚至更多次数的读内存操作,这无疑对性能构成不利影响。 基于这样的原因,我们可以得到下面的设计策略: 程序设计中的内存数据访问策略 尽可能减少对于内存的访问。在不违背这一原则的前提下,如果可能,将数据一次处理完。尽可能将数据按4或8字节对齐,以利于CPU存取 尽可能一段时间内访问范围不大的一段内存,而不同时访问大量远距离的分散数据,以利于Cache缓存* 第一条规则比较简单。例如,需要求一组数据中的最大值、最小值、平均数,那么,最好是在一次循环中做完。 “于是,这家伙又攒了一段代码”„„ int a[]={1,2,3,4,5,6,7,8,9,0,1,2,3,4,5,6,7,8,9,0};int i;int avg, max, min;avg=max=min=a[0]; for(i=1;i<(sizeof(a)/sizeof(int));i++){ avg+=a[i];if(max < a[i])max = a[i];else if(min > a[i])min = a[i];} avg /= i; Visual C++编译器把最开始一段赋值语句翻译成了一段简直可以说是匪夷所思的代码: ;int a[]={1,2,3,4,5,6,7,8,9,0,1,2,3,4,5,6,7,8,9,0}; mov edi, 2;此时edi没有意义 mov esi, 3;esi也是!临时变量而已。mov DWORD PTR _a$[esp+92], edi mov edx, 5;黑名单加上edx mov eax, 7;eax也别跑:)mov DWORD PTR _a$[esp+132], edi mov ecx, 9;就差你了,ecx ;int i;;int avg, max, min;;avg=max=min=a[0]; mov edi, 1;edi摇身一变,现在它是min了。mov DWORD PTR _a$[esp+96], esi mov DWORD PTR _a$[esp+104], edx mov DWORD PTR _a$[esp+112], eax mov DWORD PTR _a$[esp+136], esi mov DWORD PTR _a$[esp+144], edx mov DWORD PTR _a$[esp+152], eax mov DWORD PTR _a$[esp+88], 1;编译器失误? 此处edi应更好 mov DWORD PTR _a$[esp+100], 4 mov DWORD PTR _a$[esp+108], 6 mov DWORD PTR _a$[esp+116], 8 mov DWORD PTR _a$[esp+120], ecx mov DWORD PTR _a$[esp+124], 0 mov DWORD PTR _a$[esp+128], 1 mov DWORD PTR _a$[esp+140], 4 mov DWORD PTR _a$[esp+148], 6 mov DWORD PTR _a$[esp+156], 8 mov DWORD PTR _a$[esp+160], ecx mov DWORD PTR _a$[esp+164], 0 mov edx, edi;edx是max。 mov eax, edi;期待已久的avg, 它被指定为eax 这段代码是最优的吗?我个人认为不是。因为编译器完全可以在编译过程中直接把它们作为常量数据放入内存。此外,如果预先对a[0..9]10个元素赋值,并利用串操作指令(rep movsdw),速度会更快一些。 当然,犯不上因为这些问题责怪编译器。要求编译器知道a[0..9]和[10..19]的内容一样未免过于苛刻。我们看看下面的指令段: ;for(i=1;...mov esi, edi for_loop: ;avg+=a[i]; mov ecx, DWORD PTR _a$[esp+esi*4+88] add eax, ecx ;if(max < a[i]) cmp edx, ecx jge SHORT elseif_min ;max = a[i]; mov edx, ecx ;else if(min > a[i]) jmp SHORT elseif_min elseif_min: cmp edi, ecx jle SHORT elseif_end ;min = a[i];mov edi, ecx elseif_end: ;[for i=1];i<20;i++){ inc esi cmp esi, 20 jl SHORT for_loop;};avg /= i;cdq idiv esi ;esi: i ;ecx: 暂存变量, =a[i];eax: avg ;edx: max ;有趣的代码...并不是所有的时候都有用;但是也别随便删除;edi: min ;i++;i与20比较 ;avg /= i 上面的程序倒是没有什么惊人之处。唯一一个比较吓人的东西是那个jmp SHORT指令,它是否有用取决于具体的问题。C/C++编译器有时会产生这样的代码,我过去曾经错误地把所有的此类指令当作没用的代码而删掉,后来发现程序执行时间没有明显的变化。通过查阅文档才知道,这类指令实际上是“占位指令”,他们存在的意义在于占据那个地方,一来使其他语句能够正确地按CPU觉得舒服的方式对齐,二来它可以占据CPU的某些周期,使得后续的指令能够更好地并发执行,避免冲突。另一个比较常见的、实现类似功能的指令是NOP。 占位指令的去留主要是靠计时执行来判断。由于目前流行的操作系统基本上都是多任务的,因此会对计时的精确性有一定影响。如果需要进行测试的话,需要保证以下几点: 计时测试需要注意的问题 测试必须在没有额外负荷的机器上完成。例如,专门用于编写和调试程序的计算机 尽量终止计算机上运行的所有服务,特别是杀毒程序 切断计算机的网络,这样网络的影响会消失 将进程优先级调高。对于Windows系统来说,把进程(线程)设置为Time-Critical;对于*nix系统来说,把进程设置为实时进程 将测试函数运行尽可能多次运行,如10000000次,这样能够减少由于进城切换而造成的偶然误差 最后,如果可能的话,把函数放到单进程的系统(例如FreeDOS)中运行。 对于绝大多数程序来说,计时测试是一个非常重要的东西。我个人倾向于在进行优化后进行计时测试并比较结果。目前,我基于经验进行的优化基本上都能够提高程序的执行性能,但我还是不敢过于自信。优化确实会提高性能,但人做的和编译器做的思路不同,有时,我们的确会做一些费力不讨好的事情。 几种早期教育书籍简介.txt如果青春的时光在闲散中度过,那么回忆岁月将是一场凄凉的悲剧。杂草多的地方庄稼少,空话多的地方智慧少。即使路上没有花朵,我仍可以欣赏荒芜。==================== 软件简介: 世界上最受欢迎的九种教育法 1、卡尔·威特:全能教育法 《卡尔·威特的教育》 2、塞德兹:天才教育法 《俗物与天才》 3、约翰·洛克:全面教育法 《教育漫话》(又译《家庭学校》) 4、蒙台梭利:特殊教育法 《蒙台梭利教育法》、《蒙台梭利手册》、《教育人类学》、《高级蒙台梭利方法》、《童年的秘密》、《儿童的发现》等书 5、斯宾塞:快乐教育法 《智育》、《德育》、《体育》以及《什么知识最有价值》 6、M.S.斯特娜:自然教育法 《M.S.斯特娜的自然教育》 7、铃木镇一:才能教育法 《早期教育与能力培养》 8、多湖辉:实践教育法 《母爱促进身心健康》、《如何开发孩子的能力》、《管教孩子的技巧》、《学习指导法》《使孩子聪明的心理战术》、《培养孩子学习兴趣方法谈》、《孩子的心理规律》、《责备孩子的方略》等多湖辉的著作从胎儿到小学教育,从教育心理学到具体的教育行为都有详细的论述,可谓是“儿童教育的百科全书” 9、井深大:早期教育法 网络上能找到的不多,这些都是本人辛苦收集的几本,拿出来分享! =================================== 《蒙台梭利早期教育法》 本书是蒙台梭利博士的第一本儿童教育专著,被译成20多种文字,是她对自己亲手创立的“儿童之家”的经验总结。正是这本书的问世,使她成为全球儿童教育理论与实践方面最有影响力的教育家之一。本书是蒙台梭利博士对她所进行的教育创新背后的理论原则的揭示,向父母、教师和教育管理者介绍了蒙台梭利方法的指导原则,传授了如何“让孩子通过自己的努力去自由地学习”。通过本书所介绍的方法,孩子能培养自己的秩序意识和逻辑思维能力。 《蒙台梭利儿童教育手册》 本书是蒙台梭利博士在美国传授蒙台梭利方法期间,应无数对她的方法感兴趣的父母和教师的要求而写作的一本操作性手册。该手册向人们传授了“儿童之家”所运用的教具和技术,以及如何为孩子们提供一个进行“自我教育”的环境。从蒙台梭利创办第一所“儿童之家”至今,所有蒙台梭利教室的教具都十分统一。蒙台梭利博士在本书中传授了如何教学前儿童使用这些教具,以刺激其观察力、认识力和判断力的发育。蒙台梭利博士强调,对每个孩子的方法是不同的,成人的作用应该是引导孩子自己去试验,让他们自己意识到自己的错误,让他们自己在学习中冒必要的风险。 《童年的秘密》 是蒙台梭利对幼儿之谜的探索和解答,记录了她在学前儿童方面的研究和教育工作,阐述了幼儿教育的原则和方法。在“导论”中,蒙台梭利指出:“儿童并不是一个只可以从外表观察的陌生人。更确切地说,童年构成了人生中最重要的一部分,因为一个人是在他的早期就形成的。”“为儿童的利益所作的孜孜不倦和真诚的努力将使我们能够发现人类的秘密,正如科学的调查研究能使我们洞察众多的自然秘密一样。” 《童年的秘密》是一本了解儿童发育和成长秘密的最生动的著作。在书中,蒙台梭利详细而生动地描绘了儿童的生理和心理特征,揭开儿童成长奥秘的革命性观念。它让世人了解到,儿童具有丰富的潜能,但儿童只有在一个与他的年龄相适应的环境中,他的心理生活才会自然地发展,并展现他内心的秘密。而有些儿童之所以不能正常地发育和成长,主要是因为受到成年人的忽视和压抑。因此,我感受最深的是:幼儿教育的原则和方法应是理解、尊重儿童,根据儿童身心发展的规律,为儿童实现自身的潜能提供所需的帮助。 但蒙台梭利教育方法也存在着一定的局限性,如过分强调工作对儿童身心发展的作用,而轻视游戏的作用,把游戏仅视为闲暇时的一种快乐的消遣;过分强调教具的价值,而贬低玩具的价值;具有神秘主义和宗教色彩等等。因此,我们在学习和借鉴蒙台梭利教育理论方法的过程中,应本着一切为了孩子的宗旨,结合具体情况加以利用、改造和创新。 《发现孩子》 蒙台梭利教育法之所以能影响整个世界的教育体系,关键在于她在总结卢梭、裴斯泰格齐、福禄贝尔等人自然主义教育思想的基础上,形成了自己革命性的儿童观念。她认为儿童有一种与生俱来的“内在生命力”,这种生命力是一种积极的、活动的、发展着的存在,它具有无穷无尽的力量。教育的任务就是激发和促进儿童“内在潜力”的发挥,使其按自身规律获得自然的和自由的发展。她主张,不应该把儿童作为一种物体来对待,而应作为人来对待。儿童不是成人和教师进行灌注的容器;不是可以任意塑造的蜡或泥;不是可以任意刻划的木块;也不是父母和教师培植的花木或饲养的动物,而是一个具有生命力的、能动的、发展着的活生生的人。教育家、教师和父母应该仔细观察和研究儿童,了解儿童的内心世界,发现“童年的秘密”;热爱儿童,尊重儿童的个性,促进儿童的智力、精神、身体与个性自然发展。她还利用第一手观察资料和“儿童之家”的实验,提出了一系列有关儿童发展的规律。 蒙台梭利认为,每个孩子都需要去观察,对外界作出反应,去学习,去集中注意力,甚至让自己独处。为此,蒙台梭利一直致力于打破已有的教育传统,寻求了解孩子和爱孩子的新方法。在本书中,她描述了孩子的特性,传授了如何更充分地唤起孩子学习热情的方法。正如蒙台梭利所言:“即便是对那些非常幼小孩子的教育,我们的目的不应是为他们上学准备,而是为了他们的生活。” 《有吸收力的心灵》 蒙台梭利博士的封笔之作,集蒙台梭利思想和方法大成之作。 本书是蒙台梭利博士最受欢迎,并且最能体现她理论创新意义的书。在本书中,我们处处能见到她那些至今仍然超前而且十分重要的思想。如教育并非“老师做了什么”,人类自身的自然发展在其中起着举足轻重的作用;孩子的知识不是完全通过教育得到的,而是通过儿童在他们所处的环境中吸取经验获得的;教育不应该只停留在课程和时间表上,它必须符合人类自身的实际,等等。本书将蒙台梭利对儿童发展的探索与她有关“如何确立一种新的教育方法”的论述结合起来,是一本了解蒙台梭利教育思想的必读书。 =================== 1.2 蒙台梭利留下了丰富的教育著作。就其内容可大致分为四类: (一)关于从诞生到3周岁儿童教育的有:《童年的秘密》、《儿童的发现》和《有吸收力的心理》; (二)关于3岁至7岁儿童教育的有:《蒙台梭利的方法》、《蒙台梭利手册》、《家庭中的儿童》; (三)关于7岁至13岁儿童及青春期教育的有:《高级蒙台梭利方法》两卷,包括《教育中的自发活动》与《蒙台梭利初等教具》、《青春期及其后的教育》; (四)关于教育与智力开发:《新世界的教育》、《教育人类学》、《开发人类的潜能》、《教育的重建》、《和平教育》等。第五篇:几种早期教育书籍简介