HarmonyOS Next开发学习手册——C/C++标准库机制

871 篇文章 13 订阅
504 篇文章 1 订阅

概述

HarmonyOS NDK提供业界标准库 libc标准库 、 C++标准库,本文用于介绍C/C++标准库在HarmonyOS中的机制,开发者了解这些机制有助于在NDK开发过程中避免相关问题。

1. C++兼容性

在HarmonyOS系统中,系统库与应用Native库都在使用C++标准库(参考 libc++版本),系统库依赖的C++标准库随镜像版本升级,而应用Native库依赖的C++标准库随编译使用的SDK版本升级,两部分依赖的C++基础库会跨多个大版本,产生ABI兼容性问题。为了解决此问题,HarmonyOS上把两部分依赖的C++标准库进行了区分。

  • 系统库:使用libc++.so, 随系统镜像发布。
  • 应用Native库:使用libc++_shared.so,随应用发布。

两个库使用的C++命名空间不一样,libc++.so使用__h作为C++符号的命名空间,libc++_shared.so使用__n1作为C++符号的命名空间。

注意:系统和应用使用的C++标准库不能进行混用,Native API接口当前只能是C接口,可以通过这个接口隔离两边的C++运行环境。因此在使用共享库HAR包构建应用时,如果HAR包含的libc++_shared.so不同于应用使用的libc++_shared.so版本,那么只有其中一个版本会安装到应用里,可能会导致不兼容问题,可以使用相同的SDK版本更新HAR包解决此问题。

已知C++兼容性问题:应用启动或者dlopen时hilog报错symbol not found, s=__emutls_get_address,原因是API9及之前版本SDK中的libc++_shared.so无此符号,而API11之后版本SDK的libc++_shared.so是有此符号的。解决此问题需要更新应用或者共享库HAR包的SDK版本。

2. musl libc动态链接器

动态库加载命名空间隔离

动态库加载命名空间(namespace,下面统称为ns)是动态链接器设计的一个概念(区别于C++语言中的命名空间),其设计的主要目的是为了在进程中做native库资源访问的管控,以达到安全隔离的目的。例如系统native库允许加载系统目录(/system/lib64;/vendor/lib64等)下的native库,但是普通应用native库仅允许加载普通应用native库和ndk库,而不允许直接加载系统native库。

动态链接器无论是在加载编译依赖(DT_NEEDED)中指定的共享库,还是调用dlopen加载指定的共享库,都需要关联到具体的ns。

HarmonyOS中动态库加载namespace配置的情况

  • default ns:动态链接器启动时默认创建的ns,它可以搜索/system/lib{abi};/vendor/lib{abi}等系统目录路径下的so。

  • ndk ns:动态链接器启动时默认创建的ns,它可以搜索/system/lib{abi}/ndk目录下的so,主要是暴露了NDK接口的so。

  • app ns: 应用启动时创建的ns,它的搜索路径一般是应用的安装路径(可能为沙箱路径),即可加载应用的so。

当前这一套命名空间机制主要限制了应用native库和系统native库之间的调用,如图所示,具体规则为

  1. default ns和ndk ns可以互相访问全部so,不能访问app ns的so。
  2. app ns能访问ndk ns的全部so,不能访问default ns的so。

rpath机制

rpath(run-time path)是在运行时指定共享库搜索路径的机制。该机制允许在可执行文件或共享库中嵌入一个用于在运行时指定库的搜索路径的信息。

由于上文介绍的命名空间隔离机制,应用仅允许加载对应安装目录拼接native库路径下(例如arm64平台上为libs/arm64)的应用native库,当应用程序涉及加载较多的native库,期望创建多个native库加载路径方便管理,但是会导致无法加载新创建目录下的native库,这种情况可以通过rpath机制编译时指定搜索路径。

例如,应用安装目录lib/arm64下的libhello.so依赖新创建路径lib/arm64/module下的libworld.so,那么在应用的CMakeList.txt里设置上rpath编译选项后编译,使用readelf查看libhello.so的rpath配置如图所示,$ORIGIN为libhello.so所在路径,运行时即可正常加载module目录下的libworld.so。

