分类 编程语言 下的文章

参考:extern "C"语句在C++中的作用

一、问题描述

在编译C++程序时,遇到以下问题:

/tmp/cccZeFer.o: In function `main':
main.cpp:(.text+0xf): undefined reference to `****'
collect2: error: ld returned 1 exit status

看到错误的第一直觉是共享库出问题了,因为以前出现这个问题都是因为库没有加进来,但是反复确认过后发现共享库并没有问题。

第一:编译的时候使用-l选项包含了库文件,并且库里面的函数也存在。

第二:库确实存在,不然也不会报上面的错误了,报的错误应该是:

/usr/bin/x86_64-linux-gnu-ld: cannot find -l***
collect2: error: ld returned 1 exit status

试了各种方法都无效,百思不得其解,最后无意间发现竟然是c和c符号表不兼容导致的。

因为库是c编译的,代码是c编译的,c和c的符号表规则不一致,导致编译c时找不到符号,因此编译报错。

二、重现

准备一个库libadd.so和一个源文件main.cpp

> tree
.
├── libadd
│   ├── add.c
│   ├── add.h
│   └── Makefile
├── main.cpp
└── Makefile

1 directory, 5 files

add.hadd.c的内容:

> cat libadd/add.h
int add(int i, int j);

> cat libadd/add.c
#include "add.h"

> cat libadd/Makefile
app:
    gcc add.c -fPIC -shared -o libadd.so

编译libadd.so能够正常编译,然后编译main:

> cat main.cpp 
#include "libadd/add.h"
#include <iostream>

int main() {
    std::cout << add(1, 2) << std::endl;
    return 0;
}

> cat Makefile 
app:
    g++ main.cpp -Llibadd -ladd

此时编译就报错:

/tmp/cccZeFer.o: In function `main':
main.cpp:(.text+0xf): undefined reference to `add'
collect2: error: ld returned 1 exit status

解决方案

在add.h中使用宏定义把函数声明为c导出的函数:

#ifdef __cplusplus
extern "C" {
#endif

int add(int i, int j);

#ifdef __cplusplus
}
#endif

再编译就能通过了。

一、 问题描述

最近工作中遇到了一个问题:项目需要合入其他部门的模块,但是其中的一个共用共享库被更新了。因为项目很大,如果直接在我们的环境中替换更新这个库,很有可能会影响到其他模块。祖传的代码流传了差不多20年,涉及的模块也十分之多,贸然升级的风险很难评估。但是不替换这个库第三方模块又跑不起来,一度头痛。

刚开始是想到了以下几个方法:

  1. 设置LD\_LIBRARY\_PATH环境变量,修改查找路径的优先级。
  2. 修改so库名

对于第一种方法是有效的:在程序目录下加个lib目录,然后 export LD_LIBRARY_PATH=pwd/lib:$LD_LIBRARY_PATH 把当前路径放到第一搜索顺序,能解决这个问题。但是,环境变量时当前所有程序共享的,其他程序的搜索目录也是这里第一,所以这里的结果就和直接升级库没有区别。同样,修改/etc/ld.so.conf的原理也是一样。

第二种方法本来是认为比较靠谱的,但是测试发现共享库并不是根据文件名来的,修改库名无效。so库内部还有个真实的名字,可以通过 readelf -d lib*.so 来查看:

> readelf /usr/lib/libau.so -d

Dynamic section at offset 0x2c88 contains 29 entries:
  Tag        Type                         Name/Value
 0x0000000000000001 (NEEDED)             Shared library: [libdl.so.2]
 0x0000000000000001 (NEEDED)             Shared library: [libpthread.so.0]
 0x0000000000000001 (NEEDED)             Shared library: [libc.so.6]
 0x000000000000000e (SONAME)             Library soname: [libau.so.2]

所以上面两种方法就被派出了,后面查了很久也没有找到合适的办法,崩溃。。。

