Android R adb remount 调用流程

news2025/3/31 22:00:45

目的:调查adb remount 与adb shell进去后执行remount的差异

调试方法:添加log编译adbd,替换system\apex\com.android.adbd\bin\adbd

一、调查adb remount实现

关键代码:system\core\adb\daemon\services.cpp

unique_fd daemon_service_to_fd(std::string_view name, atransport* transport) {
    ADB_LOG(Service) << "transport " << transport->serial_name() << " opening service " << name;

#if defined(__ANDROID__) && !defined(__ANDROID_RECOVERY__)
    if (name.starts_with("abb:") || name.starts_with("abb_exec:")) {
        return execute_abb_command(name);
    }
#endif

#if defined(__ANDROID__)
    if (name.starts_with("framebuffer:")) {
        return create_service_thread("fb", framebuffer_service);
    } else if (android::base::ConsumePrefix(&name, "remount:")) {
        std::string cmd = "/system/bin/remount ";
        cmd += name;
        return StartSubprocess(cmd, nullptr, SubprocessType::kRaw, SubprocessProtocol::kNone);
    } else if (android::base::ConsumePrefix(&name, "reboot:")) {
        return reboot_device(std::string(name));
    } else if (name.starts_with("root:")) {
        return create_service_thread("root", restart_root_service);
    } else if (name.starts_with("unroot:")) {
        return create_service_thread("unroot", restart_unroot_service);
    } else if (android::base::ConsumePrefix(&name, "backup:")) {
        std::string cmd = "/system/bin/bu backup ";
        cmd += name;
        return StartSubprocess(cmd, nullptr, SubprocessType::kRaw, SubprocessProtocol::kNone);
    } else if (name.starts_with("restore:")) {
        return StartSubprocess("/system/bin/bu restore", nullptr, SubprocessType::kRaw,
                               SubprocessProtocol::kNone);
    } else if (name.starts_with("disable-verity:")) {
        return StartSubprocess("/system/bin/disable-verity", nullptr, SubprocessType::kRaw,
                               SubprocessProtocol::kNone);
    } else if (name.starts_with("enable-verity:")) {
        return StartSubprocess("/system/bin/enable-verity", nullptr, SubprocessType::kRaw,
                               SubprocessProtocol::kNone);
    } else if (android::base::ConsumePrefix(&name, "tcpip:")) {
        std::string str(name);

        int port;
        if (sscanf(str.c_str(), "%d", &port) != 1) {
            return unique_fd{};
        }
        return create_service_thread("tcp",
                                     std::bind(restart_tcp_service, std::placeholders::_1, port));
    } else if (name.starts_with("usb:")) {
        return create_service_thread("usb", restart_usb_service);
    }
#endif

    if (android::base::ConsumePrefix(&name, "dev:")) {
        return unique_fd{unix_open(name, O_RDWR | O_CLOEXEC)};
    } else if (android::base::ConsumePrefix(&name, "jdwp:")) {
        pid_t pid;
        if (!ParseUint(&pid, name)) {
            return unique_fd{};
        }
        return create_jdwp_connection_fd(pid);
    } else if (android::base::ConsumePrefix(&name, "shell")) {
        return ShellService(name, transport);
    } else if (android::base::ConsumePrefix(&name, "exec:")) {
        return StartSubprocess(std::string(name), nullptr, SubprocessType::kRaw,
                               SubprocessProtocol::kNone);
    } else if (name.starts_with("sync:")) {
        return create_service_thread("sync", file_sync_service);
    } else if (android::base::ConsumePrefix(&name, "reverse:")) {
        return reverse_service(name, transport);
    } else if (name == "reconnect") {
        return create_service_thread(
                "reconnect", std::bind(reconnect_service, std::placeholders::_1, transport));
    } else if (name == "spin") {
        return create_service_thread("spin", spin_service);
    }
    return unique_fd{};
}

根据之前调试adb方法可以得知adb remount会打印如下log

