call stack是什么错误_Linux C编程(一)系统调用、系统类型、错误处理

写在前面的话

     知识的认知过程其实并不是很轻松的,更何况是Linux系统编程这样一个内容繁杂的话题,足以让大部分人都感觉到一副雾霭阑珊的画面。不过不要就此略过下面那些吸引人心的话题。下面的音乐或许能让这有一个轻松愉快的开始:

    随着悠扬的旋律,我们该思考一下我们用如何认识一下这门知识。但凡走进这篇文章的朋友,多少都会听说过Linux操作系统,想必也会联想到黑色的对话窗口,飞快翻滚的白色指令和输出结果,是的Linux的界面甚是朴素,但其广泛的应用范围和其卓越的性能和稳健让其遍布世界的各个角落。

    然而这个世界就是这样公平,卓越的表现大都源自于背后的复杂原理和多样的控制,要想准确无误的控制Linux,我们用C语言和它进行对话,利用Linux提供的工具附和着其内在的运行原理,我们编织出属于我们想要的功能。说到这貌似我已经说出了关键的三个方面:C语言,API,运行原理。但我想说,这确实是我们大部分编程工作人员都很头痛的地方,的确我们在大学学习过C语言,学习过操作系统,学习过数据结构,但那有什么用呢?

    想必你听说过Redis,你也用过Nginx,你更用过MySQL等等各个在业界表现优异的应用程序,但是你想过这些是如何实现的吗?没错就是上面提到的你在大学学过的当时认为不能产生经济价值的无用知识,这些与当前应用软件开发有代沟的东西就是我们一路披荆斩棘的圣剑,是你在计算机界乘风破浪的动力源泉,Linux的程序开发便是其一个神奇的应用领域,这里没有华丽的编程界面,没有火热的宣传市场,没能让每一个想迈入计算机行业的年轻人耳熟能详,但它确是计算机理论连接那些夺人眼球的应用市场的关键一环,想学好这门知识需要足够的耐心,需要足够的坚强,需要足够的基础知识,需要我们欠缺的工匠精神,当走过黎明前的黑暗,你就会发现原来MySQL并不那么遥不可及,Redis也不是不能实现,所有这一切我们都能做得到,而不是仅仅停留在使用层面上,那么加油吧“少年”!

    我们该如何学习Linux编程呢?人类对新事物的认识是有一定的规律的,这个世界上可以说没有不能理解的知识,只有描述不够清晰的教科书。我们的所有定理,自然规律,都是我们前辈的对事物的客观总结,是人类大脑提炼出来的思维成果,我们只要具备一定的基础知识,只要拿到一个精确详尽的关于确定问题的描述,我们也是能够领悟和理解的,所以我将在本文中力求做到对问题的精确描述,将复杂问题和现实社会的容易理解的关系进行类比,将抽象的问题形象化,我们就有的放矢的去理解了。

    学会一项技能正确理解仅是第一步,可以说你只得到了一半,当你可以运用你理解的知识去指导你进行生产应用,才算是拿到了另一半。所以总的来说学习和认知可简要的归为两部分:理解知识,应用知识。所以关于Linux编程的学习离不开实际应用和编程实践,我会在每篇辅以针对本篇内容的应用试验,提供详细的代码和说明,以及运行的条件和结果(反正电子版又不用考虑用纸的成本)向读者表述出详细的情形,迅速对知识形成清晰的认识。

    另一个重要的问题我不得不说,Linux的知识很繁杂,我们不能在一开始就要求一步全都学会,对于这类知识我们的经验是学习主干知识,为读者建立清晰的主干知识网络,其余的是在让读者学习主干知识的过程中学会自己汲取Linux编程的知识去丰富知识脉络,因为世界在发展,Linux从没有停止它的进化,我们最重要的是学会自我认知。


  1. 系统调用

    系统调用是Linux内核为实现操作系统功能而提供的运行于核心态的函数,那么问题就来了:既然系统调用运行于核心态的(也就是由操作系统自身调用的,用户进程没有权限),那我们作为用户又怎么能调的动这种需要特权的东西呢?

        在说清这个问题之前我们先说一些概念,来建立我们交流的基础:

   用户态和核心态:这两个是操作系统工作的两种形式,工作于用户态的进程,只能访问自己进程内的空间;工作于核心态的进程权限就比较高,不仅能访问核心态的进程空间,还能访问其他用户态的进程空间。这就比较有意思了,打个比方,用户态的进程就相当于每个星巴克的会员顾客,他们只能看看自己会员卡的积分和消费记录,但核心态的进程就相当于星巴克的系统管理员,他们能看到每个会员的积分和消费记录,通过这种权限的区分,能够实现很多有意思的东西和很多安全保护。

    API和C标准库: API这个三个字母想必大家可能有所了解,其实这是一种标准化的对话标准(对于Linux来说著名的是POSIX),这是为了便于我们做应用程序开发而制定的一个和Linux系统打交道的标准,我们通过遵从API能够在不同版本的Linux中不改变应用程序和操作系统的交互方式,这样就省去了在Linux不断进化的时候,我们的应用程序不得不改变调用方式的麻烦。那么C标准库是实现了主要的API,所以我们的将要学习编写的C程序都可以使用C标准库。

    有了上面的描述,我们就可以开始解释操作系统怎样允许我们调用只有内核才能调用的功能(系统调用的工作过程,我们只需要了解其工作流程即可,详细的工作过程可参见关于Linux内核的书籍):

    既然我们的程序工作在用户态,我们还想让操作系统执行内核实现的功能,这其中必定有一个连接的纽带,它能够实现函数调用参数的传递,内核工作结果的返回,内核调用哪个函数的选择功能,这个纽带我们便成为系统调用,说白了系统调用只是个中介,真正执行功能的是对应的内核中的服务例程。

    当我们的程序要进行一次系统调用的时候会发生以下的过程:

      1.应用程序通过C库函数的外壳函数引发系统调用。

    2.外壳函数会把系统调用需要的准备工作做好:首先把需要传递给内核服务例程的参数传递到CPU寄存器,但是不能多于5个,因为CPU的寄存器数量有限,那要是传递的参数多了怎办,有个办法就是把用户进程空间中存储参数的栈指针放在一个寄存器中传递过去,这样系统调用就能把对应地址的内容根据这个指针读取过来,注意这时候发生了奇怪的事,那个叫做系统调用的家伙是不是读取了用户进程空间的数据?是的,系统调用的工作函数总是system_call(),负责处理用户进程和内核空间交互数据的一系列工作,它工作在核心态,拥有查看其它进程内部数据的特权。其次外壳函数还会把此次系统调用到底要调用那个功能的编号(系统调用编号)放到eax寄存器中传递给系统调用,至此系统调用的前期准备工作结束。

     3.外壳函数发生软件中断 int 0x80,将CPU由用户态切换到核心态并执行中断向量处指向的代码。

   4.很巧,这个中断向量的代码便是system_call(),在这之前操作系统自动进行空间转换为核心态用户空间的寄存器ss,esp,EFLASG,cs,eip自动压入内核空间栈。

      5.这时候系统调用例程system_call()就开始一通猛如虎的操作:

            把寄存器中的参数压入内核栈,因为内核服务例程工作也是以函数的形式进行的,在其被调用之前需要把参数环境准备好,然后把eax中的系统调用编号拿出来和存储在system_call_table内核变量(需要内核源码编译可见):

