记录C/C++库从32位机器向64为机器移植过程中导致的问题和解决方案

最近程序库从32为向64为机器移植后,在使用过程中出现了一些问题,其中包括上一篇中记录的由于va_list类型不一致导致程序core dump或者出现异常行为的原因,这篇文章记录一下库移植之后出现的小问题。持续更新…

问题一: unsigned long int类型长度不兼容并对其按位操作

一般情况下在32位和64位机器单纯使用该类型在不出现数字大小溢出的情况下一般不会出现问题。但是有一种情况需要特别注意,即当对该unsigned long int进行位操作时,此时的操作结果在32和64位机器的操作结果会不同而导致其他问题。

例如使用unsigned long int类型作为key对其按位进行加密或哈希时,这时的加密或哈希结果会出现问题。

解决方法

将unsigned long int类型换为uint32_t在64位机器上重新编译

问题二: 32位和64位默认对齐不同导致数据类型大小不同

在linux系统下,32位机器数据默认以4字节对齐,64为机器默认以8字节对齐。

看如下结构构体:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>
struct Test{
int a;
char* data[0];
};
int main() {
std::cout << sizeof(Test) << std::endl;
Test t;
std::cout << "&t:" << &t << std::endl
<< "&t.a" << &t.a << std::endl
<< "&t.data:" << &t.data << std::endl;
return 0;
}

32位机器运行结果:

1
2
3
4
8
&t:0x7ffd49f978b0
&t.a0x7ffd49f978b0
&t.data:0x7ffd49f978b4

64位机器运行结果:

1
2
3
4
8
&t:0x7ffd49f978b0
&t.a0x7ffd49f978b0
&t.data:0x7ffd49f978b8

解决方案:

在声明结构体前面加上#pragma pack(1)或#pragma pack(4),强制改变结构体的对齐方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>
#pragma pack(4)
struct Test{
int a;
char* data[0];
};
int main() {
std::cout << sizeof(Test) << std::endl;
Test t;
std::cout << "&t:" << &t << std::endl
<< "&t.a" << &t.a << std::endl
<< "&t.data:" << &t.data << std::endl;
return 0;
}

Comment and share

C中va_list在32位和64位机器的区别与差异

在将程序从32位机器移植到64位机器的过程中经常出现一些奇奇怪怪的错误,这里记录一下在使用可变参数的过程中导致在32位机器上正常运行的程序移植到64位机器上之后出现段错误的发现过程以及解决方案。

首先看下面一段代码:

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
#include <iostream>
#include <stdio.h>
#include <stdarg.h>
#include <string.h>
void parse(va_list ap) {
char* arg;
arg = va_arg(ap, char*);
std::cout << arg << std::endl
<< strlen(arg) << std::endl;
}
void test(const char* format, ...) {
va_list ap;
va_start(ap, format);
for (int i = 0; i < 2; i++) {
parse(ap);
}
va_end(ap);
}
int main() {
test("hget %s %s", "abc", "123456");
}

32位机器的运行结果如下:

1
2
3
4
abc
3
abc
3

64位机器运行结果如下:

1
2
3
4
abc
3
123456
6

原因分析

出现上述结果的原因是由于va_list类型在32位和64位机器的类型不同导致的.

32位va_list

在32位上,va_list的定义为:

1
2
//注意,由于中间宏过多,这里省去了中间如_VA_LIST宏,直接给出实际定义。
typedef va_list char**;

64位va_list

在64位上va_list定义为一个结构体数组,并且数组中记录了可变参数被读的偏移量:

1
2
3
4
5
6
7
// Figure 3.34
typedef struct {
unsigned int gp_offset;
unsigned int fp_offset;
void *overflow_arg_area;
void *reg_save_area;
} va_list[1];

程序异常分析

当在32位机器上将va_list(char**)作为参数传递给函数的时候,该函数将从头开始读取该变长参数,还是使用va_list完毕并不记录当前va_list被读的偏移量,所以当第二次传入该va_list还是从头开始读取。

