发现了一个收集C puzzle的页面,挺有意思的,都看了一遍。除去那些常见的Marco、类型转换、printf格式符、指针和内存之类的常见puzzle外,我发现以下一个puzzle很是值得想一想。
问题1
该程序的执行结果有些怪异:在终端上只看到“hello-err"一个接一个的输出,过了一段时间后突然输出一连串的“hello-out",然后又是只输出“hello-err“,如是反复。
这里需要了解的是ISO C中对于流的缓冲的相关规定。ISO C为标准I/O提供了三种类型的缓冲:
全缓冲:缓冲区填满后才执行实际I/O操作
行缓冲:在输入和输出中遇到换行符是,执行I/O操作
不带缓冲:不启用缓冲机制
而对于stdout,stdin和stderr这三个默认提供的流,ISO C 有如下要求
(1). 当且仅当stdout和stdin不涉及交互式设备时,才允许是全缓冲的
(2). stderr决不会是全缓冲的(通常是不缓冲的)
OK,现在这个程序的怪异表现就很容易解释了。
问题2
这个问题很好。估计多数人最初都往野指针或是类型错误上面想,可是再仔细看看又找不出漏洞。究竟问题处在何处?
问题在于:应该加上“#include <stdlib.h> ",这才是符合标准的调用malloc的必要前提。
在目前的ISO C标准中,仍然延续了从K&R C继承而来的”默认int类型“的特性。在未提供function prototype的情况下,编译器会假设malloc的参数和返回类型都是int。
在 IA-32 机器上,因为int和 void *都是32bits ,因此不会出现问题。而在 IA-64机器上,默认的内存模型是LP-64,即int为32bits而long和void *都为64bits。因此malloc的返回值在缺少prototype的时候被截断(truncated)为32字节,自然会出现segment error
教训:无论何时,都要保证在调用函数前已经正确声明其prototype。
问题3
程序的运行结果是
这个问题的实质在于了解在调用诸如类似printf这样具有可变参数的函数时,编译器会隐式进行类型提升——所有整数类型(int ,short)都被提升为int,所有浮点类型都被提升为double,再将参数压栈传给printf。
因此对于 “printf("%d ", a);”这条调用,float a会被提升为double。通过debugger我们可以看到a的内存布局为:
0x00 0x00 0x48 0x41,
而提升后对应的(临时)double变量的内存布局为
0x00 0x00 0x00 0x00 0x00 0x00 0x29 0x40
这样问题就清楚了,printf根据格式说明符%d从栈中提取一个int类型大小的参数,那么得到的就是0了
问题4
这个问题严格的说与C什么关系,关键在于linker的符号解析过程。
编译时,编译器将每个全局符号都输出给汇编器,其中函数和已初始化的变量为强符号,未初始化的符号为若符号;汇编器再将这些信息编码在可重定位目标文件的符号表中。
根据强弱符号的定义,unix链接其根据如下规则来处理多出定义的符号:
(1).不允许有多个强符号。
(2).如果有一个强符号的多个弱符号,选择强符号
(3).如果没有强符号而有多个弱符号,则从这些弱符号中任意选一个
该例子遵循了了上述规则的中的第一条
问题1
The following program doesn't "seem" to print "hello-out". (Try executing it)
#include <stdio.h>
#include <unistd.h>
int main()
{
while(1)
{
fprintf(stdout,"hello-out");
fprintf(stderr,"hello-err");
sleep(1);
}
return 0;
}
What could be the reason?
#include <stdio.h>
#include <unistd.h>
int main()
{
while(1)
{
fprintf(stdout,"hello-out");
fprintf(stderr,"hello-err");
sleep(1);
}
return 0;
}
What could be the reason?
该程序的执行结果有些怪异:在终端上只看到“hello-err"一个接一个的输出,过了一段时间后突然输出一连串的“hello-out",然后又是只输出“hello-err“,如是反复。
这里需要了解的是ISO C中对于流的缓冲的相关规定。ISO C为标准I/O提供了三种类型的缓冲:
全缓冲:缓冲区填满后才执行实际I/O操作
行缓冲:在输入和输出中遇到换行符是,执行I/O操作
不带缓冲:不启用缓冲机制
而对于stdout,stdin和stderr这三个默认提供的流,ISO C 有如下要求
(1). 当且仅当stdout和stdin不涉及交互式设备时,才允许是全缓冲的
(2). stderr决不会是全缓冲的(通常是不缓冲的)
OK,现在这个程序的怪异表现就很容易解释了。
问题2
The following C program segfaults of IA-64, but works fine on IA-32.
int main()
{
int* p;
p = (int*)malloc(sizeof(int));
*p = 10;
return 0;
}
Why does it happen so?
int main()
{
int* p;
p = (int*)malloc(sizeof(int));
*p = 10;
return 0;
}
Why does it happen so?
这个问题很好。估计多数人最初都往野指针或是类型错误上面想,可是再仔细看看又找不出漏洞。究竟问题处在何处?
问题在于:应该加上“#include <stdlib.h> ",这才是符合标准的调用malloc的必要前提。
在目前的ISO C标准中,仍然延续了从K&R C继承而来的”默认int类型“的特性。在未提供function prototype的情况下,编译器会假设malloc的参数和返回类型都是int。
在 IA-32 机器上,因为int和 void *都是32bits ,因此不会出现问题。而在 IA-64机器上,默认的内存模型是LP-64,即int为32bits而long和void *都为64bits。因此malloc的返回值在缺少prototype的时候被截断(truncated)为32字节,自然会出现segment error
教训:无论何时,都要保证在调用函数前已经正确声明其prototype。
问题3
What
'
s the output of the following program and why?
#include < stdio.h >
int main()
{
float a = 12.5;
printf("%d/n ", a);
printf("%d/n " *(int *)&a);
return 0;
}
#include < stdio.h >
int main()
{
float a = 12.5;
printf("%d/n ", a);
printf("%d/n " *(int *)&a);
return 0;
}
程序的运行结果是
0
1095237632
1095237632
这个问题的实质在于了解在调用诸如类似printf这样具有可变参数的函数时,编译器会隐式进行类型提升——所有整数类型(int ,short)都被提升为int,所有浮点类型都被提升为double,再将参数压栈传给printf。
因此对于 “printf("%d ", a);”这条调用,float a会被提升为double。通过debugger我们可以看到a的内存布局为:
0x00 0x00 0x48 0x41,
而提升后对应的(临时)double变量的内存布局为
0x00 0x00 0x00 0x00 0x00 0x00 0x29 0x40
这样问题就清楚了,printf根据格式说明符%d从栈中提取一个int类型大小的参数,那么得到的就是0了
问题4
Note : This question has more to
do
with Linker than C language
We have three files a.c, b.c and main.c respectively as follows:
a.c
---
int a;
b.c
---
int a = 10 ;
main.c
------
extern int a;
int main()
{
printf("a = %d ",a);
return 0;
}
Let ' s see what happens, when the files are compiled together:
bash$ gcc a.c b.c main.c
bash$ . / a. out
a = 10
Hmm !! no compilation / linker error !!! Why is it so ??
We have three files a.c, b.c and main.c respectively as follows:
a.c
---
int a;
b.c
---
int a = 10 ;
main.c
------
extern int a;
int main()
{
printf("a = %d ",a);
return 0;
}
Let ' s see what happens, when the files are compiled together:
bash$ gcc a.c b.c main.c
bash$ . / a. out
a = 10
Hmm !! no compilation / linker error !!! Why is it so ??
这个问题严格的说与C什么关系,关键在于linker的符号解析过程。
编译时,编译器将每个全局符号都输出给汇编器,其中函数和已初始化的变量为强符号,未初始化的符号为若符号;汇编器再将这些信息编码在可重定位目标文件的符号表中。
根据强弱符号的定义,unix链接其根据如下规则来处理多出定义的符号:
(1).不允许有多个强符号。
(2).如果有一个强符号的多个弱符号,选择强符号
(3).如果没有强符号而有多个弱符号,则从这些弱符号中任意选一个
该例子遵循了了上述规则的中的第一条