ENTRY(sys_call_table)

.long SYMBOL_NAME(sys_ni_syscall) /*  0 - old "setup()" system call*/

.long SYMBOL_NAME(sys_exit)

.long SYMBOL_NAME(sys_fork)

.long SYMBOL_NAME(sys_read)

.long SYMBOL_NAME(sys_write)

.long SYMBOL_NAME(sys_open) /* 5 */

.long SYMBOL_NAME(sys_close)

.long SYMBOL_NAME(sys_waitpid)

....

其实eax中存储的就是这里从0开始计数的行号,通过对应关系找到对应的内核服务例程进行调用,在调用之前会进行对之压入在内核栈中的参数进行验证,若合法便会进行对应内核服务例程的调用了,并将返回结果记录在errno全局变量中(通常用一个负的返回值来表明错误,返回一个0值通常表明成功)。

    6.system_call()将返回值存入eax寄存器。

    7.进程空间转换为用户态,自动将寄存器ss,esp,EFLASG,cs,eip自动压入内核空间栈的数值恢复到对应寄存器。

    8.C库函数中的外壳函数将对系统调用中设置的errno进行取反,这样若产生错误,errno就成为了正值,无错误为0。如果一个系统调用失败,你可以读出errno的值来确定问题所在,调用perror()库函数,可以把该变量翻译成用户可以理解的错误字符串。

bcf08f847736f971149c3b39d654ad21.png


  2.库函数

    库函数有些完全不使用系统调用,而有些则是对系统调用的包装,它们往往提供了比系统调用更便利的使用方式,比如printf函数制定了多种样式的输出定制,而其包装的write只能输出指定长度的字节数。

  我们所讨论的是基于标准库的GNU C(glibc),我们可以用过查找libc.so.6 来执行它以便查看对应的glibc的版本:

e0408357588e657ad91cf0b288609bcf.png

上图显示当前glibc的版本为2.17

 在运行时状态我们若想获取glibc的版本,我们可以使用如下函数:

#include

    const char * gnu_get_libc_version(void) ;  

 它会返回一个指向字符串的指针,就上图来讲它将返回“2.17”。

还有一种获取glibc版本的办法(glibc特有):

#include

size_t confstr(int name, char *buf, size_t len);

其中name=_CS_GNU_LIBC_VERSION,关于size_t的描述见下文。