SET(CMAKE_BUILD_WITH_INSTALL_RPATH TRUE)
SET(CMAKE_INSTALL_RPATH "\${ORIGIN}/module")

支持dlclose

支持使用dlclose真实卸载动态库的能力。

支持symbol-version机制

symbol-version是libc在动态链接-符号重定位阶段的符号检索机制,支持不同版本的符号重定位,也可以帮助解决重复符号的问题。可参考 LD Version Scripts (GNU Gnulib)

网络接口select支持fd fortify检测

宏定义FD_SET/FD_CLR新增fd有效值检查,当传入的fd不在区间[0, 1024)中会触发abort crash。

宏定义FD_ISSET新增fd有效值检查,当传入的fd不在区间[0, 1024)中会返回false。

全球化支持

自API12起,newlocale及setlocale接口支持将locale设置C、C.UTF-8、en_US、en_US.UTF-8、zh_CN及zh_CN.UTF-8。新增在zh_CN及zh_CN.UTF-8的locale设置下对strtod_l、wcstod_l和localeconv的支持。注意strtod_l及wcstod_l不支持对十六进制及十六进制小数的转换。

fdsan功能

fdsan功能可以帮助检测文件的重复关闭和关闭后使用问题。

fdsan使用指南

1. 功能介绍

fdsan针对的操作对象是文件描述符,主要用于检测不同使用者对相同文件描述符的错误操作,包括多次关闭(double-close)和关闭后使用(use-after-close)。这些文件描述符可以是操作系统中的文件、目录、网络套接字和其他I/O设备等,在程序中,打开文件或套接字会生成一个文件描述符,如果此文件描述符在使用后出现反复关闭、或者关闭后使用等场景,就会造成内存泄露、文件句柄泄露等安全隐患问题。该类问题非常隐蔽,且难以排查,为了更好地检测此类问题,因此引入了此种针对文件描述符错误操作的检测工具fdsan。

2. 实现原理

设计思路:当打开已有文件或创建一个新文件的时候,在得到返回fd后,设置一个关联的tag,来标记fd的属主信息;关闭文件前,检测fd关联的tag,判断是否符合预期(属主信息一致),符合就继续走正常文件关闭流程;如果不符合就是检测到异常,根据设置,调用对应的异常处理。

tag由两部分组成,最高位的8-bit构成type,后面的56-bit构成value。

type,标识fd通过何种封装形式进行管理,例如 FDSAN_OWNER_TYPE_FILE就表示fd通过普通文件进行管理,type类型在 fdsan_owner_type进行定义。

value,则用于标识实际的owner tag。

tag构成图示

3. 接口说明

fdsan_set_error_level

enum fdsan_error_level fdsan_set_error_level(enum fdsan_error_level new_level);

描述: 可以通过fdsan_set_error_level设定error_level,error_level用于控制检测到异常后的处理行为。默认error_level为FDSAN_ERROR_LEVEL_WARN_ALWAYS。

参数: fdsan_error_level

名称说明
FDSAN_ERROR_LEVEL_DISABLEDdisabled ,此level代表什么都不处理。
FDSAN_ERROR_LEVEL_WARN_ONCEwarn-once,第一次出现错误时在hilog中发出警告,然后将级别降低为disabled(FDSAN_ERROR_LEVEL_DISABLED)
FDSAN_ERROR_LEVEL_WARN_ALWAYSwarn-always,每次出现错误时都在hilog中发出警告
FDSAN_ERROR_LEVEL_FATALfatal ,出现错误时调用abort异常退出.

返回值: 返回旧的error_level。

fdsan_get_error_level

enum fdsan_error_level fdsan_get_error_level();

描述: 可以通过fdsan_get_error_level获取error level。

返回值: 当前的error_level。

fdsan_create_owner_tag

uint64_t fdsan_create_owner_tag(enum fdsan_owner_type type, uint64_t tag);

描述: 通过传入的type和tag字段,拼接成一个有效的文件描述符的关闭tag。

参数: fdsan_owner_type