I adbd    : transport UsbFfs opening service shell,v2,TERM=xterm-256color,raw:remount

参考文档:Android adb自身调试log开关-CSDN博客

进而会执行上面daemon_service_to_fd函数中的如下else if

else if (android::base::ConsumePrefix(&name, "shell")) {
        return ShellService(name, transport);

在ShellService中会解析参数和cmd,再调用StartSubprocess

// Shell service string can look like:
//   shell[,arg1,arg2,...]:[command]
unique_fd ShellService(std::string_view args, const atransport* transport) {
    size_t delimiter_index = args.find(':');
    if (delimiter_index == std::string::npos) {
        LOG(ERROR) << "No ':' found in shell service arguments: " << args;
        return unique_fd{};
    }

    // TODO: android::base::Split(const std::string_view&, ...)
    std::string service_args(args.substr(0, delimiter_index));
    std::string command(args.substr(delimiter_index + 1));

    // Defaults:
    //   PTY for interactive, raw for non-interactive.
    //   No protocol.
    //   $TERM set to "dumb".
    SubprocessType type(command.empty() ? SubprocessType::kPty : SubprocessType::kRaw);
    SubprocessProtocol protocol = SubprocessProtocol::kNone;
    std::string terminal_type = "dumb";

    for (const std::string& arg : android::base::Split(service_args, ",")) {
        if (arg == kShellServiceArgRaw) {
            type = SubprocessType::kRaw;
        } else if (arg == kShellServiceArgPty) {
            type = SubprocessType::kPty;
        } else if (arg == kShellServiceArgShellProtocol) {
            protocol = SubprocessProtocol::kShell;
        } else if (arg.starts_with("TERM=")) {
            terminal_type = arg.substr(strlen("TERM="));
        } else if (!arg.empty()) {
            // This is not an error to allow for future expansion.
            LOG(WARNING) << "Ignoring unknown shell service argument: " << arg;
        }
    }

    return StartSubprocess(command, terminal_type.c_str(), type, protocol);
}

继续跟踪也没发现adb remount有额外的参数或指令执行,看来只有一个remount,与在串口执行并无差异。

那为何adb remount后可以直接覆盖原文件,而串口执行remount必须先删除原文件才能cp成功呢?

adb remount
#cp system/apex/com.android.adbd/bin/adbd22 system/apex/com.android.adbd/bin/adbd
# sync

串口remount
cp system/apex/com.android.adbd/bin/adbd22 system/apex/com.android.adbd/bin/adbd
cp: system/apex/com.android.adbd/bin/adbd: Text file busy

尝试使用adb shell remount,也可以直接覆盖,adb log和adb remount一样

尝试使用adb shell /system/bin/remount,无法覆盖,和串口remount现象一样

adb log如下

03-17 01:19:13.704  8511  8511 I adbd    : transport UsbFfs opening service shell,v2,TERM=xterm-256color,raw:/system/bin/remount
03-17 01:19:13.704  8511  8511 I adbd    : shell  opening service ,v2,TERM=xterm-256color,raw:/system/bin/remount
03-17 01:19:13.704  8511  8511 I adbd    : ShellService xterm-256color opening service /system/bin/remount
03-17 01:19:13.704  8511  8511 E adbd    : starting raw subprocess (protocol=shell, TERM=xterm-256color): '/system/bin/remount'

说明adb remount并不是调用的/system/bin/remount,而是一个系统集成的函数实现的,相当于调用了一个remount系统函数 ,这也就明白为何adb remount没有走daemon_service_to_fd中的单独remount else if(调用的/system/bin/remount)

走哪个remount是system\core\adb\client\commandline.cpp中如下代码实现的

else if (!strcmp(argv[0], "remount")) {
        FeatureSet features;
        std::string error;
        if (!adb_get_feature_set(&features, &error)) {
            fprintf(stderr, "error: %s\n", error.c_str());
            return 1;
        }

        if (CanUseFeature(features, kFeatureRemountShell)) {
            std::vector<const char*> args = {"shell"};
            args.insert(args.cend(), argv, argv + argc);
            return adb_shell_noinput(args.size(), args.data());
        } else if (argc > 1) {
            auto command = android::base::StringPrintf("%s:%s", argv[0], argv[1]);
            return adb_connect_command(command);
        } else {
            return adb_connect_command("remount:");
        }
    }

反转又来了。。。 

排查了system/core下remount实现只有一处system\core\fs_mgr\fs_mgr_remount.cpp

查看Android.bp,此文件是编译remount指令的源码,但实现方式的确也和init中的raw命令类似叫do_remount

system\core\init\builtins.cpp中没有remount的实现

// Builtin-function-map start
const BuiltinFunctionMap& GetBuiltinFunctionMap() {
    constexpr std::size_t kMax = std::numeric_limits<std::size_t>::max();
    // clang-format off
    static const BuiltinFunctionMap builtin_functions = {
        {"bootchart",               {1,     1,    {false,  do_bootchart}}},
        {"chmod",                   {2,     2,    {true,   do_chmod}}},
        {"chown",                   {2,     3,    {true,   do_chown}}},
        {"class_reset",             {1,     1,    {false,  do_class_reset}}},
        {"class_reset_post_data",   {1,     1,    {false,  do_class_reset_post_data}}},
        {"class_restart",           {1,     1,    {false,  do_class_restart}}},
        {"class_start",             {1,     1,    {false,  do_class_start}}},
        {"class_start_post_data",   {1,     1,    {false,  do_class_start_post_data}}},
        {"class_stop",              {1,     1,    {false,  do_class_stop}}},
        {"copy",                    {2,     2,    {true,   do_copy}}},
        {"domainname",              {1,     1,    {true,   do_domainname}}},
        {"enable",                  {1,     1,    {false,  do_enable}}},
        {"exec",                    {1,     kMax, {false,  do_exec}}},
        {"exec_background",         {1,     kMax, {false,  do_exec_background}}},
        {"exec_start",              {1,     1,    {false,  do_exec_start}}},
        {"export",                  {2,     2,    {false,  do_export}}},
        {"hostname",                {1,     1,    {true,   do_hostname}}},
        {"ifup",                    {1,     1,    {true,   do_ifup}}},
        {"init_user0",              {0,     0,    {false,  do_init_user0}}},
        {"insmod",                  {1,     kMax, {true,   do_insmod}}},
        {"installkey",              {1,     1,    {false,  do_installkey}}},
        {"interface_restart",       {1,     1,    {false,  do_interface_restart}}},
        {"interface_start",         {1,     1,    {false,  do_interface_start}}},
        {"interface_stop",          {1,     1,    {false,  do_interface_stop}}},
        {"load_persist_props",      {0,     0,    {false,  do_load_persist_props}}},
        {"load_system_props",       {0,     0,    {false,  do_load_system_props}}},
        {"loglevel",                {1,     1,    {false,  do_loglevel}}},
        {"mark_post_data",          {0,     0,    {false,  do_mark_post_data}}},
        {"mkdir",                   {1,     6,    {true,   do_mkdir}}},
        // TODO: Do mount operations in vendor_init.
        // mount_all is currently too complex to run in vendor_init as it queues action triggers,
        // imports rc scripts, etc.  It should be simplified and run in vendor_init context.
        // mount and umount are run in the same context as mount_all for symmetry.
        {"mount_all",               {0,     kMax, {false,  do_mount_all}}},
        {"mount",                   {3,     kMax, {false,  do_mount}}},
        {"perform_apex_config",     {0,     0,    {false,  do_perform_apex_config}}},
        {"umount",                  {1,     1,    {false,  do_umount}}},
        {"umount_all",              {0,     1,    {false,  do_umount_all}}},
        {"update_linker_config",    {0,     0,    {false,  do_update_linker_config}}},
        {"readahead",               {1,     2,    {true,   do_readahead}}},
        {"remount_userdata",        {0,     0,    {false,  do_remount_userdata}}},
        {"restart",                 {1,     1,    {false,  do_restart}}},
        {"restorecon",              {1,     kMax, {true,   do_restorecon}}},

在fs_mgr_remount.cpp添加log,编译remount二进制,发现执行串口remount和adb remount都会打印这个log,说明两条指令又走到一起了。那为何表现会不同?

又有新的发现,若先串口执行remount,再adb remount,CP覆盖也会报错。。。晕了

反复尝试,发现adb remount后也会覆盖报错,奇怪了,难道是我替换后remount后导致的?

先排查到这里,至少基本理清了adb remount的调用流程,也算是有收获。

真相来了。。。

前面adb remount后可以覆盖,串口remount不能覆盖是因为,adb remount后有push 过system/apex/com.android.adbd/bin/adbd这个文件,然后再手动cp就可以覆盖,而无法覆盖都是因为没有先adb push那个文件。所以导致能不能覆盖不是remount导致的,而是adb push 具备特异功能

二、新版本adb push 无需再手动改权限、selinux标签的原因

在老版本Android系统,若push文件到system/bin等路径下,需要手动改权限,若开启了selinux还需要手动改selinux标签,否则会导致系统启动失败或相应服务启动异常。而在新版本Android系统则无需担心,这些步骤由adb自动完成了。

文件:system\core\adb\daemon\file_sync_service.cpp

先看调试log:

03-17 02:27:30.294  7466  8560 E adbd    : sync id_name stat_v2 name:system/apex/com.android.adbd/bin/adbd
03-17 02:27:30.301  7466  8560 E adbd    : sync id_name send_v2 name:system/apex/com.android.adbd/bin/adbd

adb push会先查询目标文件的属性信息并记录,然后才发送文件。在发送文件过程还会记录原文件夹、文件的属性,并会修改 push后的文件的属性

大概的实现代码如下:

static bool handle_sync_command(int fd, std::vector<char>& buffer) {
    D("sync: waiting for request");

    SyncRequest request;
    if (!ReadFdExactly(fd, &request, sizeof(request))) {
        SendSyncFail(fd, "command read failure");
        return false;
    }
    size_t path_length = request.path_length;
    if (path_length > 1024) {
        SendSyncFail(fd, "path too long");
        return false;
    }
    char name[1025];
    if (!ReadFdExactly(fd, name, path_length)) {
        SendSyncFail(fd, "filename read failure");
        return false;
    }
    name[path_length] = 0;

    std::string id_name = sync_id_to_name(request.id);

    LOG(ERROR) << "sync id_name:" << id_name.c_str() << " name:" << name;
    switch (request.id) {
        case ID_LSTAT_V1:
            if (!do_lstat_v1(fd, name)) return false;
            break;
        case ID_LSTAT_V2:
        case ID_STAT_V2:
            if (!do_stat_v2(fd, request.id, name)) return false;
            break;
        case ID_LIST_V1:
            if (!do_list_v1(fd, name)) return false;
            break;
        case ID_LIST_V2:
            if (!do_list_v2(fd, name)) return false;
            break;
        case ID_SEND_V1:
            if (!do_send_v1(fd, name, buffer)) return false;
            break;
        case ID_SEND_V2:
            if (!do_send_v2(fd, name, buffer)) return false;
            break;
        case ID_RECV_V1:
            if (!do_recv_v1(fd, name, buffer)) return false;
            break;
        case ID_RECV_V2:
            if (!do_recv_v2(fd, name, buffer)) return false;
            break;
        case ID_QUIT:
            return false;
        default:
            SendSyncFail(fd, StringPrintf("unknown command %08x", request.id));
            return false;
    }

    return true;
}
static bool do_stat_v2(int s, uint32_t id, const char* path) {
    syncmsg msg = {};
    msg.stat_v2.id = id;

    decltype(&stat) stat_fn;
    if (id == ID_STAT_V2) {
        stat_fn = stat;
    } else {
        stat_fn = lstat;
    }

    struct stat st = {};
    int rc = stat_fn(path, &st);
    if (rc == -1) {
        msg.stat_v2.error = errno_to_wire(errno);
    } else {
        msg.stat_v2.dev = st.st_dev;
        msg.stat_v2.ino = st.st_ino;
        msg.stat_v2.mode = st.st_mode;
        msg.stat_v2.nlink = st.st_nlink;
        msg.stat_v2.uid = st.st_uid;
        msg.stat_v2.gid = st.st_gid;
        msg.stat_v2.size = st.st_size;
        msg.stat_v2.atime = st.st_atime;
        msg.stat_v2.mtime = st.st_mtime;
        msg.stat_v2.ctime = st.st_ctime;
    }

    return WriteFdExactly(s, &msg.stat_v2, sizeof(msg.stat_v2));
}
static bool handle_send_file(borrowed_fd s, const char* path, uint32_t* timestamp, uid_t uid,
                             gid_t gid, uint64_t capabilities, mode_t mode, bool compressed,
                             std::vector<char>& buffer, bool do_unlink) {
    int rc;
    syncmsg msg;

    __android_log_security_bswrite(SEC_TAG_ADB_SEND_FILE, path);

    unique_fd fd(adb_open_mode(path, O_WRONLY | O_CREAT | O_EXCL | O_CLOEXEC, mode));

    if (fd < 0 && errno == ENOENT) {
        if (!secure_mkdirs(Dirname(path))) {
            SendSyncFailErrno(s, "secure_mkdirs failed");
            goto fail;
        }
        fd.reset(adb_open_mode(path, O_WRONLY | O_CREAT | O_EXCL | O_CLOEXEC, mode));
    }
    if (fd < 0 && errno == EEXIST) {
        fd.reset(adb_open_mode(path, O_WRONLY | O_CLOEXEC, mode));
    }
    if (fd < 0) {
        SendSyncFailErrno(s, "couldn't create file");
        goto fail;
    } else {
        if (fchown(fd.get(), uid, gid) == -1) {
            SendSyncFailErrno(s, "fchown failed");
            goto fail;
        }

#if defined(__ANDROID__)
        // Not all filesystems support setting SELinux labels. http://b/23530370.
        selinux_android_restorecon(path, 0);
#endif

        // fchown clears the setuid bit - restore it if present.
        // Ignore the result of calling fchmod. It's not supported
        // by all filesystems, so we don't check for success. b/12441485
        fchmod(fd.get(), mode);
    }

    {
        rc = posix_fadvise(fd.get(), 0, 0,
                           POSIX_FADV_SEQUENTIAL | POSIX_FADV_NOREUSE | POSIX_FADV_WILLNEED);
        if (rc != 0) {
            D("[ Failed to fadvise: %s ]", strerror(rc));
        }

        bool result;
        if (compressed) {
            result = handle_send_file_compressed(s, std::move(fd), timestamp);
        } else {
            result = handle_send_file_uncompressed(s, std::move(fd), timestamp, buffer);
        }

        if (!result) {
            goto fail;
        }

        if (!update_capabilities(path, capabilities)) {
            SendSyncFailErrno(s, "update_capabilities failed");
            goto fail;
        }

        msg.status.id = ID_OKAY;
        msg.status.msglen = 0;
        return WriteFdExactly(s, &msg.status, sizeof(msg.status));
    }

fail:

....
}

调用fchmod改push后文件的属性

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2324104.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

网络中常用协议

一, TCP协议 TCP&#xff08;Transmission Control Protocol&#xff0c;传输控制协议&#xff09;是互联网核心协议之一&#xff0c;位于传输层&#xff0c;为应用层提供可靠的、面向连接的数据传输服务。 1. TCP的核心特点 特性说明面向连接通信前需通过三次握手建立连接&a…

自动驾驶04:点云预处理03

点云组帧 感知算法人员在完成点云的运动畸变补偿后&#xff0c;会发现一个问题&#xff1a;激光雷达发送的点云数据包中的点云数量其实非常少&#xff0c;完全无法用来进行后续感知和定位层面的处理工作。 此时&#xff0c;感知算法人员就需要对这些数据包进行点云组帧的处理…

Linux内核软中断分析

一、软中断类型 在Linux内核中&#xff0c;中断处理分为上半部&#xff08;硬中断&#xff09;和下半部。上半部负责快速响应硬件事件&#xff0c;而下半部用于处理耗时任务&#xff0c;避免阻塞系统。下半部有三种机制&#xff1a;软中断&#xff08;Softirq&#xff09;、小任…

Linux修改默认shell为zsh

一、修改模型shell为zsh 1、检查当前使用的shell echo $SHELL 2、检查当前系统支持的shell cat /etc/shells# 输出结果显示如下&#xff1a; """ /bin/sh /bin/bash /usr/bin/sh /usr/bin/bash /bin/csh /bin/tcsh /usr/bin/csh /usr/bin/tcsh /usr/bin/zsh…

[ComfyUI] 如何升级自定义节点(Custom Nodes)

ComfyUI 提供了灵活的 自定义节点(Custom Nodes) 功能,允许用户扩展其能力。随着插件的更新,保持 Custom Nodes 处于最新状态是确保兼容性和功能完整性的关键。 1. 手动升级(Git Pull 方式) 如果你的 自定义节点 是通过 Git 克隆的,可以使用 Git 命令来升级: 步骤: …

linux和windows是采用何种机制保存密码的?

传统Linux的不足&#xff1a; 1&#xff09;存在特权用户root 任何人只要得到root的权限&#xff0c;对于整个系统都可以为所欲为。这一点Windows也一样。 &#xff12;)对于文件的访问权划分不够细 在linux系统里&#xff0c;对于文件的操作&#xff0c;只有「所有者」…

matlab打开两个工程

1、问题描述 写代码时&#xff0c;需要实时参考别人的代码&#xff0c;需要同时打开2个模型&#xff0c;当模型在同一个工程内时&#xff0c;这是可以直接打开的&#xff0c;如图所示 2、解决方案 再打开一个MATLAB主窗口 这个时候就可以同时打开多个模型了 3、正确的打开方…

HarmonyOS主题管理工具封装:动态切换、持久化存储与常见问题解析

注&#xff1a;适用版本&#xff08;Harmony OS NEXT / 5.0 / API 12 &#xff09; 一、效果展示 二、技术栈 HarmonyOS ArkUI框架 使用AppStorage实现跨组件状态管理&#xff0c;PersistentStorage持久化存储用户偏好。 系统配置常量 ConfigurationConstant.Color…

60V单通道高精度线性恒流LED驱动器防60V反接SOD123封装

产品描述: PC561A 系列产品是用于产生单通道、高精度恒流源&#xff08; Constant Current Regulator&#xff0c; CCR&#xff09; 的LED 驱动芯片&#xff0c;为各类 LED 照明应用提供高性价比恒流方案。PC561A 采用晶体管自偏置技术&#xff0c;可在超宽工作电压范围内维持…

学习threejs,使用Sprite精灵、SpriteMaterial精灵材质

&#x1f468;‍⚕️ 主页&#xff1a; gis分享者 &#x1f468;‍⚕️ 感谢各位大佬 点赞&#x1f44d; 收藏⭐ 留言&#x1f4dd; 加关注✅! &#x1f468;‍⚕️ 收录于专栏&#xff1a;threejs gis工程师 文章目录 一、&#x1f340;前言1.1 ☘️THREE.Sprite1.1.1 ☘️代码…

Cent OS7+Docker+Dify

由于我之前安装了Dify v1.0.0&#xff0c;出现了一些问题&#xff1a;无法删除&#xff0c;包括&#xff1a;知识库中的文件、应用、智能体、工作流&#xff0c;都无法删除。现在把服务器初始化&#xff0c;一步步重新安装&#xff0c;从0到有。 目录 1、服务器重装系统和配置…

本地AI大模型部署革命:Ollama部署和API调试教程

Ollama&#xff1a;你的私人AI助手 还在为云端AI服务的高昂费用和隐私问题而烦恼吗&#xff1f;Ollama横空出世&#xff0c;它是一款专为本地环境打造的大模型部署神器&#xff0c;让你轻松在自己的设备上运行各种强大的AI模型。无论你是开发者还是普通用户&#xff0c;Ollama…

centos7 linux VMware虚拟机新添加的网卡,能看到网卡名称,但是看不到网卡的配置文件

问题现象&#xff1a;VMware虚拟机新添加的网卡&#xff0c;能看到网卡&#xff0c;但是看不到网卡的配置文件 解决方案&#xff1a; nmcli connection show nmcli connection add con-name ens36 ifname ens36 type ethernet #创建一个网卡连接配置文件&#xff0c;这里con…

K8S学习之基础五十一:k8s部署jenkins

k8s部署jenkins 创建nfs共享目录&#xff0c; mkdir -p /data/v2 echo /data/v2 *(rw,no_root_squash) > /etc/exports exportfs -arv创建pv、pvc vi pv.yaml apiVersion: v1 kind: PersistentVolume metadata:name: jenkins-k8s-pv spec:capacity:storage: 1GiaccessMod…

在 Mermaid 流程图里“驯服”quot;的魔法指南!!!

&#x1f409; 在 Mermaid 流程图里“驯服”"的魔法指南 在使用 Mermaid 画流程图时&#xff0c;是不是经常遇到想秀一波 &quot; 却被它“反杀”的情况&#xff1f;&#x1f3af; 今天就来教大家如何在这头代码野兽的嘴里&#xff0c;抢回我们的双引号实体编码&#…

GitHub美化个人主页3D图表显示配置操作

这个功能主要是用的这个开源仓库&#xff1a;https://github.com/yoshi389111/github-profile-3d-contrib 想看效果的话&#xff0c;我的个人主页&#xff1a;https://github.com/Sjj1024 开始操作 1.创建自己的github主页属性项目——跟你github用户名一致即可&#xff0c;…

HarmonyOS NEXT 鸿蒙中关系型数据库@ohos.data.relationalStore API 9+

核心API ohos.data.relationalStore API 9 数据库 数据库是存储和管理数据的系统 数据库&#xff08;Database&#xff09;是一个以特定方式组织、存储和管理数据的集合&#xff0c;通常用于支持各种应用程序和系统的运行。它不仅是存放数据的仓库&#xff0c;还通过一定的…

【JavaScript】JavaScript Promises实践指南

【JavaScript】JavaScript Promises实践指南 你了解JavaScript中的Promises吗&#xff1f;这是一个很多人一开始就放弃的主题&#xff0c;但我会尽量让它变得尽可能简单。 1. “Promise”到底是什么&#xff1f; “Promise”是异步编程中的一个基本概念&#xff0c;特别是在J…

git push的时候出现无法访问的解决

fatal: 无法访问 https://github.com/...&#xff1a;gnutls_handshake() failed: Error in the pull function. push的时候没有输入自己的github账号密码&#xff0c;为了解决每次push都要登录github这个问题&#xff0c;采用ssh密钥的方式认证&#xff0c;可以免去每次都输入…

为什么大模型在 OCR 任务上表现不佳?

编者按&#xff1a; 你是否曾经用最先进的大语言模型处理企业文档&#xff0c;却发现它把财务报表中的“$1,234.56”读成了“123456”&#xff1f;或者在处理医疗记录时&#xff0c;将“0.5mg”误读为“5mg”&#xff1f;对于依赖数据准确性的运营和采购团队来说&#xff0c;这…