最后去请教大佬就被告知了可以使用 -Wl,-rpath 选项来解决这个问题,试了一下确实可以。

二、-Wl,-rpath和-Wl,-rpath-link选项

2.1 -Wl,-rpath

加上 -Wl,-rpath 选项的的作用就是指定“程序运行时”的库搜索目录,是一个链接选项,生效于设置环境变量之前。

我们已经知道,共享库的查找顺序为:

  1. LD_LIBRARY_PATH 环境变量的目录
  2. ld.so.conf 高速缓冲文件中的目录
  3. 系统的默认库目录如 /lib, /lib64

-Wl,-rpath 可以让程序在第一步搜索之前先搜索它所指定的目录,通过一个例子来说明:

// add.h
int add(int i, int j);

// add.c
#include "add.h"

int add(int i, int j) {
    return i + j;
}

// main.c
#include <stdio.h>
#include "add.h"

int main() {
    printf("1 + 2 = %d\n", add(1, 2));
    return 0;
}

add.h和add.c用于生成一个so库,实现了一个简单的加法,main.c中引用共享库计算1 + 2:

# 编译共享库
gcc add.c -fPIC -shared -o libadd.so
# 编译主程序
gcc main.o -L. -ladd -o app

编好后运行依赖库:

> ldd app 
    linux-vdso.so.1 (0x00007ffeb23ab000)
    libadd.so => not found
    libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007febb7dd0000)
    /lib64/ld-linux-x86-64.so.2 (0x00007febb83d0000
> ./app 
./app: error while loading shared libraries: libadd.so: cannot open shared object file: No such file or directory

可以看到,libadd.so这个库是没有找到的,程序也无法运行,要运行它必须要把当前目录加到环境变量或者库搜索路径中去。

但是如果在链接的时候加上 -Wl,-rpath 选项之后:


> gcc -Wl,-rpath=`pwd` main.o -L. -ladd -o app
> ldd app 
    linux-vdso.so.1 (0x00007fff8f4e3000)
    libadd.so => /data/code/c/1-sys/solib/libadd.so (0x00007faef8428000)
    libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007faef8030000)
    /lib64/ld-linux-x86-64.so.2 (0x00007faef8838000)
> ./app 
1 + 2 = 3

依赖库的查找路径就找到了,程序能正常运行。

2.2 -Wl,rpath-link

-Wl,rpath-link 是设置编译链接时候的顺序,例如app运行依赖libadd.so,但是libadd.so又依赖libadd\_ex.so,rpath-link 就是指定libadd\_ex.so的路径。和 -Wl,rpath 相比工作的时间不同,一个在链接期间,一个在运行期间。

三、其他相关

程序从编译和链接的过程

linux静态库和动态库的使用方法

‘xxx’: error while loading shared libraries的解决方案

c语言可变长参数传递问题

一、问题描述

C语言中的函数提供了一种可变长参数机制,这个机制使得我们在操作的时候充分自定义自己的功能,例如使用最多的printf函数:

printf("%s: %d", "HelloWorld", 10);

它的函数声明为:

printf(const char *fmt, ...);

其中的...就代表不固定的参数,使用起来十分方便。但是在函数嵌套的时候,不能直接使用...来占位,例如:

#define logerr(s, ...) do { fprintf(stderr, s, ...); } while (0)

编译时就会报错:

va_args.c:4:50: error: expected expression before ‘...’ token
 #define logerr(s, ...) do { fprintf(stderr, "s", ...); } while (0)

如果要嵌套使用,需要通过宏__VA_ARGS__完成:

#define logerr(s, ...) do { fprintf(stderr, s, __VA_ARGS__); } while (0)

二、参数个数为0的问题

使用上面的方法,参数个数为0的时候编译也会报错:

#define logdbg(s, ...) do { fprintf(stderr, s, __VA_ARGS__); } while (0)

int main() {
    logdbg("HelloWorld\n");
    return 0;
}