名称说明
FDSAN_OWNER_TYPE_GENERIC_00默认未使用fd对应的type值
FDSAN_OWNER_TYPE_GENERIC_FF默认非法fd对应的type值
FDSAN_OWNER_TYPE_FILE默认普通文件对应的type值,使用fopen或fdopen打开的文件具有该类型
FDSAN_OWNER_TYPE_DIRECTORY默认文件夹对应的type值,使用opendir或fdopendir打开的文件具有该类型
FDSAN_OWNER_TYPE_UNIQUE_FD默认unique_fd对应的type值,保留暂未使用
FDSAN_OWNER_TYPE_ZIPARCHIVE默认zip压缩文件对应的type值,保留暂未使用

返回值: 返回创建的tag,可以用于fdsan_exchange_owner_tag函数的输入。

fdsan_exchange_owner_tag

void fdsan_exchange_owner_tag(int fd, uint64_t expected_tag, uint64_t new_tag);

描述: 修改文件描述符的关闭tag。

通过fd所以找到对应的FdEntry,判断close_tag值与expected_tag是否一致,一致说明符合预期,可以用new_tag值重新设定对应的FdEntry。

如果不符合,则说明检测到了异常,后续则进行对应的异常处理。

参数:

名称类型说明
fdintfd句柄,作为FdEntry的索引
expected_taguint64_t期望的ownership tag值
new_taguint64_t设置新的ownership tag值

fdsan_close_with_tag

int fdsan_close_with_tag(int fd, uint64_t tag);

描述: 根据tag描述符关闭文件描述符。

通过fd找到匹配的FdEntry。如果close_tag与tag相同,则符合预期,可以继续执行文件描述符关闭流程,否则意味着检测到异常。

参数:

名称类型说明
fdint待关闭的fd句柄
taguint64_t期望的ownership tag

返回值: 0或者-1,0表示close成功,-1表示close失败。

fdsan_get_owner_tag

uint64_t fdsan_get_owner_tag(int fd);

描述: 根据文件描述符获取tag信息。

通过fd找到匹配的FdEntry,并获取其对应的close_tag。

参数:

名称类型说明
taguint64_townership tag

返回值: 返回对应fd的tag。

fdsan_get_tag_type

const char* fdsan_get_tag_type(uint64_t tag);

描述: 根据tag计算出对应的type类型。

通过获取到的tag信息,通过计算获取对应tag中的type信息。

参数:

名称类型说明
taguint64_townership tag

返回值: 返回对应tag的type。

fdsan_get_tag_value

uint64_t fdsan_get_tag_value(uint64_t tag);

描述: 根据tag计算出对应的owner value。

通过获取到的tag信息,通过偏移计算获取对应tag中的value信息。

参数:

名称类型说明
taguint64_townership tag

返回值: 返回对应tag的value。

4. 使用示例

如何使用fdsan?这是一个简单的double-close问题:

void good_write()
{
    sleep(1);
    int fd = open(DEV_NULL_FILE, O_RDONLY);
    sleep(3);
    ssize_t ret = write(fd, "fdsan test\n", 11);
    if (ret == -1) {
        OH_LOG_ERROR(LOG_APP, "good write but failed?!");
    }
    close(fd);
}

void bad_close()
{
    int fd = open(DEV_NULL_FILE, O_RDONLY);
    close(fd);
    sleep(2);
    // This close expected to be detect by fdsan
    close(fd);
}

void functional_test()
{
    std::vector<std::thread> threads;
    for (auto function : { good_write, bad_close }) {
        threads.emplace_back(function);
    }
    for (auto& thread : threads) {
        thread.join();
    }
}

int main()
{
    functional_test();
    return 0;
}

上述代码中的goog_write函数会打开一个文件并写入一些字符串而bad_close函数中也会打开一个文件同时包含double-close问题,这两个线程同时运行那么程序的执行情况会是这样的:

由于每次open返回的fd是顺序分配的,在进入主函数后第一个可用的fd是43,bad_close函数中第一次open返回的fd是43,在关闭之后,43就变成了可用的fd,在good_write函数中open返回了第一个可用的fd,即43,但是由于bad_close函数中存在double-close问题,因此错误的关闭了另一个线程中打开的文件,导致写入失败

在fdsan引入之后,有两种方法可以检测这类问题:使用标准库接口或实现具有fdsan的函数接口

