Android R adb remount 调用流程

目的:调查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);}
#endifif (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 offstatic 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/12441485fchmod(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.rhkb.cn/news/42887.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【QT继承QLabel实现绘制矩形、椭圆、直线、多边形功能,并且支持修改大小,移动位置,复制,粘贴,删除功能】

文章目录 介绍绘制一个矩形&#xff08;椭圆&#xff09;roi绘制一个多边形roi对矩形roi的缩放&#xff1a;对多边形rio的缩放&#xff08;移动点的位置&#xff09; 介绍 绘制矩形&#xff0c;椭圆&#xff0c;直线实际用的都是是同一个思路&#xff1a;鼠标第一次点击就确定…

3.0 Disruptor的使用介绍(一)

Disruptor: 其官网定义为&#xff1a;“A High Performance Inter-Thread Messaging Library”&#xff0c;即&#xff1a;线程间的高性能消息框架&#xff0c;与Labview的生产者、消费者模型很相似。 其组成部分比较多&#xff0c;先介绍几个常用的概念&#xff1a; …

ubuntu 2204键盘按键映射修改

键盘的按键和实际输出不一致&#xff0c;可以通过以下方法重新修改按键keycode. 1.在终端执行如下命令&#xff1a; xev -event keyboard 上边64是alt_l&#xff0c;但是键盘上对应的super(windows)。 2.vim /usr/share/X11/xkb/keycodes/evdev //<LALT> 64; 注释&l…

【Linux】System V共享内存:零拷贝加速进程通信!

引言 本文深入探讨System V IPC中的共享内存技术&#xff0c;涵盖其原理、操作步骤、实现细节及与其他IPC机制的关系&#xff0c;助力读者全面掌握这一高效进程间通信方式。 &#x1f4dd; 文章总结&#xff1a; 共享内存原理 System V共享内存通过让多个进程共享同一物理内存区…

UE4学习笔记 FPS游戏制作31 显示计分板

一 制作计分板 创建一个RankPanel的UI蓝图 在蓝图里拖入如下物体 覆层&#xff08;layout&#xff09;&#xff1a;让子物体跟随自己缩放&#xff0c;子物体需要设置为拉伸模式&#xff0c;有点类似于的panel&#xff0c;本身只是一个容器 调整各个物体 覆层&#xff1a; 锚…

如何在Linux CentOS上安装和配置Redis

如何在Linux CentOS上安装和配置Redis 大家好&#xff0c;我是曾续缘。欢迎来到本教程&#xff01;今天我将向您介绍在Linux CentOS上安装和配置Redis的详细步骤。Redis是一个高性能的键值存储系统&#xff0c;常用于缓存、消息队列和数据持久化等应用场景。让我们一起开始吧&…

Realsense-D400 系列手动曝光控制

文章目录 1、曝光 & 增益2、曝光 & 帧率3、调参 & 加载4、高级控制选项5、官方文档参考小结 1、曝光 & 增益 曝光exposure&#xff1a;英特尔 RealSense D400设备模组中的可见光 RGB 传感器和红外左右目传感器具有单独的曝光控制&#xff0c;对于双目红外&…

最大异或对 The XOR Largest Pair

题目来自洛谷网站&#xff1a; 思路&#xff1a; 两个循环时间复杂度太高了&#xff0c;会超时。 我们可以先将读入的数字&#xff0c;插入到字典树中&#xff0c;从高位到低位。对每个数查询的时候&#xff0c;题目要求是最大的异或对&#xff0c;所以我们选择相反的路径&am…

探索 curl ipinfo.io:从命令行获取你的网络身份卡!!!

&#x1f310; 探索 curl ipinfo.io&#xff1a;从命令行获取你的网络身份卡 &#x1faaa; &#x1f680; 简介&#xff1a;为什么需要 curl ipinfo.io&#xff1f; 当你在调试网络服务、排查访问限制或开发基于地理位置的应用时&#xff0c;公网 IP 信息就像一张网络身份证。…

Chrome(Google) 浏览器安装Vue2、Vue3 Devtools插件方法

