CSAPP-Revision-ch03---浮点代码目前不知道到底考不考
Posted 给个HK.phd读
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了CSAPP-Revision-ch03---浮点代码目前不知道到底考不考相关的知识,希望对你有一定的参考价值。
终……终于要结束啦!
当然啦,痛苦的复习季才刚刚开始哈哈。
栈帧结构
我们都知道C语言的一个大特性就是函数(Function)。
对于函数,我们都知道我们要转移控制权,我们需要把控制转移到调用的函数里。
我们的调用方应该可以向被调用方传递参数,而函数也应该能够返回值,这就是传递数据。
同时我们的被调用方也可以进行分配和释放内存。
对于这么一种抽象,我们称之为“过程”。
我们用”栈“这种后进先出的数据结构来模拟”过程“。
我们用下面这张经典的图来体现很多内容:
这是一个栈,栈地址是向低地址方向(栈“顶”方向)增长的。
当前在执行的“过程”总是处于栈顶位置,**而调用此过程的之前那些过程也作为“栈帧”保存在高地址处,**这些过程都处于挂起的状态。
我们通过指令call和ret来实现控制的转移。
比如当我们处于P调用call要执行Q时,会先保存住P继续执行的下一条代码的位置。
同时会将PC设置为Q的起始位置。
而调用ret时,就会弹出在P的栈帧结构保存的位置,作为返回地址。这样才能保证指令按我们的目标顺序执行下来。
接下来是一个例子:
图中%rip就是我们说的PC值。
可以看到一开始指向0x400563位置的指令,call执行后,PC会指向multstore函数的首地址即0x400540。ret返回后,会弹出一个地址返回到主函数接下来要执行的指令,即位于0x400568的指令。
即使是多个过程的嵌套调用,也就是不断增加我们的“栈帧”长度即可,分析起来并不会很复杂。
数据传递
我们之前看过寄存器图,我们知道用寄存器传参的话最多只能传6个,按序:
%rdi、%rsi、%rdx、%rcx、%r8、%r9。
显然这不能满足所有情况,所以假设我们还需要更多参数的话需要将其压入“栈”中。
所以在被调用函数里,不要急着先为自己程序开辟空间,将栈顶指针减去一个值。
可以先在栈顶指针不变的情况下将多余的需要用到的参数先取出到相应寄存器中。
注意进入被调用函数栈帧结构且未对栈顶指针做任何变动时,直接取栈顶指针数据应该会得到返回地址,因为这才是最后被保存的。
书上有个例子:
这是一张很有个性的图
从C源代码看出传递了8个参数,再看汇编,最后两个参数的取值我们分别在%rsp + 8和+ 16的地方获得。
因为调用者的栈帧结构尾部长这样:
寄存器的局部存储空间
在栈帧结构下我们有一些惯例。
寄存器%rbx、%rbp、%r12~%r15都属于被调用者保存的寄存器。
就是比如P调用Q了,那么Q要实现保存好这些寄存器。有效的一个办法就是将这些寄存器的原值压栈,然后到时候Q结束后弹出这些值,就可以做到返回时P保留的还是原值。
更重要的是这些寄存器就可以被Q所使用,否则不就是“占着茅坑不拉屎”吗!
除了上述寄存器,还有我们的特殊的寄存器栈顶指针%rsp,都属于调用者保存类型的。就是P调用Q前需要P自己保管好的。
递归
过程调用一种较为特殊的情况就是自己调用自己,我们称为递归。
一个比较经典的例子就是阶乘程序。ppt里给出了一个稍微复杂一些的例子,如下图所示:
我们需要先测试自己的参数x是否为0,如果为0,就会以%eax直接为0进行返回。
否则的话我们会先将%rbx进行保存,相当于是被调用者来保存。
后面我们会看到%rbx始终保存x & 1。
所以我们保存的就是调用者的%rbx。
先将x搬运到%rbx中,然后进行一个与运算。其实就是已经把结果x & 1放入%rbx中了。
进行一个逻辑右移处理,%rdi继续作为参数递归调用函数。
然后注意一下,返回的时候,会执行到当前的addq指令,将结果加在返回值%rax中。
在此之前我们要恢复上一个栈帧结构,也就是调用自己的栈帧的%rbx值。
最后返回rax,然后继续执行调用自己的栈帧的addq指令。
所以递归里很重要的一个步骤就是,被调用者要保存%rbx,其实就是其实就是把上一个过程的x & 1保存在栈中。当被调用者自己要结束时会恢复这个值,因为调用者,也就是自己的上一帧还有用。
递归这玩意儿还真是说不明白,还是自己多手动调试看看,比如代入个参数自己把流程走一遍可能就全懂了。
缓冲区溢出
我们在写代码时常常会碰到一个名为“Segementation fault”的错误。
这通常是因为越界引起的,比如我们声明A[5]的数组,实际访问下标范围是0~4。
超过的话就是刚刚说的越界错误了。
这样的问题不仅仅是我们这样的“代码小白”会犯的,即使是“大牛”们也会欠缺考虑。
比较典型的就是gets函数,我们在C语言中使用它来获取字符串。
我们来看看gets的源码:
/* Get string from stdin */
char *gets(char *dest) int c = getchar();
char *p = dest;
while (c != EOF && c != '\\n')
*p++ = c;
c = getchar();
*p = '\\0';
return dest;
代码很好懂就是把你敲入的字符一个个写入指针对应的内存中去,指针往后移。
那么很明显地,其并没有考虑我们传入的参数char* dest的长度。
报错的话真的应该谢天谢地了,否则的话就会引发更严重的错误,就是我们的缓冲区溢出攻击。
所以我们再来看到一张比较简单的图:
栈结构包含了两个“栈帧”,先前的栈帧是分配给“call_echo”函数的。
对于echo函数,我们用栈顶指针减去24开辟出一块内存。
同时将新的栈顶指针作为char数组首地址给到%rdi执行gets操作。
下图是不会有问题的情况:
即虽然我们char数组大小只有4,但因为中间有20Byte的无用空间,即使你多读入这20Byte也不会有太大的影响。
但如果你的输入比24Byte还要长呢?
太长就会修改上一个栈帧的返回地址啊!!
这就很严重了,如果黑客写好自己的一个代码段注入,将返回地址修改到自己的代码段起始位置,那不就被他攻击了么。
下图就是这么一个道理:
通过gets写入攻击代码,当然得是机器级别的,即可以是16进制的。
写完代码继续往上写,修改返回地址到攻击代码起始位置,完成攻击。
保护措施
采用带保护的库函数。
可以用fgets替代gets。
可以用是strncpy替代strcpy……
提供系统级的保护:
还有一种较好的方案就是设置“栈金丝雀”。
我们在程序初始时在栈的任意位置(肯定是越过局部缓冲区的,否则正常读写都得被破坏了)设置一个canary值(随机的)。
我们在程序运行前后就去观察其是否被改变,是的话就异常退出。
当然了如果攻击者知道这个值,抑或是运气好碰着了那也起不到保护作用。
这题不是很确定,等老师讲完再来更一波
看一道缓冲区溢出攻击+栈金丝雀的题吧。
观察代码显然是有检测能力的,我们会事先将存储在%fs:40的金丝雀值取出。
而在gets和puts完毕后,我们再次取出做异或操作,进行检查。
因为38位置开始存放了共16Byte的数据,后面的8Byte数据会给到%fs:40并被放入到%rsp+8的位置,那么应该是:
08 09 0a 0b 0c 0d 0e 0f
如果是abcdefg,是7个字符刚好7Byte,补一个“\\0”就是8Byte。
刚好不会影响到(%rsp+8)的值,那么不会报错。
会正常输出“abcdefg”。
而如果是123456789,共9Byte,外加“\\0”就是10Byte。
显然会影响到%rsp+8即保存原先金丝雀值的位置。
不能正常返回的话就是执行那个_stack_chk_fail了。
浮点代码
用到了一套新的寄存器:
浮点数的数据传送指令+例子:
可以理解为s为single,d则为double,对应于单双精度
还有类似这样的指令:
关于浮点运算也有一套自己的指令:
同样看一个例子:
题:
8 第三章 函数参数+浮点
对于一下汇编代码,请写出对应的C函数代码(整数参数请使用a/b,浮点参数请使用c)
myfun:
movsbl %dil, %edi
imull $30, %edi, %edi
addl (%rsi), %edi
movl %edi, (%rsi)
cvtsi2ss %edi, %xmm1
addss %xmm1, %xmm0
ret
float myfun(float c, int a, int* b)
a *= 30;
a += *b;
*b = a;
float d = (float)a;
c += d;
return c;
以上是关于CSAPP-Revision-ch03---浮点代码目前不知道到底考不考的主要内容,如果未能解决你的问题,请参考以下文章