使用标准库接口

标准库接口中fopen,fdopen,opendir,fdopendir都已经集成了fdsan,使用前述接口而非直接使用open可以帮助检测问题。在前述案例中可以使用fopen替代open:

void good_write()
{
    sleep(1);
    // fopen is protected by fdsan, replace open with fopen
    // int fd = open(DEV_NULL_FILE, O_RDONLY);
    FILE *f = fopen(DEV_NULL_FILE, O_RDONLY);
    sleep(3);
    ssize_t ret = write(fileno(f), "fdsan test\n", 11);
    if (ret == -1) {
        OH_LOG_ERROR(LOG_APP, "good write but failed?!");
    }
    close(fileno(f));
}

使用fopen打开的每个文件描述符都需要有一个与之对应的 tag 。fdsan 在 close 时会检查关闭的 fd 是否与 tag 匹配,不匹配就会默认提示相关日志信息。下面是上述代码的日志信息:

# hilog | grep MUSL-FDSAN
04-30 15:03:41.760 10933  1624 E C03f00/MUSL-FDSAN: attempted to close file descriptor 43,                             expected to be unowned, actually owned by FILE* 0x00000000f7b90aa2

从这里的错误信息中可以看出FILE接口体的文件被其他人错误的关闭了,FILE接口体的地址可以协助进一步定位。

此外,可以在代码中使用fdsan_set_error_level设置错误等级error_level,设置为Fatal之后如果fdsan检测到错误会提示日志信息同时crash生成堆栈信息用于定位。下面是error_level设置为Fatal之后生成的crash堆栈信息:

Reason:Signal:SIGABRT(SI_TKILL)@0x0000076e from:1902:20010043
Fault thread info:
Tid:15312, Name:e.myapplication
#00 pc 000e65bc /system/lib/ld-musl-arm.so.1(raise+176)(3de40c79448a2bbced06997e583ef614)
#01 pc 0009c3bc /system/lib/ld-musl-arm.so.1(abort+16)(3de40c79448a2bbced06997e583ef614)
#02 pc 0009de4c /system/lib/ld-musl-arm.so.1(fdsan_error+116)(3de40c79448a2bbced06997e583ef614)
#03 pc 0009e2e8 /system/lib/ld-musl-arm.so.1(fdsan_close_with_tag+836)(3de40c79448a2bbced06997e583ef614)
#04 pc 0009e56c /system/lib/ld-musl-arm.so.1(close+20)(3de40c79448a2bbced06997e583ef614)
#05 pc 000055d8 /data/storage/el1/bundle/libs/arm/libentry.so(bad_close()+96)(f3339aac824c099f449153e92718e1b56f80b2ba)
#06 pc 00006cf4 /data/storage/el1/bundle/libs/arm/libentry.so(decltype(std::declval<void (*)()>()()) std::__n1::__invoke[abi:v15004]<void (*)()>(void (*&&)())+24)(f3339aac824c099f449153e92718e1b56f80b2ba)
#07 pc 00006c94 /data/storage/el1/bundle/libs/arm/libentry.so(f3339aac824c099f449153e92718e1b56f80b2ba)
#08 pc 000067b8 /data/storage/el1/bundle/libs/arm/libentry.so(void* std::__n1::__thread_proxy[abi:v15004]<std::__n1::tuple<std::__n1::unique_ptr<std::__n1::__thread_struct, std::__n1::default_delete<std::__n1::__thread_struct>>, void (*)()>>(void*)+100)(f3339aac824c099f449153e92718e1b56f80b2ba)
#09 pc 00105a6c /system/lib/ld-musl-arm.so.1(start+248)(3de40c79448a2bbced06997e583ef614)
#10 pc 000700b0 /system/lib/ld-musl-arm.so.1(3de40c79448a2bbced06997e583ef614)

此时,从crash信息中可以看到是bad_close中存在问题,同时crash中也包含了所有打开的文件,协助进行定位,提升效率。