方式一&#xff1a;本地下载安装 步骤一&#xff1a;下载 网站:极简插件官网_Chrome插件下载_Chrome浏览器应用商店 步骤二&#xff1a;下载后解压,并拖入浏览器扩展页面&#xff0c;安装插件后&#xff0c;重启浏览器。 步骤三&#xff1a;查看是否安装成功 方式二&#x…

树莓派超全系列文档--(7)RaspberryOS播放音频和视频

播放音频和视频 播放音频和视频VLC 媒体播放器vlc GUIvlc CLI使用 cvlc 在没有图形用户界面的情况下播放媒体 在 Raspberry Pi OS Lite 上播放音频和视频指定音频输出设备指定视频输出设备同时指定音频和视频输出设备提高数据流播放性能 文章来源&#xff1a; http://raspberr…

MySQL 8.0.41安装教程(附安装包)mysql8.0.41图文详细安装教程

文章目录 前言一、MySQL 8.0.41下载安装包二、MySQL 8.0.41安装教程1.启动安装程序2.选择安装模式3.选定安装组件4.确认安装设置5.执行安装操作6.安装进行中7.设置数据库密码8.继续点击下一步9.执行配置操作10.完成配置11. 再次点击下一步12.结束安装向导 三、MySQL 8.0.41配置…

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

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

【LVS】负载均衡群集部署(DR模式)

部署前IP分配 DR服务器&#xff1a;192.168.166.101 vip&#xff1a;192.168.166.100 Web服务器1&#xff1a;192.168.166.104 vip&#xff1a;192.168.166.100 Web服务器2&#xff1a;192.168.166.107 vip&#xff1a;192.168.166.100 NFS服务器&#xff1a;192.168.166.108 …

服务器与客户端通讯测试

服务器与客户端通讯测试 1 服务器与客户端通讯建立1.1 Main函数1.2 开启服务器1.3 客户端连接服务器1.4 扩展类 2 测试过程2.1 测试12.2 测试22.3 测试32.4 测试4 3 测试总结 测试服务器与客户端通讯时&#xff0c;发现数据丢包问题非常严重&#xff0c;肯定是自己的问题不会是…

Next.js 中间件鉴权绕过漏洞 (CVE-2025-29927) 复现利用与原理分析

免责声明 本文所述漏洞复现方法仅供安全研究及授权测试使用&#xff1b; 任何个人/组织须在合法合规前提下实施&#xff0c;严禁用于非法目的&#xff1b; 作者不对任何滥用行为及后果负责&#xff0c;如发现新漏洞请及时联系厂商并遵循漏洞披露规则。 漏洞原理 Next.js 是一个…

基于STC89C51的太阳自动跟踪系统的设计与实现—单片机控制步进电机实现太阳跟踪控制(仿真+程序+原理图+PCB+文档)

摘 要 随着我国经济的飞速发展&#xff0c;促使各种能源使用入不敷出&#xff0c;尤其是最主要的能源&#xff0c;煤炭石油资源不断消耗与短缺&#xff0c;因此人类寻找其他替代能源的脚步正在加快。而太阳能则具有无污染﹑可再生﹑储量大等优点&#xff0c;且分布范围广&…

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

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

SQL语句---DDL

文章目录 1、SQL语句2、DDL2.1 数据库的操作显示当前的数据库创建数据库指定编码删除数据库切换当前数据库 2.2 数据表的操作显示表创建表显示表结构修改表添加新的字段删除原有字段 修改原有字段删除数据表 2.3 Mysql数据库中常用的数据类型 1、SQL语句 结构化查询语句&#…

界面控件Telerik和Kendo UI 2025 Q1亮点——AI集成与数据可视化

Telerik DevCraft包含一个完整的产品栈来构建您下一个Web、移动和桌面应用程序。它使用HTML和每个.NET平台的UI库&#xff0c;加快开发速度。Telerik DevCraft提供完整的工具箱&#xff0c;用于构建现代和面向未来的业务应用程序&#xff0c;目前提供UI for ASP.NET MVC、Kendo…