当在64为机器上将va_list(struct 数组)作为参数传递给函数的时候,该函数读取va_list完毕之后,将读取的偏移量记录在结构体中,由于其为数组传入函数,所以该被调用的函数改变了传入的va_list的偏移量。导致下次调用该函数从记录的偏移量开始读,造成不可预测或者内存越界等问题。

移植解决方案

将va_list初始化写到for循环内部,每次调用函数前都初始化va_list即可。

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
#include <iostream>
#include <stdio.h>
#include <stdarg.h>
#include <string.h>
void parse(va_list ap) {
char* arg;
arg = va_arg(ap, char*);
std::cout << arg << std::endl
<< strlen(arg) << std::endl;
}
void test(const char* format, ...) {
for (int i = 0; i < 2; i++) {
va_list ap;
va_start(ap, format);
parse(ap);
va_end(ap);
}
}
int main() {
test("hget %s %s", "abc", "123456");
}

参考:
https://stackoverflow.com/questions/4958384/what-is-the-format-of-the-x86-64-va-list-structure
http://blog.csdn.net/doubleface999/article/details/55798710

Comment and share

C中snprintf与vsnprintf函数

虽然snprintf函数在开发过程中比较常用,但是其中有一些细节性的问题需要注意。因为snprintf函数不是C中的标准函数,不同的编译器可能对该函数的实现不同,本文说明是基于GCC编译器。

snprintf函数

函数原型

snprintf函数的作用是将格式化的数据写入字符串。其函数原型如下:

1
snprintf(char* buffer, int n, char* format, ...);

参数说明

  • buffer : 存储格式化字符串的buffer
  • n : 指定格式化字符串的大小,包括\0
  • format : 指定需要格式化字符串的format
  • … : 可变参数

返回值

该函数的返回值为其期望字符串的长度,而不是实际存入buffer中的字符串的长度。且不包括\0

注意点

特别注意函数原型中的第二个参数包括\0的大小,而返回值为期望大小且不包括\0。

例:

1
2
3
4
5
6
7
8
char buffer[256];
//返回值ret = 13,buffer中的内容为123456789
int ret = snprintf(buffer, 10, "%s", "1234567890abc");
memset(buffer, 0x0, sizeof(buffer));
//返回值ret = 3,buffer中的内容为123
ret =snprintf(buffer, 10, "%s", "123");

snprintf与vsnprintf

snprintf和vsnprintf都是C语言printf家族函数的成员。实际上,snprintf和vsnprintf功能完全一样,只是vsnprintf将snprintf中的可变参数换成了av_list类型。如下:

1
2
3
4
5
#include <stdio.h>
int printf(const char* format, ...); //输出到标准输出
int fprintf(FILE* stream, const char* format, ...); //输出到文件
int sprintf(char* buffer, const char* format, ...); //输出到字符串
int snprintf(char* buffer, int n, const char* format, ...); //输出到字符串
1
2
3
4
5
#include <stdarg.h>
int vprintf(const char* format, va_list ap); //输出到标准输出
int vfprintf(FILE* stream, const char* format, va_list ap); //输出到文件
int vsprintf(char* buffer, const char* format, va_list ap); //输出到字符串
int vsnprintf(char* buffer, int n, const char* format, va_list ap); //输出到字符串

va_list获取

可变参数va_list获取方式通过下列函数获取,并且总是成对调用

1
2
va_start(va_list ap, last);
va_end(va_list ap);

简单的使用vsnprintf函数实现snprintf

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
#include <stdarg.h>
int my_snprintf(char* buffer, int size, const char* format, ...) {
va_list ap;
va_start(ap, format);
int ret = vsnprintf(buffer, size, format, ap);
va_end(ap);
return ret;
}

Comment and share

Linux调试工具之strings

功能

strings命令的作用是查找指定文本文件或者二进制文件中的可打印字符串。默认打印字符串长度大于等于4的字符串。