下面是实验,采用上述两种方法来获取对应的glibc版本:

44f6d544f2b37269e050bfb6bd65207d.png

编译步骤以及运行结果显示为:

5333198c032e24ce3a0afd750cf6828c.png

本实验运行环境为:

4c5cc881c0b6c3cb45773ba2afa6e4da.png


  3.错误处理

    对库函数和系统调用的返回值进行测试以监控调用是否正常运行是一个很明智的方法,这能极大地减少程序的调试时间。

    通常对于系统调用返回值为-1这表明出现错误,错误的编号为一个正值存储在了全局变量errno()中(在“系统调用”一节已经描述过),我们可以通过下面两个函数来显示具体的发生错误的原因:

#include
void perror(const char *s);

该函数直接打印出s参数和errno中数值对应的错误描述字符串拼接的字符串结果。

 #include
 char *strerror(int errnum);该函数返回一个指向对应errnum的错误描述字符串,注意该指针指向的内容在多次调用strerror时会对其进行覆盖。

下面是对两个函数调用的示例:

53db6382b2550fd79156824896bc41ae.png

编译运行结果为:

2de134e356f2c4504ade7ef27519188c.png

    对于不同的库函数返回值也不尽相同:

    与系统调用返回值相同,-1表示运行异常,errno记录错误编号;

    发生异常时返回不是-1的值,使用errno记录错误编号;

    根本就不使用errno的;

    上述三种情况都会发生,我们不能详尽的记住每一个函数的情形,故我们需使用Linux的帮助手册man(下文会有描述)来查看使用函数的具体情形。

  4.系统数据类型和可移植性

    来思考一个问题,我们在学习C语言时,经常会看到书上描述int的长度随不同的操作系统实现,能够容纳有符号整型数据类型的范围也不相同,因为有的系统int为2字节,有的系统为4字节,那么这个问题在Linux家族中也是存在的,这时候就有问题了,我在甲系统上编写的程序能够运行,到了另外一个系统上并不一定能够运行,因为C语言的基础数据类型实现并不相同,为了解决这个问题SUSV3规定各种标准系统数据类型,并要求Linux的各个实现版本加以定义和使用。

    每种标准系统数据类型的定义均使用了C语言的typedef语法,以进程ID为例其标准系统数据类型为p_id,定义如下:

    typedef int p_id;

      标准系统数据类型都以_t结尾,大多数都定义在sys/types.h中,随着测试宏的不同取值,标准系用数据类型在types.h中的定义会发生变化,故而在编译程序的时候通过制定测试性宏的指定,便能应对不同系统间C语言基础数据类型的差异问题:

eeca7bb3a6bbb4af0574069ee39661bc.png

    上面我们介绍了Linux怎样解决系统数据类型差异的情况下如何定义,那么还有一个问题发生在打印的时候printf中那么p_id的匹配字符是什么呢?%d吗,若是更长我们还要替换为%ld对应为long型,可是系统并不会给我们的苦恼买账。

    一个相对通用的办法是利用C语言自动类型转换的特性对于短于int的类型如short int 在计算和打印时会自动升级为int,对于int和long类型则不会发生词类转换,那么一个笨办法就是在printf运行时无法判定参数类型时,我们手动的将其转换为最长的long:

printf("the parent process id is %ld",(long)pid);

便在一定程度解决了这个问题,虽然我们说是笨办法,却很简单实用。

5.使用手册

    前面我们所描述的错误处理,errno的取值,对应描述字符串,函数的格式,需要哪些声明文件,都是很具有个性化的,用尽我们半生的力气也不一定会记清楚,我们不是应试教育,该记的记,不该记的Linux也给我们准备的很清晰,man命令记住了一切:

man 3 perror

便能查看系统调用的perror函数的所有信息:

fe4d133aaeaa58904b623a8e081d4311.png

怎么样,是不是很好用,但是这其中也有一些问题需要我们去解决,那个3是什么:

    1-Standard commands (标准命令)
    2-System calls (系统调用)
    3-Library functions (库函数)
    4-Special devices (设备说明)
    5-File formats (文件格式)
    6-Games and toys (游戏和娱乐)
    7-Miscellaneous (杂项)
    8-Administrative Commands (管理员命令)
    9-其他(Linux特定的), 用来存放内核例行程序的文档

 有了这个解释列表我们便清楚了,要看库函数便将man的参数取值为3,若看系统调用便将man的参数取值为2。

    但有的时候,使用 man 2 read 系统并不买账,我们可能会看不到read系统调用的详细信息,这是因为运行的系统没有安装man-page:

yum install -y man-page

mandb

安装man-page后,运行mandb,更新数据库后,便能看到全部的手册帮助信息了。

文章若疏漏错误,请各位看官批评指正,我们的公众号会持续更新Linux C编程系列文章,若对此有兴趣可关注公众号:

9f2575ceae1fb0e2155038ba891b4b74.png

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值