今天写了一个很简单的代码,编译时没啥错误和警告(主要编译选项没开启警告),然后运行时居然 segmentation fault,把我给看傻了,代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
int main()
{
int fd = open("readme", O_RDWR);
if(fd < 0)
{
printf("open fail: %s\n", strerror(errno));
return fd;
}
return 0;
}
代码简单吧,就把 errno 对应的字符串打印出来,结果就 Segmentation fault 了,搞得我以为用法不对呢,我还去 man 了一个 strerror() 函数的用法:
用 %s 打印 char * 类型,是对的啊,怎么会这样呢?搞得我还去看了源码,顺便学习了一下 errno,原因后面再讲到。先看一下函数 strerror() 的源码:
/* Copyright (C) 1991-2022 Free Software Foundation, Inc.
This file is part of the GNU C Library.
The GNU C Library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
The GNU C Library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with the GNU C Library; if not, see
<https://www.gnu.org/licenses/>. */
#include <string.h>
#include <locale/localeinfo.h>
char *
strerror (int errnum)
{
return __strerror_l (errnum, __libc_tsd_get (locale_t, LOCALE));
}
再看 __strerror_l 函数的源码:
/* Return a string describing the errno code in ERRNUM. */
char *
__strerror_l (int errnum, locale_t loc)
{
int saved_errno = errno;
char *err = (char *) __get_errlist (errnum);
if (__glibc_unlikely (err == NULL))
{
struct tls_internal_t *tls_internal = __glibc_tls_internal ();
free (tls_internal->strerror_l_buf);
if (__asprintf (&tls_internal->strerror_l_buf, "%s%d",
translate ("Unknown error ", loc), errnum) == -1)
tls_internal->strerror_l_buf = NULL;
err = tls_internal->strerror_l_buf;
}
else
err = (char *) translate (err, loc);
__set_errno (saved_errno);
return err;
}
接着往下看 __get_errlist() 这个函数:
#ifndef ERR_MAP
# define ERR_MAP(n) n
#endif
const char *const _sys_errlist_internal[] =
{
#define _S(n, str) [ERR_MAP(n)] = str,
#include <errlist.h>
#undef _S
};
const char *
__get_errlist (int errnum)
{
int mapped = ERR_MAP (errnum);
if (mapped >= 0 && mapped < array_length (_sys_errlist_internal))
return _sys_errlist_internal[mapped];
return NULL;
}
最后就是直接返回对应的指针数组元素值,这个指针数组初始化方式可以参考上一篇文件 这里 。定义了宏 #define _S(n, str) [ERR_MAP(n)] = str 其实就是 _S(n,str) 被替换为 [n]=str,即为元素初始化,而在 errlist.h 头文件里使用了该宏:
_S(0, N_("Success"))
#ifdef EPERM
/*
TRANS Only the owner of the file (or other resource)
TRANS or processes with special privileges can perform the operation. */
_S(EPERM, N_("Operation not permitted"))
#endif
#ifdef ENOENT
/*
TRANS This is a ``file doesn't exist'' error
TRANS for ordinary files that are referenced in contexts where they are
TRANS expected to already exist. */
_S(ENOENT, N_("No such file or directory"))
#endif
#ifdef ESRCH
/*
TRANS No process matches the specified process ID. */
_S(ESRCH, N_("No such process"))
#endif
#ifdef EINTR
/*往下还有很多,这里就不完整展现了*/
而这些 EPERM、ENOENT、ESRCH 等待,就是 errno 的值,这里初始化了 errno 对应的错误字符串,所以经过预编译,_sys_errlist_internal 数组的值应该类似这样:
const char *const _sys_errlist_internal[] =
{
[0] = "Success",
[1] = "Operation not permitted",
[2] = "No such file or directory",
[3] = "No such process",
[4] = "Interrupted system call",
/*.....*/
};
/* SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note */
#ifndef _ASM_GENERIC_ERRNO_BASE_H
#define _ASM_GENERIC_ERRNO_BASE_H
#define EPERM 1 /* Operation not permitted */
#define ENOENT 2 /* No such file or directory */
#define ESRCH 3 /* No such process */
#define EINTR 4 /* Interrupted system call */
#define EIO 5 /* I/O error */
#define ENXIO 6 /* No such device or address */
#define E2BIG 7 /* Argument list too long */
#define ENOEXEC 8 /* Exec format error */
#define EBADF 9 /* Bad file number */
#define ECHILD 10 /* No child processes */
#define EAGAIN 11 /* Try again */
#define ENOMEM 12 /* Out of memory */
#define EACCES 13 /* Permission denied */
#define EFAULT 14 /* Bad address */
#define ENOTBLK 15 /* Block device required */
#define EBUSY 16 /* Device or resource busy */
#define EEXIST 17 /* File exists */
#define EXDEV 18 /* Cross-device link */
#define ENODEV 19 /* No such device */
#define ENOTDIR 20 /* Not a directory */
#define EISDIR 21 /* Is a directory */
#define EINVAL 22 /* Invalid argument */
#define ENFILE 23 /* File table overflow */
#define EMFILE 24 /* Too many open files */
#define ENOTTY 25 /* Not a typewriter */
#define ETXTBSY 26 /* Text file busy */
#define EFBIG 27 /* File too large */
#define ENOSPC 28 /* No space left on device */
#define ESPIPE 29 /* Illegal seek */
#define EROFS 30 /* Read-only file system */
#define EMLINK 31 /* Too many links */
#define EPIPE 32 /* Broken pipe */
#define EDOM 33 /* Math argument out of domain of func */
#define ERANGE 34 /* Math result not representable */
#endif
到这里我们已经知道函数 strerror() 是怎么实现的了,而且也知道函数使用方法也是对的,但为什么会出现 Segmentation fault 呢?原因是没包含头文件,导致 strerror() 函数变成隐式声明了,而隐式声明的函数它的默认返回值是 int 类型(没找到C语言关于这点的说明文档,有同学知道的话烦告知一下),所以用 %s 打印 int 类型导致 segmentation fault 了。所以改用 %d 来打印还会出现段错误吗?
#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
const char *const _sys_errlist_internal[] =
{
[0] = "Success",
[1] = "Operation not permitted",
[2] = "No such file or directory",
[3] = "No such process",
[4] = "Interrupted system call",
/*.....*/
};
int main()
{
int fd = open("readme", O_RDWR);
if(fd < 0)
{
printf("open fail: %d\n", strerror(errno));
return fd;
}
return 0;
}
正确的做法:
1,编译时添加 -Wall 把所有的警告都输出
2,添加头文件 #inlcude <string.h>
因为没做第一步,编译完全看不出问题,假如做了第一步,编译警告可提供更多的线索,如:
gcc 警告信息也是相当重要的啊,要是直接忽略还得花时间查半天,还搞得莫名其妙^-^