参数说明

  • -a : 搜索整个文件,而不仅仅是数据段,以寻找可显示的字符串。如果省略这个标志,那么 strings 命令只在对象文件的初始化数据空间内寻找。
  • -n number(-number):指定寻找打印字符串的最小长度,默认最小长度为4,可以通过此参数设置最小长度为1或2等。最长不能超过4096
  • -o : 显示可打印字符串以及可打印字符串在文件中的位置,位置以8进制形式显示。
  • -t format:显示可打印字符串以及可打印字符串在文件中的位置,format可指定显示位置的格式是(o)8,(d)10,(x)16进制。

实例

1
2
3
4
5
6
7
8
#include <stdio.h>
int main() {
char l_data =0;
int a = 0;
a;
return 0;
}
  1. 直接使用stirngs,默认只显示长度大于等于4的可打印字符串

    1
    2
    3
    4
    5
    6
    $ strings main.cpp
    #include <stdio.h>
    int main() {
    char l_data =0;
    int a = 0;
    return 0;
  2. -n参数

    1
    2
    3
    4
    5
    6
    7
    8
    $strings -1 main.cpp
    #include <stdio.h>
    int main() {
    char l_data =0;
    int a = 0;
    a;
    return 0;
    }
  3. -o 参数

前面为其在文件中的8进制位置

1
2
3
4
5
6
$strings -o main.cpp
0 #include <stdio.h>
24 int main() {
41 char l_data =0;
63 int a = 0;
104 return 0;

  1. -t format参数
    -t o参数与-o参数作用一样
1
2
3
4
5
6
$strings -t d main.cpp
0 #include <stdio.h>
20 int main() {
33 char l_data =0;
51 int a = 0;
68 return 0;

用途

当我们将多个文件编译到一个库文件或者可执行文件的时候,我们可以通过strings命令查看新增文件的一些可打印字符串是否在库中,来判断该文件是否已经编译到库中。

Comment and share

Linux调试工具之readelf

readelf命令可以用来查看elf格式文件的信息,与objdump相比,该工具显示的信息较为详细

elf文件

ELF(excutable and linking format)是一种对象文件格式,用于定义不同类型的对象文件中存放了那些东西,以及以什么格式存放。ELF文件可分为三种:

  • 可重定位对象文件(relocatable file)
  • 可执行对象文件(excutable file)
  • 可被共享的对象文件(shared object file)

可重定位对象文件

可重定位对象文件中包含适合于与其他目标文件链接来创建可执行文件或者共享目标文件的代码和数据。一般包括如汇编器汇编生成的.o文件

可执行文件

可执行文件是一个可执行文件,此文件规定了exec()如何创建一个程序的进程映像

可被共享的对象文件

可被共享的对象文件中包含可在两种上下文中链接的代码和数据。首先链接编辑器可将它与其他可重定位文件和目标文件和可共享目标文件一起处理,生成另外一个目标文件,其次,动态链接器可以将它与某个可执行文件以及其他可共享目标一起组合,创建进程映像。

readelf调试工具

readelf参数较多,可直接通过readelf -h获取readelf的所有参数及用法,此处只对其中的几个常用参数以及数据详解。

readelf -h

readelf -h参数的作用是读取指定elf文件的头信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$ readelf -h
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: EXEC (Executable file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x400800
Start of program headers: 64 (bytes into file)
Start of section headers: 7344 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 9
Size of section headers: 64 (bytes)
Number of section headers: 31
Section header string table index: 28

重要参数解释:

  • Class : 该elf文件的类型,这里是64位的elf格式
  • Data : 该参数指明了文件中数据的组织方式是大端规则还是小端规则,此处为二进制存储的小端规则
  • OS/ABI : 生成该文件的操作系统类型,ABI(Application Binary Interface)
  • Type : 当前文件的类型,EXEC(可执行文件), REL(可重定位文件),DYN(可被共享文件)
  • Machine : 处理器类型
  • Entry point address : 程序的虚拟地址入口点,可执行文件之外的类型该值为0x0
  • Start of program headers : 程序头的开始处,从程序入口地址偏移地址开始。程序头table中记录elf文件中段的信息
  • Start of section headers : 段头的开始处,从程序入口地址偏移地址开始

readelf -s

readelf的作用是用来查看当前elf文件的符号表,符号表中的信息只包括全局变量和函数名
动态符号表(.synsym)用来保存与动态链接相关的导入导出符号,不包括模块内的符号;而sy,tab表则保存所有的符号,包括.dynsym中的符号。
动态符号表所包含的符号的符号名保存在动态符号字符串表.dynstr中。

如下所示程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>
//main.c
int g_data;
static int gs_data;
void hello() {
printf("hello world");
}
int main() {
static int a = 0;
char l_data =0;
hello();
return 0;
}

将上述文件编译汇编成目标文件main.o文件,使用readelf -s main.o,结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Symbol table '.symtab' contains 15 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FILE LOCAL DEFAULT ABS main.cpp
2: 0000000000000000 0 SECTION LOCAL DEFAULT 1
3: 0000000000000000 0 SECTION LOCAL DEFAULT 3
4: 0000000000000000 0 SECTION LOCAL DEFAULT 4
5: 0000000000000004 4 OBJECT LOCAL DEFAULT 4 _ZL7gs_data
6: 0000000000000000 0 SECTION LOCAL DEFAULT 5
7: 0000000000000008 4 OBJECT LOCAL DEFAULT 4 _ZZ4mainE7ls_data
8: 0000000000000000 0 SECTION LOCAL DEFAULT 7
9: 0000000000000000 0 SECTION LOCAL DEFAULT 8
10: 0000000000000000 0 SECTION LOCAL DEFAULT 6
11: 0000000000000000 4 OBJECT GLOBAL DEFAULT 4 g_data
12: 0000000000000000 22 FUNC GLOBAL DEFAULT 1 _Z5hellov
13: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND printf
14: 0000000000000016 24 FUNC GLOBAL DEFAULT 1 main

可以看出全局变量,静态全局变量,静态局部变量,全局函数名都会出现在符号表中,而局部变量不会被保存在符号表中。

Comment and share

VS code配置

in 配置管理

#VS Code配置管理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
{
"[cpp]": {
"editor.quickSuggestions": false
},
"[c]": {
"editor.quickSuggestions": false
},
"window.zoomLevel": 0,
"workbench.colorTheme": "Monokai",
"editor.fontSize": 22,
// insert space when type tab
"editor.insertSpaces": true,
"http.proxy": "http://dev-proxy.oa.com:8080",
"files.autoGuessEncoding": false,
"files.trimTrailingWhitespace": true,
"files.eol": "\n",
"editor.wordWrap": "on",
"editor.minimap.enabled": false,
"workbench.editor.enablePreview": false,
"codegnuglobal.executable": "C:\\Program Files\\glo656wb\\bin\\global.exe",
"C_Cpp.clang_format_path": "C:\\Program Files\\LLVM\\bin\\clang-format.exe"
}

Comment and share

C++截取定长utf8字符及字符集与字符编码总结

之前有个需求要求从一段utf8编码的文字中截取前n个字符,由于文字中包含既有中文又有数字字母等,这些字符所占的字节数都不相同,所以直接截取前M个字符可能会导致乱码问题。在完成截取之前,需要先了解utf8编码字符的存储规则。后面简单介绍一下常见的字符集及字符编码

utf8编码规则

在理解utf8编码规则之前先说一下utf16和utf32的编码规则,utf32规定所有的unicode字符集上的所有字符都是用32bit来存储,即可存储的字符数可达2^32个,接近40亿,这明显可以容纳已有的所有字符。utf16编码使用2个字节存储,超过此范围的使用其他特殊技巧处理。使用utf16和utf32有一个问题,就是会受到计算机存储大端规则和小端规则的影响。所以utf16又有utf16_LE和utf16_BE之分。为了解决这个问题,需要在文本文件的开头定义一个BOM(Byte order Mark),这是一个非打印标志,用此标志来标志当前文本存储是大端存储还是小端存储。

utf8是一种针对unicode的可变长度字符编码,也是一种前缀码,它可以表示unicode标准中的任何字符。且与ascii码兼容,这使得我们处理ascii字符的时候不许做特殊修改。

utf8使用1~6个字符为每个字符编码,但实际使用中只使用了4个。

utf8编码规则:

(1)对于单字节字符,字节的第一位为0,后面7位为这个字符的unicode码,因此对于ascii码,unicode与ascii保持一致。
(2)对于n字节符号(n>1),第一个字节的前n位都为1,第n+1位为0,其他字节的前两位都为10,其余位为实际的unicode位。

规则:

unicode范围 二进制表示范围
U-00000000 - U-0000007F 0xxxxxxx
U-00000080 - U-000007FF 110xxxxx 10xxxxxx
U-00000800 - U-0000FFFF 1110xxxx 10xxxxxx 10xxxxxx
U-00010000 - U-001FFFFF 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
U-00200000 - U-03FFFFFF 111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
U-04000000 - U-7FFFFFFF 1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx

利用上述utf8编码规则实现C++截取定长字符串。

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
const string utf8Cut(const string &src, int utf8Len) {
string ret;
int utf8LenCnt = 0;
int srcIdx = 0;
int srcLen = src.length();
int cutLen = 0;
unsigned char tmp;
while (utf8LenCnt < utf8Len && srcIdx < srcLen) {
tmp = (unsigned char)src[srcIdx];
if (tmp >= 252)
cutLen = 6;
else if (tmp >= 248)
cutLen = 5;
else if (tmp >= 240)
cutLen = 4;
else if (tmp >= 224)
cutLen = 3;
else if (tmp >= 192)
cutLen = 2;
else if (tmp >= 65 && tmp <=90)
cutLen = 1;
else
cutLen = 1;
ret += src.substr(srcIdx, cutLen);
srcIdx += cutLen;
++utf8LenCnt;
}
return ret;
}

字符集与字符编码

字符集是一个系统所支持的字符的集合,常见的字符集包括ascii字符集,GB2312字符集,GBK字符集以及unicode字符集。其中ascii字符集包括所有的英文字母数字以及控制字符等等。

字符编码是用来定义计算机中某个字符的存储和传输规则。

区别:字符集仅仅是一个字符集合,与计算机的存储和传输没有关系,而字符编码规定了计算机中字符的编码规则。

常见的字符集及字符编码

  • ascii字符集及字符编码:使用一个字符编码,包括所有的字母数字,换行以及控制字符等。
  • GB*字符集及编码:是一种多字节编码标准,GB表示国标,包括GB2312,GBK等编码,2312是标准号,GB2312涵盖了绝大多数的中文,但是一些生僻字不包含在其中,GBK是对GB2312的扩展.
  • BIG*:常见的为BIG5字符集及编码,BIG5中收录了大部分的中文繁体,常用语香港台湾等地区。
  • unicode字符集及编码:unicode又称为统一码,几乎涵盖了所有国家和地区的所有字符,并还在不断的扩充新字符。unicode是一个字符集,而不是编码规则,我们常见的utf8,utf16以及utf32是unicode常见的字符编码.

Comment and share

Unix系统调用hook函数以及使用dl库实现

参考链接:http://www.it165.net/os/html/201407/8847.html

系统调用属于一种软中断机制(内中断陷阱),它有操作系统提供的功能入口(sys_call)以及CPU提供的硬件支持(int 3 trap)共同完成。
hook系统调用:为当用户代码调用系统调用的时候,我们通过某种手段入侵该系统调用,使得系统调用中除了执行原有的操作,还可以完成我们需要其完成的一些自定义特性,也可以理解为我们为这个函数做了一个钩子。这样我们就可以实现如当一个文件发生写操作的时候,通知所有关注此文件的进程或线程等等。

通过动态链接库挟持系统调用

在linux操作系统的动态链接库的世界中,LD_PRELOAD就是这样一个环境变量,它可以影响程序的运行时的链接(Runtime linker),它允许你定义在程序运行前优先加载的动态链接库。loader在进行动态链接的时候,会将有相同符号名的符号覆盖成LD_PRELOAD指定的so文件中的符号。换句话说,可以用我们自己的so库中的函数替换原来库里有的函数,从而达到hook的目的。

上述hook系统调用的方法可以使用dlfcn.h库中的一系列函数实现。
example,此函数hook了fcntl系统调用:

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
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
#include <stdarg.h>
#include <dlfcn.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <fcntl.h>
typedef int (*fcntl_pfn_t)(int fildes, int cmd, ...);
int fcntl(int fildes, int cmd, ...)
{
fcntl_pfn_t g_sys_fcntl_func = (fcntl_pfn_t)dlsym(RTLD_NEXT,"fcntl");
va_list arg_list;
va_start( arg_list,cmd );
int ret = -1;
switch( cmd )
{
case F_DUPFD:
{
int param = va_arg(arg_list,int);
ret = g_sys_fcntl_func( fildes,cmd,param );
break;
}
case F_GETFD:
{
ret = g_sys_fcntl_func( fildes,cmd );
break;
}
case F_SETFD:
{
int param = va_arg(arg_list,int);
ret = g_sys_fcntl_func( fildes,cmd,param );
break;
}
case F_GETFL:
{
ret = g_sys_fcntl_func( fildes,cmd );
break;
}
case F_SETFL:
{
int param = va_arg(arg_list,int);
int flag = param;
flag |= O_NONBLOCK;
ret = g_sys_fcntl_func( fildes,cmd,flag );
break;
}
case F_GETOWN:
{
ret = g_sys_fcntl_func( fildes,cmd );
break;
}
case F_SETOWN:
{
int param = va_arg(arg_list,int);
ret = g_sys_fcntl_func( fildes,cmd,param );
break;
}
case F_GETLK:
{
struct flock *param = va_arg(arg_list,struct flock *);
ret = g_sys_fcntl_func( fildes,cmd,param );
break;
}
case F_SETLK:
{
struct flock *param = va_arg(arg_list,struct flock *);
ret = g_sys_fcntl_func( fildes,cmd,param );
break;
}
case F_SETLKW:
{
struct flock *param = va_arg(arg_list,struct flock *);
ret = g_sys_fcntl_func( fildes,cmd,param );
break;
}
}
va_end( arg_list );
return ret;
}
int main() {
int old = fcntl(STDIN_FILENO, F_GETFL);
printf ("old:%d\n", old);
fcntl(STDIN_FILENO, F_SETFL, old);
int _new = fcntl(STDIN_FILENO, F_GETFL);
printf ("new:%d\n", _new);
}

Comment and share

Linux中dlfcn库相关学习

在linux中静态链接库和动态链接库是进程之间代码共享的两种方式。Linux在库中提供了加载和处理动态连接库的系统调用,使用非常方便。具体用法如下:

dlfcn库中函数说明

dlfcn库中主要包括四个函数:

1
2
3
4
5
6
7
8
9
#include <dlfcn.h>
void* dlopen(const char*, int flag);
char* dlerror();
void* dlsym(void* handler, char* symbol);
int dlclose(void* handler);

  • dlopen : 打开一个动态连接库,并返回一个类型为void*的handler,flag为打开模式,可选的模式有两种
    • RTLD_LAZY 暂缓决定,等有需要时再解出符号
    • RTLD_NOW 立即决定,返回前解除所有未决定的符号。
  • dlerror : 返回dl操作的错误,若没有出现错误,则返回NUlL,否则打印错误信息
  • dlsym : 查找动态链接库中的符号symbol,并返回该符号所在的地址
  • dlclose : 关闭动态链接库句柄

使用实例

动态链接库cal.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//cal.cpp
extern "C" {
int add(int a, int b) {
return a + b;
}
int sub(int a, int b) {
return a - b;
}
int mul(int a, int b) {
return a * b;
}
int div(int a, int b) {
return a / b;
}
}

生成动态链接库libcal.so

1
g++ -shared -fPIC cal.cpp libcal.so

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
//main.cpp
#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>
#define LIB_LIBRARY_PATH_1 "./libcal.so"
typedef int (*CAC_FUNC)(int ,int);
int main() {
void* handler = NULL;
char* error = NULL;
CAC_FUNC cac_func = NULL;
handler = dlopen(LIB_LIBRARY_PATH_1, RTLD_LAZY);
if (!handler) {
fprintf(stderr, "err:%s\n", dlerror());
exit(1);
}
dlerror();
//此处取对应函数地址,
*(void **) (&cac_func) = dlsym(handler, "add");
if ((error = dlerror()) != NULL) {
fprintf(stderr, "err:%s", error);
exit(1);
}
printf("add:%d\n", cac_func(1,2));
cac_func = (CAC_FUNC)dlsym(handler, "sub");
printf("sub:%d\n", cac_func(1,2));
cac_func = (CAC_FUNC)dlsym(handler, "mul");
printf("mul:%d\n", cac_func(1,2));
cac_func = (CAC_FUNC)dlsym(handler, "div");
printf("div:%d\n", cac_func(1,2));
dlclose(handler);
return 0;
}

编译函数main:

1
g++ main.cpp -rdynamic -ldl

执行结果:

1
2
3
4
add:3
sub:-1
mul:2
div:0

注意问题

特别注意,若使用c++编译动态链接库,一定要在需要使用的符号处添加extern “C”,否则会出现符号找不到的问题。即undefined symbol

Comment and share

(no title)

AT&T汇编学习

汇编格式主要包括Intel和AT&T汇编格式两种,两种汇编语言的格式在使用上有较大的差别,我们上一章讲解的汇编格式是以intel的风格为例的,但是在Unix和Linux系统中,大部分的汇编语言采用的是AT&T格式。

注:本节中的实例均以i386处理器中汇编指令为例

AT&T汇编与intel汇编格式对比

AT&T intel 说明
pushl %eax push eax 在AT&T格式中,需要在寄存器前面加上%
pushl $1 push 1 在AT&T中使用$作为前缀表示立即数操作
addl $1 %eax add eax 1 AT&T格式的目的操作数和源操作数的顺序与intel格式相反
movl val,%al mov al, byte ptr val AT&T格式操作数的长度由操作符的最后一个字母标识,分别是(movw[word],movl(long)movb[byte])
section:disp(base, index, scale) section:[base + index*scale + disp] 内存寻址方式不同

简单汇编代码分析

下面一段代码的作用是在屏幕上打印helloworld。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
.data # 数据段,该段的数据为初始化数据
msg: .string "helloworld" #定义string类型变量msg,并赋初始值helloworld
len = . - msg #使用段的起始地址和msg地址相减的方式计算msg的长度
.text #代码段初始位置
.global _start #使得连接程序看得到该symbol,这样就可以在其他文件中引用
_start:
movl $len,%edx #edx中存储要打印的数据的长度
movl $msg,%ecx #ecx中存储打印为数据内容
movl $1,%ebx #ebx中为文件描述符,其中1为标准输出流的文件描述符
movl $4,%eax #eax中为系统调用号,4表示sys_write
int $0x80 #调用内核功能执行,此段代码执行后屏幕上打印出helloworld
movl $0,%ebx #0标识退出代码
movl $1,%eax #1为系统调用号(sys_exit)
int $0x80 #调用内核功能运行代码

Comment and share

魏传柳(2824759538@qq.com)

author.bio


Tencent


ShenZhen,China