网站内容建设和运营工作,编程代码怎么学,建站系统下载 discuz,外贸网站建设公司服务程序的机器级表示
有关CSAPP第三章一些我关注到的重点的记录
操作指令
.c-.exe的流程 1.选项 -E : 预编译过程,处理宏定义和include#xff0c;并作语法检查
gcc -E hello.c -o hello.i #将hello.c预处理输出为hello.i文件2.选项 -S : 编译过程,生成通用…程序的机器级表示
有关CSAPP第三章一些我关注到的重点的记录
操作指令
.c-.exe的流程 1.选项 -E : 预编译过程,处理宏定义和include并作语法检查
gcc -E hello.c -o hello.i #将hello.c预处理输出为hello.i文件2.选项 -S : 编译过程,生成通用的汇编代码
gcc -S hello.c #生成汇编代码hello.s生成的汇编文件以“.”开头的行都是指导汇编器和链接器工作的伪指令
3.选项 -c : 汇编过程,生成ELF格式的可重定位目标文件目标文件(机器代码)用文本编辑器打开是乱码
gcc -c hello.c #生成目标代码hello.o(中间文件)不能执行在Makefile中应用广泛4.选项 -L : 链接过程,将.o文件与所需库文件链接合并成ELF格式的可执行目标文件分静态链接和动态链接
gcc hello.o -L dir(如./lib) #指定库搜索路径有多个则从前往后搜索5.选项 -l : 链接过程,指定链接库库命名规则是libxxx.a,指定库名时使用的格式是-lxxx
gcc hello.c -o hello -lm #链接数学库
ld -o hello hello.o -lxxx #链接xxx库6.选项 -o : 将源文件预处理、编译、汇编并链接形成可执行目标文件-o选项指定可执行文件的文件名加载到内存中即可执行
gcc hello.c -o hello #生成可执行文件hello7.部分选项 选项 -Wall : 编译时打开警告信息开关 选项 -D : 在文件中定义宏INFO编译时加上-D INFO使其生效 选项 -O : 后指定数字使用编译优化级别1~3优化程序 选项 -g : 产生调试信息
8.选项 -static : 使用静态链接库将使用的静态库对象嵌入至可执行映像文件中加载时无需进一步的链接
gcc -c -Wall x1.c x2.c #生成目标文件
ar -cru libxxx.a x1.o x2.o #创建静态库
#定义静态库的应用接口xxx.h里面显式引用上面的源文件函数和对象
gcc -O2 -c main.c #测试用例调用静态库的函数
gcc -static -o p main.o ./libxxx.a #链接静态库和目标文件生成可执行文件p9.选项 -share : 使用共享库在运行时动态加载目标程序所需要的信息 选项 -fPIC : 指示编译器生成与地址无关的目标文件(position-independent code)
gcc -shared -fPIC -o libxxx.so x1.c x2.c #生成共享库libvector.so
gcc -o p1 main.c ./libvector.so #共享库中的目标对象并未嵌入可执行文件中执行时完成链接过程.c-.exe
linux gcc -Og -o p -g p.c-Og优化等级比较符合原始C代码整体结构方便学习(为了更高的性能可以使用-O1或-O2甚至更高的编译优化选项)-o转化成可执行文件-g生成调试信息p为转化成可执行文件的文件名p.c为源文件名
.c-.s 编译生成汇编文件
linux gcc -Og -S p.c.c-.o 汇编生成目标文件
linux gcc -Og -c p.c.o/.exe-.s 反汇编
linux objdump -d p.oC语言嵌套汇编语言
C编译器在把程序中表达的计算转换到机器代码中表现很出色但仍然有一些及其特性是C语言访问不到的。例如x86-64处理器执行算术或逻辑运算时修改奇偶标志位寄存器PF的值时用汇编语言的效率远高于C语言故如果能在C语言中嵌套C语言会提供大大的方便。
第一种方法源代码中插入汇编代码
#include stdio.h
#include stdlib.hint main(void)
{/* basic command demo */__asm__(movl %eax, %ecx);/* set b 10 */int a 10, b 0;__asm__(movl %1, %%eax;movl %%eax, %0;:r (b) /* output */:r (a) /* input */:%eax /* clobbered register */);printf(%s: b %d\n, __func__, b);return 0;
}第二种方法写好汇编文件和C文件用汇编器和链接器把它们合并起来
保存寄存器
假设现在有两个函数funcA和funcB函数A称为调用者函数B称为被调用者由于调用了函数B寄存器rbx在函数B中被修改了而逻辑上rbx寄存器的内容在调用函数B的前后应该保持一致解决这个问题有两个策略调用者保存和被调用者保存。
func_A:...movq $123, %rbxcall func_Badd %rbx, %rax...retfunc_B:...addq $456, %rbx...rer调用者保存
func_A:...movq $123, %rbx保存rbxcall func_B恢复rbxadd %rbx, %rax...retfunc_B:...addq $456, %rbx...rer被调用者保存
func_A:...movq $123, %rbxcall func_Badd %rbx, %rax...retfunc_B:...保存rbxaddq $456, %rbx恢复rbx...rer具体使用哪种策略取决于寄存器被定义为那种类型下图是寄存器类型 c语言基本类型对应汇编后缀表示 访问信息
各存储部件的性价比 通用寄存器
寄存器用途%eax操作数运算%ebx指向DS段中数据的指针%ecx字符串操作和循环计数器%edx输入输出指针%esi指向DS段中数据的指针或字符串操作中字符串的复制源%edi指向ES段中数据的指针或字符串操作中字符串的复制地%esp栈指针(SS段)%ebp指向SS段上数据的指针
段寄存器
寄存器用途CS代码段DS数据段SS堆栈段ES数据段FS数据段GS数据段
C类型长度
C声明Intel数据类型汇编代码后缀大小字节char字节b1short字w2int双字l4long四字q8char*四字q8float单精度s4double双精度l8 指令
指令包含操作码和操作数。 操作码 操作数movq (%rdi), %raxaddq $8, %rsxsubq %rdi, %raxxorq %rsi, %rdiret操作码决定CPU执行操作的类型
指令可以有一个、多个或没有操作数
操作数分为3类分别为立即数、寄存器以及内存引用
数据寻址模式 这里的比例因子s会根据数据类型取
数据传送命令 以上命令中没有movzlq是因为一个结论当复制和生成字节以寄存器为目标时对于生成4字节的指令会把高位4个字节置为0所以用movl就能代替命令movzlq例如
movl %eax,%edx实际上除了将低32位数据由eax传递给rdx的低32位之外还把高32位设置为0
这里注意到练习题3.3的一题找以下代码的错误
movl %eax,%rdx在这里错误是源操作数和目标操作数类型不匹配虽然eax传值后会扩展为64位但在写代码时依然需要保持操作数类型的统一
movq指令的限制
当movq指令的源操作数是立即数时只能是32位的立即数此时会对该立即数进行符号扩展到64位再将得到的64位立即数传送到目的位置。
那么当源操作数是64位立即数时就引入了一个新的指令movabsq此时就能将64位立即数作为源操作数但目的操作数只能是寄存器
cltq指令
cltq movslq %eax%rax
算术和逻辑操作
操作指令 具体操作如下图之所以z被分为两步操作是因为比例因子只能取1、2、4、8这四个数中的一个 移位操作
移位量可以是一个立即数或者放在单字节寄存器%cl中。
移位操作对w位长的数据值进行操作移位量是由**%cl寄存器**的低m位决定的这里2mw高位被忽略。所以例如寄存器%cl的十六进制值为0xFF时指令salb会移7位salw会移15位sall会移31位而salq会移63位。
SAR算术右移高位补符号位SHR逻辑右移高位补0
以下操作使用移位操作而不使用乘法操作的原因是因为乘法指令执行需要更长时间因此编译器在生成汇编指令时会优先考虑更高效的方式。 特殊的算术操作 控制
条件码
CPU除了提供上面的几个整数寄存器外还维护着一组单个比特位的条件码描述最近的算术或逻辑操作特性用于执行条件分支指令。 CF 进位标志表示最近的操作使最高位产生了进位。用于检查无符号操作数的溢出如下图 ZF 零标志表示最近的操作得出的结果为0如下图 SF 符号标志表示最近的操作得出的结果为负数 OF 溢出标志表示最近的操作使补码溢出-正溢出或负溢出
条件码寄存器的值是由ALU执行算术逻辑运算指令改变的
有几种设置条件码的情形 INC加一和DEC减一指令会设置OF溢出标志和ZF零标志但不会改变CF进位标志。
因为指令系统设计人员考虑该指令主要用于对指针即地址进行增加不存在进位问题所以没有设计让INC影响进位标志CF。 INC,DEC指令不影响CF标志位,这个是Intel规定的其原因是硬件设计造成的总之对软件人员来制说不重要 INC,DEC指令不影响CF标志位,这表明执行INC/DEC指令之后CF不能反映进位情况。
INC 0000000011111111
00000000111111111当然要进位但不设置CF为1。 我们的问题就在于将进位与CF等同 CF被称为进位标志位在多数情况下它确实反映进位情况但不是绝对的INC/DEC就是其中两例 INC/DEC指令不影响CF标志位这句话就是明明白白地告诉你此时CF与进位无关
A. 比较和测试指令它们只设置条件码而不改变任何其他寄存器
cmp S2,S1 通过S1-S2的结果比较两者的大小 test S2,S1 通过S1S2的结果(按位与)比如testl %eax,%eax用来检查%eax是正数负数还是0或者其中一个操作数是掩码用来指示哪些位应该被测试
B. 根据条件码的组合使用set指令不同后缀名表示不同条件
set指令的目的操作数是8个单字节寄存器或者存储一个字节的存储器位置把该字节位置设置成0或1。它的基本思路是执行比较或测试指令根据set指令的类型决定计算结果ta-b操作数的大小是有符号的还是无符号的程序值的数据类型。如图所示为set指令的常见情形 跳转指令 关于跳转指令如何编码 可以看到第2行中跳转指令目标指明位0x8第5行中跳转指令跳转目标是0x5这里有一个规则在指令的字节编码中我们可以看到第二个字节中编码位0x3再将其加上下一条指令的地址即0x5就可以得到跳转目标地址0x8同样第5行0xf8(即十进制-8)这个数加上0xd即为地址0x5
条件分支
用条件控制来实现条件分支 实际上C语言中有一种语句叫做goto一般不推荐使用但是它的控制和汇编代码的条件转移十分相似。
例如我们有这样一段正常的代码实际上就是得到两数之差的绝对值
long absdiff(long x, long y) { long result; if (x y) result x-y; else result y-x; return result; }
然后我们使用goto语句改写一下
long absdiff_j(long x, long y) { long result; int ntest x y; if (ntest) goto Else; result x-y; goto Done; Else: result y-x; Done: return result; }
从控制流的角度来看这两个代码基本上是一样的。
用条件传送来实现条件分支 条件传送和set指令有些相似也就是根据条件码部分来判断是否要进行数据传送使用的是cmovconditional move比如当相等的时候进行条件传送也就是cmove。
现代处理器会使用一种特殊的技术叫做流水线pipeline它的名字就是取自工厂流水线在CPU中也就是说当你执行一条指令的时候下一条指令的一部分会被执行下下一条指令的一部分也会被执行这样就提高了并行的程序。
但是条件转移会破坏流水线的运作于是我们会把两个条件的结果都计算一遍然后再根据跳转选择其中的一条。这里也就用到了cmov。
比如还是之前的程序我们汇编变成如下这个样子也就是把x-y和y-x都计算了然后再根据条件选择其中一个结果返回
absdiff: movq %rdi, %rax # x subq %rsi, %rax # result x-y movq %rsi, %rdx subq %rdi, %rdx # eval y-x cmpq %rsi, %rdi # x:y cmovle %rdx, %rax # if , result eval ret 但是使用cmov也会有一些负面影响
只有当计算较为简单时才用cmov进行优化如果两条分支都较为复杂那么使用cmov反而不好 对于某个分支而言计算它可能没有什么用只是浪费时间。 两个分支可能会存在关联性比如val x 0 ? x*7 : x3;如果两个都进行计算就会出现错误。
指令同义名传送条件描述cmove S,RcmovzZF相等/零cmovne S,Rcmovnz~ZF不相等/非零cmovs S,RSF负数cmovns S,R~SF非负数cmovg S,Rcmovnle~(SF^OF) ~ZF大于有符号)cmovge S,Rcmovnl~(SF^OF)大于或等于有符号cmovl S,RcmovngeSF^OF小于有符号cmovle S,Rcmovng(SF^OF) | ZF小于或等于有符号cmova S,Rcmovnbe~CF ~ZF超过无符号cmovae S,Rcmovnb~CF超过或相等无符号cmovb S,RcmovnaeCF低于无符号cmovbe S,RcmovnaCF | ZF低于或相等无符号
练习题3.20
在这里发现一个规则当负数做被除数时需要将该数先加上2k-1k为要右移的位数。这是为了保证正数向下舍入负数向上舍入 循环
一、do-while
如果用C的goto来实现则如下面的代码
loop:Bodyif (Test)goto loop实际上就是先循环体然后进行测试如果测试成功那么跳回到loop再继续循环。
举个例子比如我们有这样一个C程序的goto版本
long pcount_goto (unsigned long x) {long result 0;loop:result x 0x1;x 1;if(x) goto loop;return result;
}那么会发现汇编的版本也类似
movl $0, %eax # result 0
.L2: # loop:movq %rdi, %rdx andl $1, %edx # t x 0x1addq %rdx, %rax # result tshrq %rdi # x 1jne .L2 # if (x) goto looprep; ret
二、while
while和do-while的区别就在于do-while第一次不进行测试所以总会执行一遍循环体而while在开始就测试如果不满足就跳出不执行。
while的实现有2种方式第一种方式就是先跳到了do-while的中间然后进行测试。
C的goto版本如下
goto test;
loop:Body
test:if (Test)goto loop;
done:我们还是用pcount这个程序那么while就是下面这种实现
long pcount_goto_jtm(unsigned long x) {long result 0;goto test;loop:result x 0x1;x 1;test:if(x) goto loop;return result;
}第二种实现方式比较传统就是一开始进行判断如果不满足直接goto跳出满足那么进入到和do-while相同的语句块中。 if (!Test)goto done;
loop:Bodyif (Test)goto loop;
done:pcount的第二种while实现如下
long pcount_goto_dw(unsigned long x) {long result 0;if (!x) goto done;loop:result x 0x1;x 1;if(x) goto loop;done:return result;
}三、for
for循环实际上包含了4个部分例如一个C语言的for循环for(int i 0;i 5;i){body}包括了初始化int i 0测试i 5更新i和循环体。
如果用while循环来表示for循环那么就是先进行初始化然后是while循环在while循环体的最后加上更新操作。
Init;
while (Test ) {BodyUpdate;
}还是之前的例子我们使用for循环用while实现for实现
long pcount_for_while(unsigned long x)
{size_t i;long result 0;i 0;while (i WSIZE){unsigned bit (x i) 0x1;result bit;i;}return result;
}然后我们用goto替代
long pcount_for_goto_dw(unsigned long x) {size_t i;long result 0;i 0;if (!(i WSIZE))goto done;loop:{unsigned bit (x i) 0x1;result bit;}i;if (i WSIZE)goto loop;done:return result;
}如果使用了-O1优化级别那么第一次的判断很有可能不需要了编译器会将其舍弃
这里需要提示一下再跳转指令后若跟随ret会出现一些判断问题所以我们需要在中间加一个rep;这个什么都不会做所以也不需要管
过程
栈帧
当函数执行所需要的存储空间超出寄存器能够存放的大小时就会借助栈上的存储空间这部分存储空间称为函数的栈帧 如果一个函数的参数数量大于6超出部分就要使用栈来传递。 两点注意
1.通过栈传递参数时所有数据大小向8对齐 2.使用寄存器进行参数传递时寄存器的使用是由特殊顺序规定的 局部变量在栈帧存储不需要对齐参数才需要对齐 数组
不同类型指针加1得到结果不同 数组元素的计算 xd表示数组的起始地址L表示数组类型T的大小如果T时int类型L就等于4T是char类型L就等于1
例如下图 使用以下汇编代码将A[i][j]的值复制到寄存器eax中如下图所示 结构体
结构体在内存中的存储遵循内存对齐如下图由于变量j是int类型占4个字节它的起始地址必须是4的倍数所以在变量c和变量j之间插入了一个3字节的间隙结构体大小也就变成了12个字节。 如果我们变更顺序如下图此时能满足结构体的对齐要求但无法满足结构体数组的对齐要求所以如果定义结构体数组需要在末端加入3个字节的间隔。 **复杂示例**每个元素的偏移地址都必须是它数据大小的倍数且为满足每个元素都对齐最后要在结构体末端填充间隙根据结构体最大类型的长度如下图所示。 联合体
联合体中所有字段共享同一存储区域因此联合体的大小取决于它最大字段的大小如下图变量v和数组i的大小都是8个字节因此该联合体占8个字节的存储空间两个不同字段的使用是互斥的那么我们就可以将这两个字段声明为一个联合体。 示例一个二叉树包含叶子节点只包含两个double数据和内部节点只包含左右节点指针其定义如下图使用结构体定义需要占用32个字节而使用联合体只用占用16个字节。 但此时有一个问题就是无法确定节点是哪种节点解决办法是引入一个枚举类型如下图所示type占4个字节枚举占4个字节加上最后末尾间隔的4个字节最终这个结构体占24个字节 **类型转换**一种类型来存储另一种类型来访问 栈溢出攻击
解决通过栈溢出攻击系统的三种办法
1.栈随机化
栈的位置在程序每次运行时都发生变化在Linux系统中栈随机化已经成为了一种标准行为ASLR
2.栈破坏检测
编译器会在产生的汇编代码中加入一种栈保护者的机制来检测缓冲区越界就是在缓冲区与栈保存的状态值之间存储一个特殊值金丝雀值canary函数返回之前检测金丝雀值是否被修改来判断是否遭受攻击 3.限制可执行代码区域
这三种机制都不需要程序员做额外的操作都是通过编译器和操作系统实现的单独每一种机制都能降低用户的等级组合起来使用会更有效不幸的是仍然有方法能对计算机进行攻击。