CSAPP基本版实验学习日志:关于struct代码的阅读以及在ubantu上的运行

struct(关于结构体可能越界)代码内容:

#include <stdio.h>
#include <stdlib.h>

typedef struct {
    int a[2];
    double d;
} struct_t;

double fun(int i) {
    volatile struct_t s;
    s.d = 3.14;
    s.a[i] = 1073741824; /* Possibly out of bounds */
    return s.d; /* Should be 3.14 */
}

int main(int argc, char *argv[]) {
    int i = 0;
    if (argc >= 2)
	i = atoi(argv[1]);
    double d = fun(i);
    printf("fun(%d) --> %.10f\n", i, d);
    return 0;
}

下面我将分别解释:

#include <stdio.h>
#include <stdlib.h>

**#include<stdio.h>是编译预处理命令,即在程序编译之前要处理的内容,<>中以以“.h ”作为结尾的文件称为头文件,如stdio.h,stdlib.h,string.h均为头文件。
stdio 即 “standard input & output"(标准输入输出)在开头加上#include <stdio.h>即可实现c语言中的 输入函数printf、输出函数scanf等函数。
同理,在开头加上#include <stdlib.h>即可实现c语言中的exitmallocfree等函数。
同样的,在开头加上#include <string.h>即可实现关于字符数组的函数,例如连接字符串函数strcat,复制字符串函数strcpy,比较字符串(区分大小写)函数strcmp等。
类似头文件还有math.h(包括各种常用的三角函数、双曲线函数、指数和对数函数等)等。

typedef struct {
	int a[2];
	double d;
} struct_t;

C语言提供了两种将不同类型的对象组合到一起创建数据类型的机制:结构(struct),用关键字struct来声明,将多个对象集合到一个单位中;联合(union),用关键字union来声明,允许用几种不同的类型来引用一个对象。
C语言的struct声明创建了一个数据类型,将可能不同类型的对象聚合到一个对象中。用名字来引用结构的各个组成部分。结构的所有组成部分都存放在内存中一段连续的区域内,而指向结构的指针就是结构第一个字节的地址。编译器维护关于每个结构类型的信息,指示每个字段的字节偏移。它以这些偏移作为内存引用指令中的位移,从而产生对结构元素的引用。
开头的typedef意为给类型起一个别名。起别名的目的不是为了提高程序运行效率,而是为了编码方便。这里用typedef将结构体
struct {
int a[2];
double d;
}
命名为 struct_t,在程序中即可直接使用。
这里定义了一个包括两个字段:一个类型为整型(int)的元素组成的数组,变量名为a和一个8字节的浮点型,变量名为d。
整型变量(int),在内存中占4个字节,最长能到32位二进制位,其取值为基本整常数。
数组是由若干类型相同的相关数据项按顺序存储在一起形成的一组同类型有序数据的集合。注意数组下标从0开始,a[2]也就是定义了2长度的数组。其元素分别存在a[0]到a[1]中。由于该数组定义为整型,则它有2个元素,每个元素均为整型。每个整型元素占4个字节(bytes)
数组还有许多扩展知识,可参考:关于数组
浮点数以浮点形式表示,即小数点位置可以是浮动的,最长能到64位二进制位。 double,可参考:​float和double的区别

补充:关于c语言基本数据类型,除了float,double,char和int,还有short,long,可参考c语言基本数据类型short、int、long、char、float、double

double fun(int i) {

定义一个返回值类型为浮点数的函数,函数名为fun,括号里参数i的类型为整型,参数i为形参,主函数输入的实参必须与形参类型匹配,注意实参与形参有各自的存储空间,如不使用指针则形参值的改变不会影响实参。

	volatile struct_t s;

volatile意为“易变的”,volatile 关键字和const 一样是一种类型修饰符,用它修饰的变量表示可以被某些编译器未知的因素更改,比如操作系统、硬件或者其它线程等。遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问。
该行程序的意思为定义一个struct_t类型变量,变量名为s,其中s值取当下最新值,既让编译器其值不断变化,不要急于优化。

	s.d = 3.14;
	s.a[i] = 1073741824; /* Possibly out of bounds */

这里表达式s.d和s.a[i]就会分别选择s的d字段和a[i]字段。并分别为其赋值为3.14和1073741824。1073741824为2的30次方。/ * 符号和 * / 符号:里面内容为C语言的注释,不会被运行,英文意思为可能越界。

	return s.d; /* Should be 3.14 */
}

返回s.d的值。return 用于实现函数值的返回,将s.d返回到主函数。关于return

