西工大CSAPP第二章课后题2.55答案及解析
因为我获取并阅读CSAPP电子书的方式是通过第三方网站免费下载,没有付给原书作者相应的报酬,遵循价值交换原则,我会尽我所能通过博客的方式,推广这本书以及原书作者就职的大学,以此回馈原书作者的劳动成果。另外,由于西工大让我能够不认真听课、不好好写作业、糊弄考试还能过,有了很多很多时间做自己认为对社会有价值的事情,所以感谢西工大对我的宽容与支持。
2.55 在你能接触到的不同机器上,使用show-bytes.c文件中的show-bytes,编译并且运行样例代码,决定被这些机器所使用的字节顺序。
首先我们要获取到show-bytes.c文件,在Edge浏览器中输入“CSAPP”,发现第一个网站就是Carnegie Mellon University的官网,网址是CS:APP3e, Bryant and O'Hallaron,这本书就是被这所大学的几个教授联合编写的。单击进入网站后,单击选择“Student Site”,Student Site汇总了作为学生的读者为了帮助理解原书中的概念、原理、解决方案、题目、实验材料等,所需要的所有线上材料。进入Student Site之后,单击选择”Material from the CS:APP Textbook“标题下面的“Code examples”,"Material from the CS:APP Textbook"囊括了CS:APP这本书中,所有可能需要的电子版材料,"Code examples"囊括可CS:APP这本书中,所有可能需要用到的代码文件,2.55题目中所要求的"show-bytes.c"文件就处在"Code examples"中。进入"Code examples"之后,浏览内容,就能找到"show-bytes.c"文件,单击定位到的“show-bytes”后,我们就能看到"show-bytes.c"的内容。如果你发现,这时候界面网址是:csapp.cs.cmu.edu/3e/ics3/code/data/show-bytes.c,就说明你成功找到了"show-bytes.c"文件。复制页面中的所有内容,并将它粘贴到任意的记事本文件中。为了帮助理解"show-bytes.c"文件中各部分代码的功能,"show-bytes.c"文件的内容将被显示在下面:
/* $begin show-bytes */
#include <stdio.h>
/* $end show-bytes */
#include <stdlib.h>
#include <string.h>
/* $begin show-bytes */
typedef unsigned char *byte_pointer;
void show_bytes(byte_pointer start, size_t len) {
size_t i;
for (i = 0; i < len; i++)
printf(" %.2x", start[i]); //line:data:show_bytes_printf
printf("\n");
}
void show_int(int x) {
show_bytes((byte_pointer) &x, sizeof(int)); //line:data:show_bytes_amp1
}
void show_float(float x) {
show_bytes((byte_pointer) &x, sizeof(float)); //line:data:show_bytes_amp2
}
void show_pointer(void *x) {
show_bytes((byte_pointer) &x, sizeof(void *)); //line:data:show_bytes_amp3
}
/* $end show-bytes */
/* $begin test-show-bytes */
void test_show_bytes(int val) {
int ival = val;
float fval = (float) ival;
int *pval = &ival;
show_int(ival);
show_float(fval);
show_pointer(pval);
}
/* $end test-show-bytes */
void simple_show_a() {
/* $begin simple-show-a */
int val = 0x87654321;
byte_pointer valp = (byte_pointer) &val;
show_bytes(valp, 1); /* A. */
show_bytes(valp, 2); /* B. */
show_bytes(valp, 3); /* C. */
/* $end simple-show-a */
}
void simple_show_b() {
/* $begin simple-show-b */
int val = 0x12345678;
byte_pointer valp = (byte_pointer) &val;
show_bytes(valp, 1); /* A. */
show_bytes(valp, 2); /* B. */
show_bytes(valp, 3); /* C. */
/* $end simple-show-b */
}
void float_eg() {
int x = 3490593;
float f = (float) x;
printf("For x = %d\n", x);
show_int(x);
show_float(f);
x = 3510593;
f = (float) x;
printf("For x = %d\n", x);
show_int(x);
show_float(f);
}
void string_ueg() {
/* $begin show-ustring */
const char *s = "ABCDEF";
show_bytes((byte_pointer) s, strlen(s));
/* $end show-ustring */
}
void string_leg() {
/* $begin show-lstring */
const char *s = "abcdef";
show_bytes((byte_pointer) s, strlen(s));
/* $end show-lstring */
}
void show_twocomp()
{
/* $begin show-twocomp */
short x = 12345;
short mx = -x;
show_bytes((byte_pointer) &x, sizeof(short));
show_bytes((byte_pointer) &mx, sizeof(short));
/* $end show-twocomp */
}
int main(int argc, char *argv[])
{
int val = 12345;
if (argc > 1) {
if (argc > 1) {
val = strtol(argv[1], NULL, 0);
}
printf("calling test_show_bytes\n");
test_show_bytes(val);
} else {
printf("calling show_twocomp\n");
show_twocomp();
printf("Calling simple_show_a\n");
simple_show_a();
printf("Calling simple_show_b\n");
simple_show_b();
printf("Calling float_eg\n");
float_eg();
printf("Calling string_ueg\n");
string_ueg();
printf("Calling string_leg\n");
string_leg();
}
return 0;
}
接下来读题目。题目要求,这个代码应该被运行在多种机器上。考虑到目前市面上个人用计算机的CPU生产厂家几乎都是Intel,而适配Intel生产的CPU的机器几乎都是用一样的字节顺序,所以我们没有办法在使用不同字节顺序的机器上运行"show-bytes.c"代码。那么,在我们的运行Windows操作系统的个人用计算机上,打开Powershell并且进入到粘贴有"show-bytes.c"代码的txt文件的目录中,使用gcc编译"show-bytes.c"文件以得到"show-bytes.exe"可执行文件,接着执行该可执行文件,得到了如下图所示的运行结果:
PS D:\C> dir -Filter a10292023.c
Directory: D:\C
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a--- 10/29/2023 10:31 AM 2763 a10292023.cPS D:\C> gcc -o a10292023 a10292023.c
PS D:\C> a10292023.exe
calling show_twocomp
39 30
c7 cf
Calling simple_show_a
21
21 43
21 43 65
Calling simple_show_b
78
78 56
78 56 34
Calling float_eg
For x = 3490593
21 43 35 00
84 0c 55 4a
For x = 3510593
41 91 35 00
04 45 56 4a
Calling string_ueg
41 42 43 44 45 46
Calling string_leg
61 62 63 64 65 66
在Powershell中,"dir"命令使得当前目录下的所有文件被输出在Powershell中,配合使用"-Filter"选项以及"a10292023.c"参数,过滤掉"dir"命令的输出结果,使得当前目录下只有名为"a10292023.c"的文件才会被输出在Powershell中(被执行粘贴操作,并且保存有"show-bytes.c "文件内容的记事本文件的名称是"a10292023.c")。通过这条命令,我们确保"a10292023.c"文件就处在当前的工作目录下,这样子在使用gcc命令编译文件时,就不会出现文件未找到的错误。接着使用"gcc -o a10292023 a10292023.c"命令编译"a10292023.c"文件,其中"-o"选项以及"a10292023"参数表明将要被生成的可执行文件的名称是"a10292023",但考虑到Windows系统为了帮助使用者区分不同文件类型,会自动在文件后添加相应的文件后缀,比如".msi"、".iso"、".exe",所以将要被生成的可执行文件的名称是"a10292023.exe"。接下来我们运行该可执行文件。之后便得到了上图的结果。
为了确定该机器使用的字节顺序,我们需要参照代码执行的结果。但是为了更好理解代码执行的结果,我们仍需要参考源文件中的主函数部分。在"a10292023.c"文件中,"int main(int argc, char* argv[])"表示定义一个main函数,main函数的返回值是int类型。它有两个参数,一个是int型的变量argc,另一个是数组argv,数组argv中的元素都是指针,指向字符串。当Powershell中的命令是"PS D:\C> a10292023.exe"时,argc的值为1,argv[0]的值为“a10292023.exe。当Powershell中的命令是“PS D:\C> a10292023.exe 0x80000000”时,argc的值为2,argv[0]的值为"a10292023.exe",argv[1]的值为"0x80000000"。"int val=12345;"一个有符号整形变量val被定义并且初始化,val的值被初始化为12345。接着判断"argc>1",如果argc大于1,那么就意味着,在Powershell中输入"a10292023.exe"时,后面还添加了一个参数,而且这个参数已经以字符串的形式被保存在argv[1]字符数组中。在这里"if(argc>1)"出现了两次,我认为是错误的。考虑到我们初次运行"a10292023.exe"文件,遵循从简到繁的原则,先不带参数运行"a10292023.exe",所以我们跳过"argc>1"时的情况,转而去看else下的情况。在else中,"printf("calling show_twocomp\n");"在Powershell中输出"calling show_twocomp"这句话,"show_twocomp();"表示,调用"show_twocomp()"函数。"printf("Calling simple_show_a\n");"在Powershell中输出"Calling simple_show_a"这句话,"simple_show_a();"表示,调用"simple_show_a()"函数,"printf("Calling simple_show_b\n");"表示在Powershell中输出"Calling simple_show_b"这句话,"simple_show_b();"表示调用"simple_show_b()"函数,"printf("Calling float_eg\n");"表示在Powershell中输出"Calling float_eg"这句话,"float_eg();"表示调用"simple_show_b"函数。剩下的4句执行类似的操作。我们接下来看这6个函数:
首先是"show_twocomp()"函数,"void show_twocomp()"表示"show_twocomp()"函数是一个既没有返回值,又没有参数的函数。忽略掉"/* $begin show-twocomp */"注释,"short x = 12345;"表示一个有符号短整型变量x被定义,并且变量x的值被初始化为12345,"short mx = -x;"表示一个有符号短整型变量mx被定义,并且变量mx的值被初始化为-12345。"show_bytes((byte_pointer) &x, sizeof(short));"表示调用show_bytes函数,并将变量x的地址、有符号短整型变量的长度(以字节为单位)这两个参数传给show_bytes函数。考虑到show_bytes函数将会在整个程序中被频繁用到,将show_bytes函数的内容显示在下面并辅以讲解:
void show_bytes(byte_pointer start, size_t len) {
size_t i;
for (i = 0; i < len; i++)
printf(" %.2x", start[i]); //line:data:show_bytes_printf
printf("\n");
}
"void show_bytes(byte_pointer start, size_t len)"表示"show_bytes"函数是一个没有返回值的函数,并且需要两个参数作为输入,一个是byte_pointer类型的变量"start",另一个是"size_t"类型的变量"len"。在程序的开头有一个宏"typedef unsigned char *byte_pointer;",意思是byte_pointer类型的变量的值,将会是无符号字符型变量的地址。"size_t"是一个无符号整型的数据类型,至于"size_t"类型的变量的长度,则将取决于具体的机器。"size_t i"表示定义一个"size_t"类型的变量i,但却不对变量i的值进行初始化。"for (i = 0; i < len; i++)"表示,循环len-0=len次。"printf(" %.2x", start[i]);"表示,在每一次循环中,都输出"start[i]"。"%.2x"中的".2"表示,至少输出2个字符,"x"表示以16进制形式输出。最后"printf("\n");"表示另起一行。
回到6个函数中的第1个函数"show_twocomp()","show_twocomp()"函数中的语句"show_bytes((byte_pointer) &x, sizeof(short));"表示调用show_bytes函数,并将变量x的地址、有符号短整型变量的长度(以字节为单位)这两个参数传给show_bytes函数。而变量x的值是12345,或者是0x3039,现在市面上普遍使用的笔记本电脑都使用64位Intel的CPU,对应的short类型变量的长度都是2字节,所以"sizeof(short)"的值是2。但是变量x在内存中的地址未知,不妨假设变量x在内存中的占据的空间的地址从0x004005f4开始,考虑到变量x的数据类型是short,占据2个字节,所以变量x在内存中占据的空间的地址在0x004005f5结束。进入show_bytes()函数,变量start的值是0x004005f4,变量len的值是2。在第一次循环中,printf将以无符号16进制整型的形式输出start[0]的值,start[0]=*(start+0)=*start,输出start[0]的值,意味着将输出内存中地址为0x004005f4的值。第二次循环中,printf同样以无符号16进制整型的形式输出start[1]的值,start[1]=*(start+1),输出start[1]的值,意味着将输出内存中地址为0x004005f5的值。至此循环结束。运行的结果是"39 30",这就表明内存中地址为0x004005f4的空间存储的是0x39,内存中地址为0x004005f5的空间存储的是0x30,即低地址存储低有效位,高地址存储高有效位,这就是小段的字节顺序。为什么printf以无符号16进制整型的形式输出一个变量的值,就能够输出这个变量在内存中分配到的空间中的值呢?因为无符号16进制整型数和机器数的对应关系是最简单最直观的,相比于有符号16进制整型数和机器数、浮点数和机器数而言。接着函数"show_twocomp()"中的语句"show_bytes((byte_pointer) &mx, sizeof(short));"接着调用show_bytes函数,同时将有符号短整型变量mx在内存中的地址、short类型的变量的大小这两个参数传给show_bytes函数。在Windows系统使用计算器,切换到程序员模式,在十进制模式输入-12345,然后切换到十六进制模式,得知-12345的用两个字节的十六进制表示是:0xcf c7。但是在Powershell中运行a10292023.exe,发现0xcfc7中,低位字节0xc7处在内存中的低地址,高位字节0xcf处在内存中的高地址。所以我们得知,被市面上大部分适配Intel64位CPU的笔记本采用的字节顺序是小端。
接着看6个函数中的第2个函数,"simple_show_a()","void simple_show_a()"表示"simple_show_a"函数没有返回值,并且也没有参数作为输入。"int val = 0x87654321;"表示定义一个有符号整型变量val,并将变量val的值初始化为0x87654321。"byte_pointer valp = (byte_pointer) &val;"表示定义一个byte_pointer类型的变量valp,指向变量val。"show_bytes(valp, 1); /* A. */"表示将valp和1作为参数,传给show_bytes函数。"show_bytes(valp, 2); /* B. */"与"show_bytes(valp, 3); /* C. */"同理。分析输出结果,我们不难得知,被市面上大部分适配Intel64位CPU的笔记本采用的字节顺序是小端。
6个函数中的第3个函数,"simple_show_b()",除了有符号整型变量val的值被初始化为0x12345678之外,与第2个函数相同。
6个函数中的第4个函数,"float_eg()","int x = 3490593;"定义一个有符号整型变量x,并初始化变量x的值为3490593,用16进制形式可以表示为0x00354321,遵循IEEE的相关规定,变量x在内存中的值也为0x00354321。"float f = (float) x;"定义一个单精度浮点型变量f,并且初始化f的值为x,遵循IEEE对单精度浮点数格式的要求,得知变量f在内存中的值为:0x4a550c84。"printf("For x = %d\n", x); show_int(x); show_float(f);"在Powershell中输出"For x = 3490593"之后,调用"show_int()"和"show_float()"函数。最终在Powershell中,通过输出结果,我们能够得知,被市面上大部分适配Intel64位CPU的笔记本采用的字节顺序是小端。
6个函数中的第5、6个函数,需要注意的是,不要将数组中的元素在内存中的存放顺序与数组的索引之间的关系,和应用于整型、短整型、长整型、长长整型变量的字节顺序搞混。在数组中,元素的索引值小的将被存放在低地址,元素的索引值大的将被存放在高地址。
综上所述,我们得知,被市面上大部分适配Intel64位CPU的笔记本采用的字节顺序是小端。