#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
void sigfunc(int sig) {
printf("recv a signal %d\n", sig);
abort();
}
int main(void) {
signal(SIGSEGV, sigfunc);
char sz[10] = {0};
printf("cpy begin 111111111\n");
strcpy(sz, "hello liwq, hello liwq, hello liwq");
printf("cpy end 2222222\n");
printf("pid %d\n", getpid());
printf("ok 1111111111\n");
sleep(3);
printf("ok 2222222222\n");
sleep(500);
printf("ok 3333333333\n");
return 0;
}
不知道是和系统有关系么?在我的机器上运行到结束以后才会处理内存越界的信号。
信号是在内核态向用户态切换前处理的吧,那这段代码就没有切换到内核态运行过么,sleep了这么久,就一直占用CPU,没有被调度过么?请大神们帮解答一下!!!
char sz是动态分配在堆栈上得,strcpy将超过sz大小的数据拷贝到堆栈上面去,导致堆栈的其他部分被所拷贝的内容覆盖。x86将调用函数的返回地址和堆栈指针也保存在堆栈上,由于上面的拷贝将保存在堆栈的数据改写,这样会导致堆栈上保存的调用函数返回地址或者指针指向无效的数据,这样在从被调用函数返回的时候,出发了无效地址访问,从而产生了非法内存访问的信号产生。
你这个问题并不只牵扯到 。
而是牵扯到堆栈保护。
如果你使用:
strcpy((void *)0x1,"hahahahaha");
则在strcpy执行时即刻触发段错误。因为cpu在访问该地址时,
发现该地址是非法的。然后即刻跳转到异常处理流程。抛出段错误。
而你的代码实际是对栈的引用超出界限。但实际上,这玩意儿并不影响你的代码执行。
无非是可能覆盖掉其他的指令。因为x86_64支持栈外128字节的地址合法访问,所以你的代码本不会报错,而是完整的运行。 不信你在编译选项中添加 -fno-stack-protector。程序应该是可以完美执行的。
所以,这个错误实际上不是硬件抛出来的,而是汇编为你的代码插入了 栈保护检查代码,在执行完操作后
会对栈安全进行检查。所以才在最后main返回的时候抛出了 段错误。你不行的话,在main中调用foo函数,foo函数再调用 strcpy(buf,"xxxxxxxxxxxxxxxxxxxxxxx");你会发现他是在foo函数返回时 进行检查。
所以,我也不知道该说啥了。 哈哈哈哈哈哈哈
这是很正常的行为呀,看汇编(x86_64)(希望没弄错什么):
# 初始化 sz,8+2 个 0,占用 -32(%rbp) 到 -23(%rbp)
movq $0, -32(%rbp)
movw $0, -24(%rbp)
movl $.LC1, %edi
call puts
# 将 sz 的地址放入 %rax 中
leaq -32(%rbp), %rax
# 复制字符串中...
movabsq $7596482334399685992, %rdx
movq %rdx, (%rax)
movabsq $7812730951471755639, %rcx
movq %rcx, 8(%rax)
movabsq $2318352665872703599, %rbx
movq %rbx, 16(%rax)
movabsq $7596482334399685992, %rdx
movq %rdx, 24(%rax)
movw $29047, 32(%rax)
movb $0, 34(%rax)
# 复制字符串完成,覆盖 -32(%rbp) 到 1(%rbp)。-23(%rbp) 到 1(%rbp) 的数据被覆盖,这里包括了函数返回地址等数据
所以直到 main 函数返回时才会发生内存访问违例,导致 SIGSEGV 信号的发送。硬件才不在乎你的访问有没有越界呢,它只关心你有没有权限访问指定的内存地址。
另外,严格来讲,在信号处理函数里是不能调用 printf 等会导致内存分配的函数的,因为信号处理是异步的,你需要假设它在任意时候都可能被执行。如果你的程序正好在执行 malloc 之类的函数,锁住了 C 标准库的内存分配用的数据结构,你再次使用需要这个锁的函数就会导致死锁。所以信号处理函数中只能调用可重入函数。
又,你要处理 SIGSEGV 信号通常得使用另外分配的栈来处理,因为已有的函数栈已经坏掉不能用了。当然很少会有程序去费心思处理 SIGSEGV 的,最多记录一些日志而已。
我感觉内存越界的检查(或者说信号被发送)是在函数完成后才进行的,也就是调用函数的堆栈检查。
补充一个例子比较能说明问题:
void sigfunc(int sig) {
printf("recv a signal %d\n", sig);
abort();
}
void testmem(){
char sz[10] = {0};
printf("fun cpy begin 11111111\n");
strcpy(sz,"hello wpjsolo,hello wpjsolo,hello wpjsolo");
printf("fun cpy end 2222222\n");
}
int main(void) {
signal(SIGSEGV, sigfunc);
char sz[10] = {0};
printf("cpy begin 111111111\n");
strcpy(sz, "hello liwq, hello liwq, hello liwq");
printf("cpy end 2222222\n");
printf("pid %d\n", getpid());
printf("ok 1111111111\n");
sleep(3);
printf("ok 2222222222\n");
testmem();
sleep(5);
printf("ok 3333333333\n");
return 0;
}
将在 testmem()运行后马上就abort()
拷贝内存是从低地址向高地址拷贝,栈是从高地址向低地址伸展,你的字符串覆盖了上一个栈帧的内容,
当越界不太多时,只有当函数返回时,才能出现异常。