int main(int argc, char *argv[]) {

程序的主函数,有main()函数的程序才能运行,即函数必须被main()直接或间接调用才能发挥作用,主函数的返回值类型为int(整型)。其中,main函数可以带参数,这个参数可以认为是main函数的形式参数。C语言规定main函数的参数只能有两个,习惯上这两个参数写为argc和argv。可参考关于main函数参数int main(int argc,char* argv[])详解
C语言还规定argc(第一个形参)必须是整型变量,argv(第二个形参)必须是指向字符串的指针数组。
argc是命令行总的参数个数,argv[]是argc个参数,其中第0个参数argv[0]指向程序运行的全路径名,以后的参数为命令行后面跟的用户输入的参数。
通过阅读后续代码,可以发现它利用了这些参数间关系。

	int i = 0;

定义一个整型变量(int),整型变量,在内存中占4个字节,最长能到32位二进制位,其取值为基本整常数,在该定义中,变量名为i,并为其赋值为0。

	if (argc >= 2)

一个条件语句,如果参数个数大于等于2,即用户除./a.out外输入了另外的参数则执行,否则不执行下面语句。

	i = atoi(argv[1]);

atoi函数用来将字符串转换成整数(int),它的头文件为#include <stdlib.h>。atoi函数会扫描参数 str 字符串,跳过前面的空白字符(例如空格,tab缩进等,可以通过 isspace() 函数来检测),直到遇上数字或正负符号才开始做转换,而再遇到非数字或字符串结束时(’\0’)才结束转换,并将结果返回。更多可参考:C语言atoi()函数
在这里则是将argv数组的第二个元素,即用户输入的第一个参数做为函数的参数,并将转换来的整型数赋给i,令i不再为0而为用户输入的第一个参数。

		double d = fun(i);

定义一个浮点型变量,变量名为d,并为其赋值为函数fun(i)的返回值。注意这里的d与结构体中的d是同名的两个变量。

		printf("fun(%d) --> %.10f\n", i, d);

printf函数是一个标准库函数,它的函数原型在头文件stdio.h中。printf函数调用的一般形式为:printf(“格式控制字符串”, 输出表列)。printf是将一个格式化的字符串输出到屏幕。更多可参考:C语言格式输出函数printf()详解
这里将打印出“fun(i的值) --> d的值”并换行。
%d:输出带符号的十进制整数,正数的符号省略。%f:以十进制小数形式输出实数(包括单、双精度),整数部分全部输出,隐含输出6位小数。由于是%.10f,则代表输出10位小数。而%10f意思是输出宽度占10个位置,右对齐。
这里出现了一个转义字符,’\n’是一个转义字符,它是换行符,即实现回车换行的功能。其余类似转义字符还有’\t’,它的功能是横向跳到下一制表位置。
关于转义字符:C语言字符型数据(字符)

	return 0;
}

主函数返回0表程序结束。
以下为该代码在ubantu上运行结果:

gec@ubuntu:/mnt/hgfs/share/csapp_code$ ./a.out 0
fun(0) --> 3.1400000000
gec@ubuntu:/mnt/hgfs/share/csapp_code$ ./a.out 1
fun(1) --> 3.1400000000
gec@ubuntu:/mnt/hgfs/share/csapp_code$ ./a.out 2
fun(2) --> 3.1399998665
gec@ubuntu:/mnt/hgfs/share/csapp_code$ ./a.out 3
fun(3) --> 2.0000006104
gec@ubuntu:/mnt/hgfs/share/csapp_code$ ./a.out 4
fun(4) --> 3.1400000000
段错误 (核心已转储)

这是一个栈溢出程序。结构体中定义的变量存放在栈中,并由栈的地址由低到高存放了a[0]、a[1]、d低4字节、d高4字节。
当输入参数为0时,即令a[0]赋为1073741824,不影响d,打印出d为正常的3.1400000000。
当输入参数为1时,即令a[1]赋为1073741824,不影响d,打印出d为正常的3.1400000000。
当输入参数为2时,按照结构体在栈中的存储位置,输入的参数不会为a[0]或a[1]赋值而影响了浮点数d的二进制低32位。由于浮点数表示中后几位为小数位,故最终d只是小数位被影响了,但其值仍接近3.14。
当输入参数为3时,按照结构体在栈中的存储位置,输入的参数不会为a[0]或a[1]赋值而影响了浮点数d的二进制高32位。由于浮点数表示中前几位包含符号位与阶码(小数点前的实数位),故最终d结果被影响得比较大,但其值以远离3.14。
当输入参数为4时,按照结构体在栈中的存储位置,输入的参数不会为a[0]或a[1]赋值,也不会影响浮点数d的数值。因此输出d为正常的3.1400000000。但在栈中,比d高的地址存放的值为未知数,可能是未被利用的空间,也可能存放了重要的数据,也可能是返回地址,因此导致了栈溢出。这个程序深刻体现了结构体无法对栈的边界检测的缺点。
若想在输入参数2和3时d仍能正确输出,将结构体中d与a数组换下位置即可。

附图:计算机中堆栈的结构,图源读薄 CSAPP
在这里插入图片描述
博客参考:《深入理解计算机系统》第三章

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值