Linux编程知识集锦
共享库(Shared libraries),在程序执行起始时被自动加载(而非执行过程中随时动态加载)。在链接阶段,必须有共享库才能链接。
使用共享库时,运行时加载的库,应当与链接时期望的库的“版本”一致,即功能上没有不兼容的变化。二进制文件(库、可执行文件)本身知道其依赖的共享库的版本。
如何识别这种变化并没有一致的规范,某些UNIX系统通过soname来版本化共享库,所谓soname就是在共享库名称后附加可选的数字后缀,例如libx.so.1。仅当共享库的接口发生不兼容变化时soname才改变,如果libx从1.0到1.9维持了一致性的接口,它们的soname应该一致。注意soname和文件名不是一回事,1.3版本的libx的文件名可能叫libx.so.1.3,但是它的soname可能是libx.so.1。
通过soname,可以很容易的允许不同版本的共享库存在于同一个系统中(相比Windows的DLL地狱)。不带后缀的example.so常常是指向最新版本的符号链接。不带后缀的so用于链接器,因为gcc不知道去寻找有后缀版本,例如-lpthread只能让gcc去寻找libpthread.so。
soname可以具有主次版本号区分,使用额外的点号分隔。例如: libFOO.so.MAJOR.MINOR.BUGFIX,MAJOR表示ABI不再向后兼容,MINOR表示ABI仍然向后兼容,BUGFIX意味着属于内部改变,ABI不发生变化。
当安装libx库到系统时,可以建立符号链接 libx.so -> libx.so.1 ,当libx出现不兼容升级时,则需要修改前述符号链接,例如 libx.so -> libx.so.2 。这样,新的程序总是和最新的libx版本进行链接。
另一方面,这种符号链接用法可以扩展为,将某个soname链接到特定文件,例如 libx.so.1 -> libx.so.1.3 ,如果1.3版本有一个BUG在1.3.2中修复,可以修改前述符号链接为 libx.so.1 -> libx.so.1.3.2 ,这样基于libx.so.1链接的可执行文件在获得BUG修复的同时,能够找到正确的共享库。
动态加载库(Dynamically loaded libraries),类似于Windows的DLL,运行期间,使用到的时候进行加载。编译链接都不需要目标库文件。
DLL中包含两类函数:导出(exported)函数、内部(internal)函数,只有导出函数可以被外部模块使用。SO中的所有函数均可以被外部使用,不需要特殊的export语句导出函数(在大部分UNIX系统中,默认所有符号被导出)。
静态库(Static libraries),简单的把一些obj文件打包为.a归档文件。优势是执行速度通常更快,而且是静态链接,因此不会出现找不到库、版本错误导致无法执行的情况。缺点是难以维护,库出现BUG则所有依赖都得重现链接
有时,如果依赖库声明的顺序不正确会导致:undefined reference错误。对于静态链接库libA,如果它依赖于链接库libB,那么,在Linker的参数列表中,libA必须在libB前面出现。动态链接库则无此顺序要求。
属于C编程语言的一部分,它是一个规范(Specification),它定义了若干函数,以及这些函数应该具有什么样的行为。
C标准库的更多内容参考C语言学习笔记。主要头文件包括assert.h ctype.h errno.h float.h limits.h locale.h math.h setjmp.h signal.h stdarg.h stddef.h stdio.h stdlib.h string.h wchar.h time.h
某种程度上是C标准库的超集 dlopen, fork等函数由POSIX库定义。
GNU的glibc库是C标准库、POSIX C库的超集,在Linux下通常作为共享对象位于/lib/x86_64-linux-gnu/libc.so...
glibc使用C语言的GCC方言编写,是Linux系统的基石。glibc简化了你和系统调用的交互(进行了函数封装)。不使用glibc的情况下,你可以直接写汇编代码来执行系统调用。如果仅仅需要使用C标准库的函数,可以使用musl-libc或者dietlibc等。
libanl libc libc_nonshared libcrypt libdl libg libmvec libpthread libresolv librt libutil等都来自glibc库。
操作系统内核仅仅是Linux的一小部分,而大部分应用程序由Linux社区开发。Linux社区支持自由软件的概念,遵从GNU(GNU's not UNIX)通用许可证(GPL)。
GNU项目由Richard Stallman创立,目的是创建一个与UNIX系统兼容,但不受UNIX名字、源代码私有权限制的OS和开发环境。下面列出一些著名的GNU软件:
软件 | 说明 |
GCC | GNU编辑器套件,包括GNU C编译器 |
G++ | C++编译器,GCC的一部分 |
GDB | 源代码级调试器 |
GNU make | UNIX make的免费版本 |
Autotools | 包含Autoconf、Automake、Libtool、Gettext等组件的自动化构建套件 |
Bison | 与UNIX yacc兼容的语法分析程序生成器 |
bash | 命令解释器 |
GNU Emacs | 文本编辑器 |
除上表以外,还有大量软件,例如电子表格、源代码管理工具、编译器、因特网工具、桌面环境(GNOME、KDE),都是在GPL条款下发布的。由于GNU软件的重要贡献,现在很多人把Linux称为GNU/Linux。
关于GNU的软件的更多内容:
- GCC / GDB:将在本文后续内容中详细介绍
- Make可以参考:GNU Make学习笔记
- Autotools可以参考:Autotools学习笔记
GCC是GNU项目的C/C++编译器。
当调用gcc时,正常是执行从预处理、汇编、编译、链接的整个过程,通过某些选项可以在某个步骤中断此处理过程。例如: -c表示不运行链接,只输出汇编器的目标文件。
以 -f、 -W开头的选项(例如-fmove-loop-invariants、-Wformat),通常包含正/反作用的一对选项, -ffoo的反选项是-fno-foo。
C++程序可以使用g++命令来编译。g++是gcc的一个前端应用
变量 | 说明 |
LIBRARY_PATH | 编译期间,GCC基于此环境变量来定位(静态)库所在的目录 |
LD_LIBRARY_PATH |
链接、运行时需要的共享库文件的位置,通常/lib、 /usr/lib默认可用
可执行程序或者库所依赖的其它共享库,可以通过 ldd获知 在 /etc/ld.so.conf文件中也可以列出共享库的位置 |
PKG_CONFIG_PATH | pkg-config命令搜索 .pc文件的路径 |
CPATH | C/C++头文件搜索位置 |
C_INCLUDE_PATH | C头文件搜索位置 |
CPLUS_INCLUDE_PATH | C++头文件搜索位置C++头文件搜索位置 |
1 2 3 4 5 6 7 8 9 10 11 12 |
cd ~/CPP/lib/gcc/7.2/src contrib/download_prerequisites mkdir build && cd build ../configure -v --build=x86_64-linux-gnu --host=x86_64-linux-gnu --target=x86_64-linux-gnu --prefix=/home/alex/CPP/lib/gcc/7.2.0 --enable-languages=c,c++ --disable-multilib --program-suffix=-7.1 # 程序名字后缀 # 构建 make -j 8 # 安装 make install |
1 2 3 4 5 6 7 8 |
# 下面列出最常用的选项,g++基本上接受与gcc同样的选项 gcc [-c|-S|-E] [-std=standard] [-g] [-pg] [-Olevel] [-Wwarn...] [-Wpedantic] [-Idir...] [-Ldir...] [-Dmacro[=defn]...] [-Umacro] [-foption...] [-mmachine-option...] [-o outfile] [@file] infile... |
选项 | 说明 | ||
控制输出 | |||
-E | 仅执行编译预处理 | ||
-S | 将C代码转换为汇编代码 | ||
-c | 仅执行编译操作,不进行链接操作 | ||
-o | 指定生成的输出文件文件 | ||
C方言控制 | |||
-ansi | 在C模式下,等价于 -std=c90。在C++模式下,等价于 -std=c++98 | ||
-std |
控制使用的方言:
|
||
启用/抑制警告 | |||
-w | 抑制所有警告信息 | ||
-Wall | 显示所有警告信息 | ||
-Wno-unused-parameter | 忽略未使用的变量 | ||
-Werror | 在出现警告的地方强制停止编译 | ||
-Werror= | 指定哪些警告被看作错误 | ||
调试选项 | |||
-g |
使用操作系统本地格式来生成调试信息,可供GDB使用,可以指定0-3
|
||
优化选项 | |||
-O |
指定编译优化级别,0-3,级别越大优化效果越好,编译时间越长
|
||
预处理选项 | |||
-D | 定义宏,name或者name=definition,默认definition为1 | ||
-U | 取消宏定义 | ||
-I | 指定头文件的查找目录, /usr/include自动寻找 | ||
-include | 用来指定头文件,很少使用 | ||
-nostdinc | 不去搜索系统的标准C头文件位置 | ||
-nostdinc++ | 不去搜索系统的标准C++头文件位置 | ||
连接选项 | |||
-L | 指定寻找链接库的目录, /lib /usr/lib /usr/local/lib自动寻找 | ||
-l |
指定需要链接的库的名称,必须去掉前后缀,并且不留空格,例如 -lm。链接Windows下的库时,只需要去掉后缀 |
||
-Wl,-rpath-link |
当使用ELF时,一个共享库可能依赖于另外一个。当ld -shared并且将另外一个共享库作为输入的时候就会发生 该选项指定查找依赖的共享库时,优先寻找的一系列目录(冒号分隔) |
||
-Wl,-rpath |
修改runtime linker 搜索共享库的路径,优先级高于LD_LIBRARY_PATH RPATH是写入到ELF文件中的信息:
|
||
-Wl,--dynamic-linker |
使用指定的连接器:
|
||
-shared |
可以用于编译出共享库 | ||
-static | 默认情况下,链接时GCC优先使用共享库。该选项强制链接到静态库 | ||
-fmessage-length=n | 格式化错误信息,最长n字符。对于g++默认72,;其它gcc前端0,即不换行 | ||
-Wl,option | 向连接器传递选项,多个选项使用逗号分隔 | ||
-Wl,-soname=new_soname | 设置生成的共享库的soname,等价于-Wl,-hnew_soname | ||
-Wl,--out-implib= | 生成的导入库的名称 | ||
-Wl,--output-def= | 生成的DEF文件的名称 | ||
-Wl,--dynamic-linker | 指定使用的连接器 | ||
-nostartfiles | 连接时不使用标准的系统startup文件。标准库文件仍然被使用,除非使用 -nostdlib 或 -nodefaultlibs | ||
-nodefaultlibs | 连接时不使用标准系统库 | ||
-nostdlib | 连接时不使用标准的系统startup文件、库文件 | ||
未归类选项 | |||
@file | 从文件中读取命令行选项 | ||
outfile | 命令输出文件 | ||
infile | 命令输入文件的列表 | ||
-fpic |
作用于编译阶段,告诉编译器产生与位置无关代码(PIC),生成动态库时应该启用该选项,启用该选项后,所有代码通过全局偏移表(GOT)访问地址常量。GOT的条目将由操作系统的动态加载器负责解析。如果GOT超过机器限制的大小,你会得到错误提示,此时可以使用-fPIC 代替 PIC需要机器/OS的支持:例如对于i386来说GCC支持System V,Sun 386i则不支持 不使用PIC的时候,共享库的代码段必须被每个引用进程修改,因而无法实现共享 |
||
-fPIC | 同上,但是不会受到GOT大小的限制 | ||
-fpie -fPIE | 类似上面两个选项,但是生成的动态库仅仅供可执行文件链接 | ||
-fexec-charset=charset | 设置执行字符集,用于字符串和字符常量,默认UTF-8 | ||
-fwide-exec-charset=charset | 设置宽执行字符集,用于宽字符串和宽字符常量,默认UTF-32或UTF-16,应和wchar_t宽度对应 | ||
-finput-charset=charset | 输入字符集,当GCC将输入文件(源代码文件)字符集转换为源字符集(GCC内部使用)时使用。默认值UTF-8,可以被该选项或者locale覆盖 | ||
i386/x86-64选项 | |||
-mthreads | 用于在MinGW32下支持线程安全的异常处理 | ||
-m32 | 生成32位代码,long/指针为32位,支持任意i386架构 | ||
-m64 | 生成64位代码,long/指针为64位,支持x86-64架构 | ||
i386/x86-64的Windows选项 | |||
-mconsole | 用于Cygwin、MinGW目标,提示生成控制台应用程序,连接器会设置PE头的subsystem,该选项是Cygwin、MinGW目标的默认行为 | ||
-mdll | 用于Cygwin、MinGW目标,提示生成动态链接库(DLL) | ||
-mnop-fun-dllimport | 用于Cygwin、MinGW目标,忽略dllimport属性 | ||
-mthread | 用于MinGW目标,使用MinGW专有的线程支持 | ||
-mwin32 | 用于Cygwin、MinGW目标,预处理器将使用Windows典型的预定义宏 | ||
-mwindows |
用于Cygwin、MinGW目标,提示生成GUI程序 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
# 不指定选项,将预处理、汇编链接形成可执行文件,默认输出a.out gcc hello.c # 指定输出文件的名称 gcc hello.c -o hello # 得到预处理输出文件 gcc -E hello.c -o hello.i # 将预处理输出文件编译为汇编文件hello.s gcc -S hello.i # 将汇编文件编译为目标文件hello.o gcc -c hello.s # 不指定选项进行链接,生成可执行文件 gcc hello.o -o hello # 多个文件一起编译,形成可执行文件 gcc hellolib.c hello.c -o hello # 亦可逐个编译,然后链接,这样可以只编译部分文件 gcc -c hellolib.c gcc -c hello.c gcc hellolib.o hello.o -o hello # 编译出共享库 gcc -shared hellolib.c -o libhello.so # 强制使用静态hi库来链接 gcc hello.o -static -lhi -o hello |
显示已经安装的软件包的元数据信息,该工具可以用于:
- 检查库的版本号。如果所需要的库的版本不满足要求,它会打印出错误信息,避免链接错误版本的库文件
- 获得编译预处理参数,如宏定义,头文件的位置
- 获得链接参数,如库及依赖的其它库的位置,文件名及其它一些链接参数
- 自动加入所依赖的其它库的设置
要使用pkg-config命令,目标库必须提供一个包含了元数据的.pc文件。对于大部分系统,这些.pc文件存放在:
- /usr/lib/pkgconfig
- /usr/share/pkgconfig
- /usr/local/lib/pkgconfig
- /usr/local/share/pkgconfig
但是可以通过环境变量 PKG_CONFIG_PATH进行自定义。
字段 | 说明 |
Name | 该字段显示一个人类可读的包的名称 |
Description | 该字段显示一个包的简短描述 |
URL | 可以从其下载包的网址 |
Version | 详细的软件包版本字符串 |
Requires | 逗号分隔的,被当前包依赖的包的列表,来自依赖包的标记会被自动合并输出,可以指定操作符:=, <, >, >=, <= |
Requires.private | 类似于Requires,但是在进行动态链接时,这些依赖不会被考虑存放在标记中 |
Conflicts | 类似于Requires,列出与当前包冲突的库 |
Libs | 该字段显示当前包依赖的库 |
Libs.private | 显示当前包依赖的私有库,这些库不需要暴露给当前库的使用者,但是当前库需要与之静态链接 |
Cflags | 显示编译标记的列表 |
下面是一个例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
# This is a comment prefix=/home/hp/unst # this defines a variable exec_prefix=${prefix} # defining another variable in terms of the first libdir=${exec_prefix}/lib includedir=${prefix}/include Name: GObject # human-readable name Description: Object/type system for GLib # human-readable description Version: 1.3.1 URL: http://www.gtk.org Requires: glib-2.0 = 1.3.1 Conflicts: foobar <= 4.5 Libs: -L${libdir} -lgobject-1.3 Libs.private: -lm Cflags: -I${includedir}/glib-2.0 -I${libdir}/glib/include |
变量 | 说明 |
PKG_CONFIG_PATH | 用于搜索.pc文件的目录的集合,默认目录在这些目录后面搜索。默认目录为:libdir/pkgconfig:datadir/pkgconfig,其中libdir、datadir是pkg-config安装时的相关目录 |
PKG_CONFIG_DEBUG_SPEW | 如果该变量被设置,pkg-config将打印所有的调试信息、错误信息 |
PKG_CONFIG_TOP_BUILD_DIR | 设置.pc文件中可能包含的pc_top_builddir变量的值,默认使用$(top_builddir)。只在依赖一个尚未安装的库时用到 |
PKG_CONFIG_DISABLE_UNINSTALLED | 默认的,如果你指定一个foo,并且存在一个库:foo-uninstalled,那么后一个变体被使用,该变量禁止此行为 |
PKG_CONFIG_ALLOW_SYSTEM_CFLAGS | 不从cflags中去除-I/usr/include |
PKG_CONFIG_SYSROOT_DIR | 修改-I、-L使用的系统根目录,如果该变量设置为/var/target,那么目标库的-I/usr/include/libfoo将被解释为-I/var/target/usr/include/libfoo |
PKG_CONFIG_LIBDIR | 替换默认的pkg-config搜索目录,一般设置为/usr/lib/pkgconfig |
1 2 3 4 5 6 7 8 |
pkg-config [--modversion] [--help] [--print-errors] [--silence-errors] [--cflags] [--libs] [--libs-only-L] [--libs-only-l] [--cflags-only-I] [--variable=VARIABLENAME] [--define-variable=VARIABLENAME=VARIABLEVALUE] [--print-variables] [--uninstalled] [--exists] [--atleast-version=VERSION] [--exact-version=VERSION] [--max-version=VERSION] [--list-all] [LIBRARIES...] [--print-provides] [--print-requires] [--print-requires-private] [LIBRARIES...] |
选项 | 说明 |
--modversion | 请求显示目标库的版本信息,如果指定多个库并且都成功找到,那么每个库的版本信息占一行 |
--print-errors | 如果一个或者多个目标库、或者其依赖没有被找到,或者解析.pc文件失败,该选项会打印错误信息。环境变量PKG_CONFIG_DEBUG_SPEW覆盖此选项 |
--cflags | 打印在编译目标库时,使用的预处理、编译标记,包括其依赖的所有标记 |
--cflags-only-I | 仅打印--cflags中的 -I部分,即头文件搜索路径 |
--libs | 显示链接标记 |
--libs-only-L | 仅显示-- libs中的 -L/-R部分,即依赖库的搜索路径 |
--libs-only-l | 仅显示-- libs中的 -l部分,即依赖库的名称 |
--variable=NAME | 返回.pc文件中定义的变量 |
--define-variable=NAME=VALUE | 设置某个变量的全局值,覆盖.pc中的值 |
--print-variables | 打印目标库定义的变量的列表 |
--exists | 用于判断目标包/包列表可以被检测到 |
--atleast-version=VERSION | |
--exact-version=VERSION | |
--max-version=VERSION | |
--msvc-syntax | 仅用于Windows,使输出格式中的-l、-L可以被MSVC编译器cl识别 |
--static | 输出适合静态链接的库 |
--list-all | 列出能识别的所有模块 |
--print-provides | 列出目标库提供的所有模块 |
--print-requires | 列出目标库需要的所有模块 |
--print-requires-private | 列出目标库需要静态链接的所有依赖库 |
1 2 3 4 |
# 打印glib-2.0库的prefix变量 # 检查是否存在大于1.3.4版本的glib-2.0库、以及大于1.8.3的libxml库 pkg-config --variable=prefix glib-2.0 pkg-config --exists 'glib-2.0 >= 1.3.4 libxml = 1.8.3' |
配置动态连接器的运行时绑定,修改额外的链接库位置后,需要调用该命令使之生效
该命令为参数中提供的路径下找到的共享库,在:
- /etc/ld.so.conf(动态库的额外搜索位置)
- 受信目录(/lib /usr/lib)
中创建必要的链接、缓存。修改文件/etc/ld.so.conf后应当调用ldconfig。运行时连接器ld.so、ld-linux.so会使用这些缓存
ldconfig通过检查库的文件名和头,来判断那些版本(的库)需要更新其软链接。
1 2 3 |
ldconfig [ -nNvXV ] [ -f conf ] [ -C cache ] [ -r root ] directory ... ldconfig -l [ -v ] library ... ldconfig -p |
-v 打印当前版本号,正在处理的目录、创建的链接
-n 仅仅处理命令行指定的目录。不处理/etc/ld.so.conf中列出的目录,以及受信目录(/lib、/usr/lib)
-N 不重建缓存
-X 不更新软链接
-f 使用其它文件而非/etc/ld.so.conf
-C 使用其它缓存而非/etc/ld.so.cache
打印指定的程序、共享库,所依赖的共享库列表
1 |
ldd [OPTION]... FILE... |
-u 打印没有使用的直接依赖
-d 进行重定位,报告所有缺失的对象
-r 同时对数据对象和函数进行重定位,报告缺失的对象或函数
显示一个或多个目标文件(object files)的信息
1 |
objdump [options] objfile... |
-a 如果某个目标文件是归档文件(静态库),显示归档的头信息
-f 显示目标文件一般性的头
-p 显示特定目标文件格式的私有头
-h 显示目标文件section header的基本信息
-x 显示所有可用的头信息,包括符号表、relocation entries
-d 执行反汇编,仅针对可能存在代码的section
-D 对所有section执行反汇编
-S 显示混合着汇编的源码,隐含-d
-g 显示Debug信息
-e 显示Debug信息,输出格式与ctags工具兼容
-C 解码(demangle)低级的符号名称为用户可读名称,让C++函数名可读
1 2 3 4 5 |
# 显示共享库的soname objdump -p /usr/lib/mysql/plugin/auth.so | grep SONAME # 查找符号 objdump -x /home/alex/.local/lib/libc.so.6 | grep getaddrinfo |
显示ELF文件的信息。ELF是UNIX系统实验室(USL)作为应用程序二进制接口(Application Binary Interface,ABI)而开发和发布的,也是Linux的主要可执行文件格式。
选项 | 说明 | ||
-a | 等价于 -h -l -S -s -r -d -V -A -I | ||
-h |
显示ELF头信息,这是一些最基础的信息:
|
||
-l --program-headers --segments |
显示program headers信息 | ||
-S --section-headers --sections |
显示section headers信息 | ||
-g --section-groups |
显示section groups信息 | ||
-t --section-details |
显示section详细信息
|
||
-s --syms --symbols |
显示符号表 | ||
--dyn-syms | 显示动态符号表 |
可以将可执行文件中的调试信息抽取出来:
1 2 |
objcopy --only-keep-debug foo foo.debug strip -g foo |
GNU的调试器,可以:
- 启动应用程序,并指定任何参数
- 在特定情况下,让应用程序暂停执行
- 暂停指向后,检查应用程序的状态
- 改变应用程序状态
编译器有时候不去记录源文件所被编译时的实际目录,而仅仅记录源文件的名称或相对路径。即使记录了,在你调试的时候,编译目录可能已经删除,这就让gdb面临如何将符号表中的源文件路径映射到Debugger机器的源文件的问题
gdb使用一系列目录,从中搜索源文件,此所谓source path。每次gdb需要源文件时,逐个搜索这些目录,直到找到一个名称匹配的文件
例如,假设可执行文件符号表中引用/usr/src/foo-1.0/lib/foo.c,而Debugger的source path设定为 /mnt/cross,则文件的(在Debugger上)搜索顺序为:
- 按符号表字面值搜索:/usr/src/foo-1.0/lib/foo.c
- 添加source path作为前缀:/mnt/cross/usr/src/foo-1.0/lib/foo.c
- 添加source path作为前缀,剥离符号表的目录部分:/mnt/cross/foo.c
符号表引用的相对路径../lib/foo.c类似,分别搜索 ../lib/foo.c、/mnt/cross/../lib/foo.c、/mnt/cross/foo.c
在符号表中,程序源代码中的每个标识符都和它的声明或使用信息绑定在一起,比如其数据类型、作用域以及内存地址。
目标文件中通常会有一个包含了所有外部可见标识符的符号表。在链接不同的目标文件时,链接器会使用这些文件中的符号表来解析所有未解析的符号引用。
在调试阶段,需要通过符合表来解读各种信息。
对于Ubuntu来说,软件包的符号表独立分发。它们发布在 ddebs.ubuntu.com,命名为 <package>-dbgsym。相比传统的 <package>-dgb,它的优势包括:
- dbgsym可以自动生成、自动上传到ddebs.ubuntu.com。维护成本低
GDB支持在独立文件中存放的符号信息,并且能够自动发现、加载。
GDB支持两种方式来指定调试信息文件:
- 在可执行文件中,包含debug link,指定了独立调试文件的名称,debug link是可执行文件中名为.gnu_debuglink的特殊section。调试文件名称格式通常是executable-name.debug。debug link 还提供调试文件的CRC检查,用于验证调试文件、可执行文件来自同一次build
- 在可执行文件中,包含build ID,在对应的调试文件中,此ID也存在。build ID是可执行文件中(通常)名为 .note.gnu.build-id的section
对于方式debug link方式,GDB依次在以下位置寻找调试文件:
- 在可执行文件所在目录
- 名为.debug的子目录
- 全局debug目录
对于build ID方式,GDB会在全局debug目录的.build-id子目录中查找nn/nnnnnnnn.debug文件。前面的nn是build ID的最前面2个HEX字符,nnnnnnnn则是build ID的剩余部分。
举例来说,调试/usr/bin/ls的时候,如果debug link指定了 ls.debug,并且具有build ID是abcdef1234,并且全局debug目录包括/usr/lib/debug,那么以下位置用于寻找调试文件:
- /usr/lib/debug/.build-id/ab/cdef1234.debug
- /usr/bin/ls.debug
- /usr/bin/.debug/ls.debug
- /usr/lib/debug/usr/bin/ls.debug
全局debug目录取决于GDB的配置项 --with-separate-debug-dir。
你也可以使用交互式命令查看、设置全局dbug目录:
1 2 3 4 |
# 查看 (gdb) show debug-file-directory # 设置,path separator分隔 (gdb) set debug-file-directory directories |
找到调试文件,是设置断点的前提条件。运行到断点之后,你可能遇到:res_send.c: No such file or directory. 这样的错误,这是因为没有找到源码。可以用下面的命令来设置源码路径:
1 2 3 |
(gdb) directory /glibc-2.27/resolv # 指定多个源码路径 (gdb) directory /glibc:$cdir:$cwd |
1 2 3 4 5 6 7 8 9 |
gdb [-help] [-nh] [-nx] [-q] [-batch] [-cd=dir] [-f] [-b bps] [-tty=dev] [-s symfile] [-e prog] [-se prog] [-c core] [-p procID] [-x cmds] [-d dir] [prog|prog procID|prog core] # 连接到指定的进程以调试 gdb -p 1234 # 调试应用程序 gdb /path/to/program |
选项 | 说明 |
-s | 从指定的文件中读取符号表 |
-e | 指定可执行文件,在适合(发起run命令)的时候执行。也用于和core dump联用来检查pure data |
-se | 从文件读取符号表同时将其作为可执行文件 |
-c | 指定需要检查的core dump |
-x | 从文件中读取GDB命令 |
-ex |
执行指定的GDB命令 |
-d | 指定搜索源码文件的目录 |
-cd | 指定工作目录 |
-write | 启用写入到可执行文件/core文件 |
-tty | 使用指定的设备作为程序的stdio |
-p |
Attach到指定的进程 |
-tui | 使用CLUI模式启动,在容器中运行的GDB,CLUI模式下显示有问题,建议从宿主机上进入容器的空间执行GDB |
子命令 | 说明 | ||
常用 | |||
target |
连接到目标,例如gdbserver或者进程:
|
||
attach | 附着到进程进行调试 | ||
detach | 解除对进程的调试 | ||
<ENTER> | 再次执行上一个命令 | ||
ctrl + c | 立即暂停正在运行中的程序,暂停在当前正在执行的行,等待后续调试指令 | ||
break |
设置断点,注意,你可能需要设置在期望函数的上一行,否则它可能是在调用函数结束之后才暂停,而不是调用开始前 示例:
|
||
tbreak | 一次性断点 | ||
c |
continue,让程序从暂停中恢复,当遇到下一个breakpoint或者ctrl + c程序会再次暂停 c N 继续且忽视当前断点N次 |
||
until | until line-number,一直执行,直到指定的行数 | ||
s |
step,执行一行代码,如果正在执行的是函数,则进入此函数。相当于大部分调试工具的step into s N 则执行N行代码 |
||
si | step的精确版,每次仅执行一个汇编指令,可能需要多次才能执行完一行 | ||
n | next,执行到下一行,相当于大部分调试工具的step over | ||
ni | next的精确版,每次仅执行一个汇编指令,可能需要多次才能执行完一行 | ||
f | 运行直到当前函数结束,相当于大部分调试工具的step out | ||
l | list,显示当前行附近的代码。默认显示20行 | ||
bt | backtrace,打印栈追踪 | ||
u | 转到栈帧的上一级别 | ||
d | 转到栈帧的下一级别 | ||
where | 打印所有栈帧的信息,包括指令地址、函数名、源码位置。如果在调试时源码路径映射不对(导致显示反编译代码),可以通过此命令知晓构建时的源码路径 | ||
directory |
添加指定的目录到源码搜索路径中 示例:
|
||
i info |
显示各种信息: info all-registers 列出所有寄存器及其值 示例:
|
||
set |
可以用来设置变量值:
|
||
p | print,可以用来打印变量值: print varname | ||
help | 使用 help commandname可以查看某个命令的帮助 | ||
q | quit,退出GDB | ||
断点 | |||
watch | 为表达式设置监控点,例如 watch x > 1表示当x大于1时暂停执行 | ||
break-range | 为地址范围设置断点 | ||
catch | 设置捕获点以捕获事件 | ||
commands | 指定到达断点时执行的命令 | ||
delete | 删除断点或自动显示表达式 | ||
skip | 单步跟踪时,忽略指定的函数、文件 | ||
tbreak | 设置临时断点 | ||
disable | 禁用断点 | ||
enable | 启用断点 | ||
程序操控 | |||
advance |
运行到指定位置:
|
||
finish | 执行直到选中的栈帧返回 | ||
handle | 指定如何处理信号 | ||
jump | 跳转正在执行的程序到指定的行或地址 | ||
kill | 杀死被调试的程序 | ||
queue-signal | 入队一个信号,在当前线程resume后传递给它 | ||
run | 启动被调试的程序 | ||
start | 启动被停止在主程序开始处的程序 | ||
thread | 切换到其它线程 | ||
thread find | 使用正则式查找线程 | ||
thread name | 设置当前线程的名称 | ||
文件和路径 | |||
set directories | 设置源码搜索路径 | ||
set substitute-path | set substitute-path from to 设置查找源码使用的路径映射 | ||
set sysroot |
设置包含了调试目标的库的副本的目录,当连接到gdbserver进行远程调试时有用
如果运行gdbserver的远程服务器、运行gdb的本地服务器,上面的库不匹配,设置此变量可以解决找不到调试文件的问题 |
||
add-symbol-file | 从文件加载符号表 | ||
cd | 设置调试器的工作目录 | ||
core-file | 使用指定的文件作为coredump,用于检查内存和寄存器 | ||
generate-core-file | 生成正在调试的程序的coredump | ||
load | 动态的加载文件到当前程序中 | ||
nosharedlibrary | 卸载所有共享库的符号 | ||
状态和信息 | |||
info address |
描述指定的符号在何处存储 |
||
info symbol | 显示指定地址的符号 | ||
info all-registers | 列出所有寄存器及其值 | ||
info args | 显示当前栈帧的参数值 | ||
info locals | 显示当前栈帧的本地变量值 | ||
info variables | 显示全局和静态变量的值 | ||
info breakpoints | 显示断点状态 | ||
info files | 目标的名称以及被调试的文件 | ||
info frame | 选中栈帧的信息 | ||
info functions | 列出所有函数名 | ||
info macro | 显示宏定义 | ||
info macros | 显示指定 LINESPEC的所有宏定义 | ||
info program | 显示程序的执行状态 | ||
info stack | 显示栈的追踪 | ||
info target | 显示正在调试的目标名 | ||
info threads | 显示线程列表 | ||
info source | 显示当前(被中断时断点所在文件)源文件的信息 | ||
info sources |
显示源文件列表。启动gdb时,其source path仅仅包含:
$cdir 编译目录 |
||
info set | 显示所有GDB设置 | ||
info proc cwd | 显示当前被调试程序的工作目录 | ||
info proc exe | 显示当前被调试程序的可执行文件的路径 | ||
show architecture | 显示目标体系结构 | ||
show args | 显示被调试目标启动时的参数 | ||
show version |
显示GDB版本信息 |
||
show sysroot | 显示当前的system root | ||
show substitute-path | 显示当前的源码映射 | ||
反汇编 | |||
disassemble | 反汇编,打印所有指令: disassemble main | ||
x/8i | 反汇编,打印前8个指令: x/8i main |
这是一个控制程序,允许你通过Remote GDB连接到应用程序。要在目标系统中运行gdbserver,你需要应用程序的二进制文件,但是不一定需要符号表。
1 2 3 |
gdbserver [OPTIONS] COMM PROG [ARGS ...] gdbserver [OPTIONS] --attach COMM PID gdbserver [OPTIONS] --multi COMM |
其中:
- COMM:要么是一个TTY设备(串口调试),要么是HOST:PORT(用于监听GDB客户端连接),还可以指定 - 或者 stdio,表示使用gdbserver的标准输入/输出
- PROG ARGS:被调试的可执行文件的路径和参数
- PID:需要附着到的进程的PID
选项 | 说明 |
--attach | 附着到指定进程 |
--multi | 不指定需要执行/Attach的应用程序,仅仅在显示给出命令时才退出 |
--once | 第一个连接关闭后,关闭服务器 |
--wrapper WRAPPER -- | 通过运行WRAPPER来启动应用程序 |
--disable-randomization --no-disable-randomization |
运行应用程序是,是否禁用地址空间随机化 |
--startup-with-shell --no-startup-with-shell |
是否在Shell中运行应用程序 |
--debug | 启用一般性的调试输出 |
1 2 |
# 附着到正在运行的程序,在2345端口上等待客户端连接 gdbserver --attach 0.0.0.0:2345 449 |
这是GDB自带的前端,使用 gdb -tui即可启动。
命令 | 说明 |
ctrl-x o | 窗口之间切换焦点 |
layout |
设置窗口布局: src 上面源码、下面命令 |
ctrl-l | 重新绘制窗口 |
Emacs编辑器提供的功能。按 M-x,输入gdb即可使用。
输入命令 gdb-many-windows可以进入多窗口模式。默认6个窗口,分别是GDB终端、本地变量列表、源码、栈帧、断点。
输入命令 gdb-display-disassembly-buffer可以将某个窗口切换为反汇编。
命令 | 功能 | 默认快捷键 | 熟悉快捷键 |
添加断点 | gud-break | C-x C-a C-b | |
删除断点 | gud-remove | C-x C-a C-d | |
继续运行程序 | gud-go | ||
单步执行,不进入函数 | gud-next | C-x C-a C-n | F6 |
单步执行,进入函数 |
gud-step |
C-x C-a C-s | F5 |
跳出当前函数 | gud-finish | C-x C-a C-f | F7 |
运行到光标所在语句 | gud-until | C-x C-a C-u | Ctrl+F8 |
继续运行程序 | gud-cont | C-x C-a C-r | F8 |
如果不习惯Emacs的快捷键,可以使用cgdb,它更加简洁,vim用户友好。
默认情况下,cgdb将控制台分为两个窗口:
- 源码窗口:使用vim风格命令,输入:quit可以退出
- GDB窗口:使用GDB命令,输入quit或者C-d退出
可用窗口列表:
窗口 | 说明 | ||||||||
源码窗口 |
在这个窗口查看源码,使用快捷键进行单步跟踪。支持C, C++语法高亮,支持源码导航导航到指定的源码,然后按Space即可设置断点。支持在源码中基于正则式进行搜索 快捷键:
命令:
命令可以写在 ~/.cgdb/cgdbrc文件中,不需要前导的冒号。 map命令用于修改按键映射,例如:
map <F2> ip<Space>argc<CR>表示,按F2后,会自动输入i进入GDB模式,然后输入 p空格argc回车 下面的配置修改成Eclipse风格的调试快捷键:
其中尖括号中的是keycode:
|
||||||||
GDB窗口 | 在这个窗口中可以使用GDB命令 | ||||||||
文件对话框 |
用于选择需要查看的文件 在源码窗口输入 o即可打开对话框,输入 q退出对话框 |
||||||||
状态栏 |
最右侧是 *表示当前焦点在GDB窗口,按 ESC即可让源码窗口获得焦点(进入CGDB模式),输入 i焦点回到GDB窗口(进入GDB模式) |
Linux或者UNIX下C语言程序入口函数的标准声明方式如下:
1 2 3 4 |
int main(int argc, char *argv[]); //argc 表示参数的个数,其中第一个参数是程序的名称 //argv 表示参数的数组 //多个参数使用空白符区分,如果单个参数中间有空白符,使用引号包围该参数 |
下面的代码说明如何读取程序参数:
1 2 3 4 5 6 7 8 9 10 11 12 |
#include <stdio.h> #include <stdlib.h> int main( int argc, char *argv[] ) { int arg; for ( arg = 0; arg < argc; arg++ ) { if ( argv[arg][0] == '-' ) printf( "选项: %s\n", argv[arg] + 1 ); else printf( "参数: %s\n", argv[arg] ); } exit( 0 ); } |
X/Open规范定义了命令行选项的标准格式,并提供了获取命令行选项的接口:getopt函数:
1 2 3 4 5 6 7 8 9 |
#include <unistd.h> int getopt(int argc, char *const argv[], const char *optstring); //optstring:字符列表,表示支持的选项,每个字符代表一个选项,:表示前面的选项需要一个值 //例如if:lr表示支持4个选项,其中f后面紧跟一个关联值 extern char *optarg; //optind存放下一个待处理参数的索引 //如果遇到无法识别的选项,getopt返回一个?并将此选项保存到optopt中 //如果一个选项要求关联值,但是没有传递,则getopt返回一个? extern int optind, opterr, optopt; |
反复调用getopt,即可依次得到选项,示例如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
#include <stdio.h> #include <unistd.h> #include <stdlib.h> int main( int argc, char *argv[] ) { int opt; //直到返回值是-1,依次读取 while ( ( opt = getopt( argc, argv, ":if:lr" ) ) != -1 ) { switch ( opt ) { case 'i' : case 'l' : case 'r' : printf( "option: %c\n", opt ); break; case 'f' : printf( "filename: %s\n", optarg ); //读取参数值 break; case ':' : printf( "option needs a value\n" ); break; case '?' : printf( "unknown option: %c\n", optopt ); //读取未知参数名 break; } } for ( ; optind < argc; optind++ ) printf( "argument: %s\n", argv[optind] ); exit( 0 ); } |
GNU C函数库支持双横线开头的长参数,函数为getopt_long,有关联值的参数使用 --option=value 的形式给出。该函数用法示例如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
#include <stdio.h> #include <unistd.h> #include <stdlib.h> #define _GNU_SOURCE #include <getopt.h> int main( int argc, char *argv[] ) { int opt; struct option longopts[] = { //此结构的定义如下: //name:长选项的名字,缩写亦可,只要不与其它选项混淆 //has_arg:是否带参数,0表示不带;1表示必须有一个参数;2表示有一个可选参数 //flag:设置为NULL,那么找到该选项时,getopt_long返回val中给定的值(对应的短选项) // 否则,getopt_long返回0并且将val的值写入flag指向的变量 //val:getopt_long为该选项返回的值 { "initialize", 0, NULL, 'i' }, { "file", 1, NULL, 'f' }, { "list", 0, NULL, 'l' }, { "restart", 0, NULL, 'r' }, { 0, 0, 0, 0 } }; while ( ( opt = getopt_long( argc, argv, ":if:lr", longopts, NULL ) ) != -1 ) { switch ( opt ) { case 'i' : case 'l' : case 'r' : printf( "option: %c\n", opt ); break; case 'f' : printf( "filename: %s\n", optarg ); break; case ':' : printf( "option needs a value\n" ); break; case '?' : printf( "unknown option: %c\n", optopt ); break; } } for ( ; optind < argc; optind++ ) printf( "argument: %s\n", argv[optind] ); exit( 0 ); } |
1 2 3 4 5 |
#include <stdlib.h> //读取环境变量 char *getenv(const char *name); //设置环境变量,以name=value形式的字符串作为参数 int putenv(const char *string); |
除了上面两个方法外,可以通过environ变量直接访问环境变量:
1 2 3 |
#include <stdlib.h> //字符串数组 extern char **environ; |
示例如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
#include <stdlib.h> #include <stdio.h> extern char **environ; int main() { char **env = environ; while ( *env ) { printf( "%s\n", *env ); env++; } exit( 0 ); } |
UNIX/Linux下的时间的表示,均是以格林尼治(GMT)时间1970-01-01 00:00:00为起点进行计时的,表示为从这一时间点流逝的时间。时间使用预定义类型: time_t 表示,通过以下函数可以得到当前时间值(流逝的秒数):
1 2 3 4 5 |
#include <time.h> //如果tloc不为NULL,那么返回值同时写入到该指针 time_t time( time_t *tloc ); //把结构还原为时间值 time_t mktime(struct tm *timeptr); |
difftime函数可以返回两个时间点之间的差值:
1 2 3 |
#include <time.h> //返回time1 - time2 double difftime(time_t time1, time_t time2); |
使用gmtime函数可以把time_t分解为一个结构:
1 2 3 4 5 6 7 8 9 10 11 12 |
#include <time.h> struct tm *gmtime( const time_t timeval ); //结构成员包括: //int tm_sec,秒0-61 //int tm_min,分0-59 //int tm_hour,时0-23 //int tm_mday,月份中的日期1-31 //int tm_mon,月份0-11 //int tm_year,年度,从1900开始算 //int tm_wday,星期几,0-6,周日为0 //int tm_yday,年份中的日期,0-365 //int tm_isdst,是否夏令时 |
以上函数处理的都是GMT标准时间,如果需要得到当地时间,可以使用:
1 2 |
#include <time.h> struct tm *localtime(const time_t *timeval); |
以人类易于阅读的方式输出日期,可以使用:
1 2 3 4 5 |
#include <time.h> char *asctime( const struct tm *timeptr ); //输出格式为固定26字符:Sun Jun 9 12:34:56 2007\n\0 //下面这个函数等价于:asctime(localtime(timeval)) char *ctime( const time_t *timeval ); |
Linux提供了strftime函数,可以对日期格式进行细致的格式化:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
#include <time.h> size_t strftime( char *s, size_t maxsize, const char *format, struct tm *timeptr ); /** * 其中,format为格式化字符串,类似于printf: * %a 星期几的缩写 * %A 星期几的全称 * %b 月份的缩写 * %B 月份的全称 * %c 日期和时间 * %d 月份中的日期01-31 * %H 小时0-23 * %I 12小时制中的小时01-12 * %j 年份中的日期,001-366 * %m 年份中的月份,01-12 * %M 分钟,00-59 * %p a.m表示上午,p.m.下午 * %S 秒,00-61 * %u 星期几,1-7,周一为1 * %U 一年中的第几周,01-53(周日算第一天) * %V 一年中的第几周,01-53(周一算第一天) * %w 星期几,0-6,周日为9 * %x 本地格式的日期 * %X 本地格式的时间 * %y 年份-1900 * %Y 年份 * %Z 时区名 * %% 字符% */ |
1 2 3 4 5 6 7 8 9 10 |
#include <stdio.h> //创建一个唯一的文件名,缓冲区至少L_tmpnam长 char *tmpnam( char *s ); //创建一个唯一的临时文件,以读写方式打开 FILE *tmpfile( void ); //来自UNIX的临时文件函数,字符串模板必须以XXXXXX结尾,这6个字符会被替换 #include <stdlib.h> char *mktemp( char *template ); int mkstemp( char *template ); |
除了init之外,所有Linux进程均是由其它程序或者用户启动的。每个程序都是以某个用户的名义去运行的,用户的内部识别方式是UID。
一般情况下,启动程序的用户的UID,即是程序的有效UID;如果程序的SUID位被设置,那么程序的所有者将作为其有效UID,不论谁启动该程序。
UID的类型是 uid_t ,定义在头文件 sys/types.h 中,是一个小整数,某些UID是系统预定义的,其它则是系统管理员添加的,一般情况下UID大于100。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
#include <sys/types.h> #include <unistd.h> //得到程序关联的UID,通常是启动程序的用户的ID uid_t getuid( void ); //返回当前用户关联的登录名 char *getlogin( void ); #include <sys/types.h> #include <pwd.h> //获取用户信息的标准接口,出错时返回空指针并设置errno struct passwd *getpwuid( uid_t uid ); struct passwd *getpwnam( const char *name ); /** * passwd结构成员: * char *pw_name 用户登录名 * uid_t pw_uid UID号 * gid_t pw_gid GID号 * char *pw_dir 家目录 * char *pw_gecos 用户全名 * char *pw_shell 默认Shell */ |
uname命令可以获取主机的若干信息,例如主机名等。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
#include <unistd.h> //获取主机名,字符串被假设最少有namelen长,成功返回0否则返回-1 int gethostname( char *name, size_t namelen ); #include <sys/utsname.h> //使用uname系统调用,可以获取更多的信息 int uname( struct utsname *name ); struct utsname { char sysname[]; //操作系统名称 char nodename[]; //主机名称 char release[]; //操作系统的发布级别 char version[]; //操作系统的版本号 char machine[]; //硬件类型 }; //得到唯一的机器ID #include <unistd.h> long gethostid( void ); |
Linux系统文件一般被存放到专用的目录,例如/usr/adm或者/var/log目录。UNIX规范通过syslog函数为所有程序产生日志信息提供了统一接口:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
#include <syslog.h> //该函数向系统的日志设施(facility)发送一条日志消息 //priority:严重级别与实施值的位或 //LOG_EMERG 紧急情况,该信息可能广播给所有用户 //LOG_ALERT 高优先级故障,例如数据库崩溃,该信息可能EMAIL管理员 //LOG_CRIT 严重错误,例如硬件故障 //LOG_ERR 错误 //LOG_WARNING 警告 //LOG_NOTICE 需要注意的特殊情况 //LOG_INFO 一般信息 //LOG_DEBUG 调试信息,该信息可能被忽略,其它级别的一般均记录日志 // //message:基于printf风格的消息模板,%m表示当前errno //arguments:模板参数 void syslog( int priority, const char *message, arguments...); //其它函数 //关闭openlog打开的文件描述符 void closelog( void ); //该函数用于改变日志的表示方式,该函数会打开一个文件描述符,通过它写日志,使用syslog前并不是必须调用该函数 //ident:添加在日志信息前面的前缀 //logopt:对后续syslog调用的行为进行配置,是以下四个值的位或: //LOG_PID 在日志信息中包含进程标识符 //LOG_CONS 如果无法记录到文件,信息被发送到控制台 //LOG_ODELAY 第一次调用syslog时才打开日志设施 //LOG_NDELAY 立即打开日志设施 //facility:后续syslog调用的默认设施值 void openlog( const char *ident, int logopt, int facility ); //设置日志掩码,来控制日志的优先级,没有在此掩码中置位的优先级,在后续syslog调用中,全部被丢弃 int setlogmask( int maskpri ); |
syslog创建的日志包含消息头和消息体,消息体由设施值和时间导出,消息体由调用者提供。syslog使用示例如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
#include <syslog.h> #include <stdio.h> #include <unistd.h> #include <stdlib.h> int main() { int logmask; openlog( "logmask", LOG_PID | LOG_CONS, LOG_USER ); syslog( LOG_INFO, "informative message, pid = %d", getpid() ); syslog( LOG_DEBUG, "debug message, should appear" ); logmask = setlogmask( LOG_UPTO( LOG_NOTICE ) ); //设置日志掩码,改变优先级 syslog( LOG_DEBUG, "debug message, should not appear" ); exit( 0 ); } |
Linux系统上运行的程序会受到资源限制的影响,这些限制包括硬件方面的物理性限制、系统策略的限制、具体实现的限制(例如整数的长度、文件名允许的最大字符数)。
头文件limits.h包含了一些操作系统方面限制的显式常量:
常量 | 说明 |
NAME_MAX | 文件名最大字符数 |
CHAR_BIT | char类型值的最大位数 |
CHAR_MAX | char类型的最大值 |
INT_MAX | int类型的最大值 |
头文件sys/resource.h提供了资源操作方面的定义:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
#include <sys/resource.h> #include <sys/time.h> /** * 获取优先级。默认的优先级是0,正数用于后台任务,负数使一个程序运行的更加频繁,获得更多CPU时间 * 优先级的有效范围:-20 ~ +20 * @param which 指明who参数的含义:PRIO_PROCESS 进程标识符;PRIO_PGRP 进程组;PRIO_USER 用户 * @param who 标识符 * @return 成功时返回优先级,失败返回-1并设置errno,因为-1本身是有效的优先级,所有调用函数前应当置errno为0 */ int getpriority( int which, id_t who ); //设置优先级。普通用户只能降低优先级,不能提高 int setpriority( int which, id_t who, int priority ); /** * 获取资源限制 * @param resource 资源类型,RLIMIT_开头的若干常量 */ int getrlimit( int resource, struct rlimit *r_limit ); struct rlimit { rlim_t rlim_cur; //当前软限制,建议性的最好不要超过的限制,如果超过可能导致库函数返回错误 rlim_t rlim_max; //硬限制,如果被超越,系统可能发送信号终止程序 }; int setrlimit( int resource, const struct rlimit *r_limit ); /** * 将CPU时间信息写入到r_usage中 * @param who RUSAGE_SELF 仅返回当前程序使用的信息;RUSAGE_CHILDREN 包括子进程的使用信息 */ int getrusage( int who, struct rusage *r_usage ); struct rusage { struct timeval ru_utime; //使用的用户时间(程序自身的指令消耗的时间) struct timeval ru_stime; //使用的系统时间(操作系统为程序执行所消耗的时间,包括I/O等系统调用、其它系统函数调用) }; |
C标准库函数可以用来分配内存:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
#include <stdlib.h> //注意,所有这些内存分配函数,都不保证两次调用分配的内存是连续的,不能假设并进行连续寻址 /** * 分配指定大小的内存 */ void *malloc( size_t size ); /** * 用于为数组分配内存,所分配的内存全部初始化为0 * @param number_of_elements 数组元素个数 * @param element_size 单个元素的大小 * @return 指向第一个元素的指针 */ void *calloc( size_t number_of_elements, size_t element_size ); /** * 扩大或者减小已分配内存块的长度,可能需要移动整个内存块的位置 * 注意,一定要使用新的指针,而不是existing_memory的指针进行后续访问 * 如果无法调整内存块的大小,该函数返回空指针 */ void *realloc( void *existing_memory, size_t new_size ); |
当物理内存耗尽时,内存分配会启用交换空间(Swap space),交换空间与Windows的虚拟内存交换文件类似,但是没有需要代码处理的局部堆、全局堆、可丢弃内存段等内容——Linux内核会管理所有工作。
Linux实现了“按需换页的虚拟内存系统”,程序看到的内存全部是虚拟的,Linux将内存按页划分,通常每页4096字节。
Linux内存管理系统能保护系统的其它部分免受错误使用内存的程序的影响,每个程序只能看到属于自己的内存映像,尝试访问不属于自己的内存空间,会导致段错误(Segmentation fault)。
空指针操作:glibc可能允许某些空指针读操作,输出“(null)\0”;空指针写、直接空指针读都是禁止的,会导致程序终止。
动态分配的内存在使用完毕后,应当调用free,将内存归还系统:
1 2 |
#include <stdlib.h> void free(void *ptr_to memory); |
free函数接受的指针必须指向由malloc、calloc、realloc调用所分配的内存。
下面的几个函数用于连接的初始化与关闭,以及读写数据:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 |
/** * 首先,需要使用该函数初始化MySQL连接句柄 * @param MYSQL* 使用既有的句柄,一般NULL */ MYSQL *mysql_init( MYSQL * ); /** * 再正式连接之前,可以调用该函数提供一些选项,每次调用只能设置一个选项 * @param option_to_set 选项,支持: * const unsigned int * MYSQL_OPT_CONNECT_TIMEOUT 连接超时时间 * NULL MYSQL_OPT_COMPRESS 启用网络流量压缩 * const char * MYSQL_INIT_COMMAND 连接成功后指向的命令 * * @return 成功后返回0 */ int mysql_options( MYSQL *connection, enum option_to_set, const char *argument ); /** * 然后,提供参数进行连接 * @return 如果无法连接,返回NULL,可以通过mysql_error函数获取细节 */ MYSQL *mysql_real_connect( MYSQL *connection, //指向已经由mysql_init初始化的结构 const char *server_host, //主机名或者IP地址 const char *sql_user_name, //MySQL用户名,为空则使用当前Linux的登录ID const char *sql_password, //密码,为空则只能访问不需要密码的资源 const char *db_name, //使用的MySQL数据库 unsigned int port_number, //如果使用默认端口,传递0 const char *unix_socket_name, //一般传递NULL unsigned int flags ); //位或标记 /** * 执行SQL语句 * @return 如果成功返回0 */ int mysql_query( MYSQL *connection, const char *query ); //检查语句影响的行数 my_ulonglong mysql_affected_rows( MYSQL *connection ); //逐行提取结果集 MYSQL_RES *mysql_use_result( MYSQL *connection ); /** * 返回全部结果集 * @return 如果成功,返回结果集指针,否则返回NULL */ MYSQL_RES *mysql_store_result( MYSQL *connection ); //上述函数调用成功后,应当调用下面的函数来获取结果集的行数 my_ulonglong mysql_num_rows( MYSQL_RES *result ); //读取下一行数据,如果没有更多的数据返回NULL MYSQL_ROW mysql_fetch_row( MYSQL_RES *result ); //读取某个字段 MYSQL_FIELD *mysql_fetch_field( MYSQL_RES *result ); //可以在结果集中跳转到指定的偏移量,如果指定为0,那么下一次fetch_row返回第一行 void mysql_data_seek( MYSQL_RES *result, my_ulonglong offset ); //获取当前游标在结果集中的偏移量 MYSQL_ROW_OFFSET mysql_row_tell( MYSQL_RES *result ); //根据偏移量移动游标,返回先前的偏移量 MYSQL_ROW_OFFSET mysql_row_seek( MYSQL_RES *result, MYSQL_ROW_OFFSET offset ); /** * 返回结果集中字段的个数 */ unsigned int mysql_field_count( MYSQL *connection ); //释放结果集相关资源 void mysql_free_result( MYSQL_RES *result ); /** * 最后使用完毕后,需要关闭MySQL连接,该函数调用后,MySQL结构被释放,指针失效无法再次使用 */ void mysql_close( MYSQL *connection ); /** * 获取错误代码 */ unsigned int mysql_errno( MYSQL *connection ); /** * 获取错误文本,这些文本存放在MySQL内部静态空间 */ char *mysql_error( MYSQL *connection ); |
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
setvbuf( stdout, NULL, _IONBF, 0 ); MYSQL* conn = mysql_init( NULL ); const unsigned int timeout = 5; mysql_options( conn, MYSQL_OPT_CONNECT_TIMEOUT, ( char* ) &timeout ); if ( !mysql_real_connect( conn, "127.0.0.1", "root", "root", "test", 0, NULL, 0 ) ) { printf( "Failed to connect: %s\n", mysql_error( conn ) ); } if ( !mysql_query( conn, "INSERT INTO T_CORP (CORP_NAME, CAPI) VALUES ('Gmem Inc.', 1000.20)" ) ) { printf( "Inserted %lu rows", ( unsigned long ) mysql_affected_rows( conn ) ); mysql_query( conn, "SELECT LAST_INSERT_ID()" ); MYSQL_RES *res_ptr = mysql_use_result( conn ); //使用结果集 MYSQL_ROW sqlrow; //代表行的字符串数组 while ( ( sqlrow = mysql_fetch_row( res_ptr ) ) ) //循环读取结果集 { printf( ", Last Corp ID: %s", sqlrow[0] ); } mysql_free_result( res_ptr ); //释放结果集 } else { printf( "Failed to execute SQL: %s\n", mysql_error( conn ) ); } mysql_close( conn ); |
Leave a Reply