编译报错:

> gcc va_args.c  -o debug/va_args
va_args.c: In function ‘main’:
va_args.c:5:59: error: expected expression before ‘)’ token
 #define logdbg(s, ...) do { fprintf(stderr, s, __VA_ARGS__); } while (0)

原因是因为参数个数零,预编译后main函数里面的代码变成了:

> gcc -E va_args.c | tail 
int main() {
    const char *msg = "HelloWorld";

    do { fprintf(
# 10 "va_args.c" 3 4
   stderr
# 10 "va_args.c"
   , "HelloWorld\n", ); } while (0);
    return 0;
}

可以看到:fprintf函数的最后是"HelloWorld", );,最后一个逗号和括号之间没有数据,语法不通过。

解决方案

__VA_ARGS__前面加上##,例如:

#include <stdio.h>
#include <stdarg.h>

#define logerr(s, ...) do { fprintf(stderr, s, ##__VA_ARGS__); } while (0)
#define logdbg(s, ...) do { fprintf(stderr, s, ##__VA_ARGS__); } while (0)

int main() {
    const char *msg = "HelloWorld";
    logerr("%s\n", msg);
    logdbg("HelloWorld\n");
    return 0;
}

编译运行:

> make va_args
gcc va_args.c -o debug/va_args
> ./debug/va_args 
HelloWorld
HelloWorld

STL中的string类有两个方法size()length()用来返回字符串的长度。 两者在实现上没有区别:

> sed -n 907,918p /usr/include/c++/7/bits/basic_string.h 
      // Capacity:
      ///  Returns the number of characters in the string, not including any
      ///  null-termination.
      size_type
      size() const _GLIBCXX_NOEXCEPT
      { return _M_string_length; }

      ///  Returns the number of characters in the string, not including any
      ///  null-termination.
      size_type
      length() const _GLIBCXX_NOEXCEPT
      { return _M_string_length; }

一、概述

html中的标签可以分为两种类型:块标签和行标签。两者最明显的区别是块标签会重启一行,而行标签不会。

常见的块标签有:p, h1, ul, div等,常见的行标签有strong, a, font等。

1.1 块标签

对于一个块标签而言,它会新起一行:

<body>
<h1>HelloWorld</h1>
<p>helloworldhelloworld</p>
</body>

- 阅读剩余部分 -

一、信号函数的理解

C语言中信号函数的原型为:

void (*signal(int signo, void (*func)(int)))(int);

这个函数定义看起来十分复杂,可以分为以下两步来理解:

首先看signal(int signo, void (*func)(int))部分,signal是一个函数,它的形参为一个int类型的signo和一个函数指针func。

除去形参部分后剩下的就是返回值了,即void (*)(int)这部分是返回值,它是一个函数指针——这个函数形参类型为int,没有返回值。

- 阅读剩余部分 -

使用exec族函数时抛出以下警告:

exec.c: In function ‘main’:
exec.c:8:3: warning: missing sentinel in function call [-Wformat=]
   if (execlp("/bin/ls", "/bin/ls", "-l", ".") == -1)
   ^

错误的原因在man page中找到:

The execv(), execvp(), and execvpe() functions provide an array of pointers to null-terminated strings that represent the argument list available to the new program.  The  first  argu‐ment, by convention, should point to the filename associated with the file being executed.  The array of pointers must be terminated by a null pointer.

- 阅读剩余部分 -

三者的区别:

  1. return作用于函数,使用return只是退出当前函数,而exit和_exit直接终止程序。
  2. return和exit在退出各自作用域前会自动刷新缓冲区,_exit不会刷新当前缓冲区。

例如以下代码的f函数中使用return,exit和_exit退出的结果都不一样。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

void f() {
    printf("f1\n");
    printf("f2");
    return;
}

int main() {
    f();
    printf("main\n");
    return 0;
}

- 阅读剩余部分 -