OpenFiles:
0->/dev/null native object of unknown type 0
1->/dev/null native object of unknown type 0
2->/dev/null native object of unknown type 0
3->socket:[28102] native object of unknown type 0
4->socket:[28103] native object of unknown type 0
5->anon_inode:[eventpoll] native object of unknown type 0
6->/sys/kernel/debug/tracing/trace_marker native object of unknown type 0
7->anon_inode:[eventpoll] native object of unknown type 0
8->anon_inode:[eventpoll] native object of unknown type 0
9->/dev/console native object of unknown type 0
10->pipe:[95598] native object of unknown type 0
11->pipe:[95598] native object of unknown type 0
12->socket:[18542] native object of unknown type 0
13->pipe:[96594] native object of unknown type 0
14->socket:[18545] native object of unknown type 0
15->pipe:[96594] native object of unknown type 0
16->anon_inode:[eventfd] native object of unknown type 0
17->/dev/binder native object of unknown type 0
18->/data/storage/el1/bundle/entry.hap native object of unknown type 0
19->anon_inode:[eventpoll] native object of unknown type 0
20->anon_inode:[signalfd] native object of unknown type 0
21->socket:[29603] native object of unknown type 0
22->anon_inode:[eventfd] native object of unknown type 0
23->anon_inode:[eventpoll] native object of unknown type 0
24->anon_inode:[eventfd] native object of unknown type 0
25->anon_inode:[eventpoll] native object of unknown type 0
26->anon_inode:[eventfd] native object of unknown type 0
27->anon_inode:[eventpoll] native object of unknown type 0
28->anon_inode:[eventfd] native object of unknown type 0
29->anon_inode:[eventpoll] native object of unknown type 0
30->anon_inode:[eventfd] native object of unknown type 0
31->anon_inode:[eventpoll] native object of unknown type 0
32->anon_inode:[eventfd] native object of unknown type 0
33->anon_inode:[eventpoll] native object of unknown type 0
34->anon_inode:[eventfd] native object of unknown type 0
35->socket:[97409] native object of unknown type 0
36->socket:[94716] native object of unknown type 0
38->socket:[94720] native object of unknown type 0
40->/data/storage/el1/bundle/entry_test.hap native object of unknown type 0
41->socket:[95617] native object of unknown type 0
42->/sys/kernel/debug/tracing/trace_marker native object of unknown type 0
43->/dev/null FILE* 4155724704
44->socket:[94737] native object of unknown type 0
45->pipe:[95634] native object of unknown type 0
46->pipe:[95634] native object of unknown type 0
47->pipe:[95635] native object of unknown type 0
49->pipe:[95636] native object of unknown type 0
50->pipe:[95636] native object of unknown type 0

实现具有fdsan的函数接口

除了直接使用具有fdsan功能的标准库函数之外,还可以实现具有fdsan的函数接口。fdsan机制主要通过两个接口实现:fdsan_exchange_owner_tag和fdsan_close_with_tag,fdsan_exchange_owner_tag可以设置对应fd的tag,而fdsan_close_with_tag可以在关闭文件时检查对应的tag是否正确。

下面是一个具有fdsan的函数接口实现实例:

struct fdsan_fd {
    fdsan_fd() = default;

    explicit fdsan_fd(int fd)
    {
        reset(fd);
    }

    fdsan_fd(const fdsan_fd& copy) = delete;
    fdsan_fd(fdsan_fd&& move)
    {
        *this = std::move(move);
    }

    ~fdsan_fd()
    {
        reset();
    }

    fdsan_fd& operator=(const fdsan_fd& copy) = delete;
    fdsan_fd& operator=(fdsan_fd&& move)
    {
        if (this == &move) {
            return *this;
        }
        reset();
        if (move.fd_ != -1) {
            fd_ = move.fd_;
            move.fd_ = -1;
            // Acquire ownership from the moved-from object.
            exchange_tag(fd_, move.tag(), tag());
        }
        return *this;
    }

    int get()
    {
        return fd_;
    }

    void reset(int new_fd = -1)
    {
        if (fd_ != -1) {
            close(fd_, tag());
            fd_ = -1;
        }
        if (new_fd != -1) {
            fd_ = new_fd;
            // Acquire ownership of the presumably unowned fd.
            exchange_tag(fd_, 0, tag());
        }
    }

  private:
    int fd_ = -1;

    // Use the address of object as the file tag
    uint64_t tag()
    {
        return reinterpret_cast<uint64_t>(this);
    }

    static void exchange_tag(int fd, uint64_t old_tag, uint64_t new_tag)
    {
        if (&fdsan_exchange_owner_tag) {
            fdsan_exchange_owner_tag(fd, old_tag, new_tag);
        }
    }

    static int close(int fd, uint64_t tag)
    {
        if (&fdsan_close_with_tag) {
            return fdsan_close_with_tag(fd, tag);
        }
    }
};

这里的实现中使用fdsan_exchange_owner_tag在开始时将fd与结构体对象地址绑定,然后在关闭文件时使用fdsan_close_with_tag进行检测,预期tag是结构体对象地址。

在实现了具有fdsan的函数接口之后,可以使用该接口包装fd:

void good_write()
{
    sleep(1);
    // int fd = open(DEV_NULL_FILE, O_RDONLY);
    fdsan_fd fd(open(DEV_NULL_FILE, O_RDONLY));
    sleep(3);
    ssize_t ret = write(fd.get(), "fdsan test\n", 11);
    if (ret == -1) {
        OH_LOG_ERROR(LOG_APP, "good write but failed?!");
    }
    close(fd.get());
}

此时运行该程序可以检测到另一个线程的double-close问题,详细信息可以参考3.2节。同样也可以设置error_level为fatal,这样可以使fdsan在检测到crash之后主动crash以获取更多信息。

鸿蒙全栈开发全新学习指南

之前总有很多小伙伴向我反馈说,不知道学习哪些鸿蒙开发技术?不知道需要重点掌握哪些鸿蒙应用开发知识点?而且学习时频繁踩坑,最终浪费大量时间。所以这里为大家准备了一份实用的鸿蒙(HarmonyOS NEXT)学习路线与学习文档用来跟着学习是非常有必要的。

针对一些列因素,整理了一套纯血版鸿蒙(HarmonyOS Next)全栈开发技术的学习路线,包含了鸿蒙开发必掌握的核心知识要点,内容有(ArkTS、ArkUI开发组件、Stage模型、多端部署、分布式应用开发、WebGL、元服务、OpenHarmony多媒体技术、Napi组件、OpenHarmony内核、OpenHarmony驱动开发、系统定制移植等等)鸿蒙(HarmonyOS NEXT)技术知识点。

本路线共分为四个阶段

第一阶段:鸿蒙初中级开发必备技能

在这里插入图片描述

第二阶段:鸿蒙南北双向高工技能基础:gitee.com/MNxiaona/733GH

第三阶段:应用开发中高级就业技术

第四阶段:全网首发-工业级南向设备开发就业技术:gitee.com/MNxiaona/733GH

《鸿蒙 (Harmony OS)开发学习手册》(共计892页)

如何快速入门?

1.基本概念
2.构建第一个ArkTS应用
3.……

开发基础知识:gitee.com/MNxiaona/733GH

1.应用基础知识
2.配置文件
3.应用数据管理
4.应用安全管理
5.应用隐私保护
6.三方应用调用管控机制
7.资源分类与访问
8.学习ArkTS语言
9.……

基于ArkTS 开发

1.Ability开发
2.UI开发
3.公共事件与通知
4.窗口管理
5.媒体
6.安全
7.网络与链接
8.电话服务
9.数据管理
10.后台任务(Background Task)管理
11.设备管理
12.设备使用信息统计
13.DFX
14.国际化开发
15.折叠屏系列
16.……

鸿蒙开发面试真题(含参考答案):gitee.com/MNxiaona/733GH

鸿蒙入门教学视频:

美团APP实战开发教学:gitee.com/MNxiaona/733GH

写在最后

  • 如果你觉得这篇内容对你还蛮有帮助,我想邀请你帮我三个小忙:
  • 点赞,转发,有你们的 『点赞和评论』,才是我创造的动力。
  • 关注小编,同时可以期待后续文章ing🚀,不定期分享原创知识。
  • 想要获取更多完整鸿蒙最新学习资源,请移步前往小编:gitee.com/MNxiaona/733GH

  • 14
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值