当前位置: 首页 > news >正文

C/C++ 虚函数

虚函数的定义

  • 虚函数是指在基类内部声明的成员函数前面添加关键字 virtual 指明的函数
  • 虚函数存在的意义是为了实现多态,让派生类能够重写(override)其基类的成员函数
  • 派生类重写基类的虚函数时,可以添加 virtual 关键字,但不是必须这么做
  • 虚函数是动态绑定的,在运行时才确定,而非虚函数的调用在编译时确定
  • 虚函数必须是非静态成员函数,因为静态成员函数需要在编译时确定
  • 构造函数不能是虚函数,因为虚函数是动态绑定的,构造函数创建时需要确定对象关系。
  • 析构函数一般是虚函数
  • 虚函数一旦声明,就一直是虚函数,派生类也无法改变这一事实

虚函数工作原理

虚函数表 + 虚表指针

  • 编译器在含有虚函数的类中创建一个虚函数表,称为 vtable,这个vtable用来存放虚函数的地址。另外还隐式的设置了一个虚表指针,称为vptr,这个vptr指向了该类对象的虚函数表。
  • 派生类在继承基类的同时,也会继承基类的虚函数表。
  • 派生类重写(override)了基类的虚函数时,则会将重写后的虚函数的地址替换掉由基类继承而来的虚函数表中对应虚函数地址。
  • 若派生类没有重写,则由基类继承而来的虚函数的地址将直接保存在派生类的虚函数表中。

示例

class A
{
public:int a;int function() {return this->a;}virtual int vfunction() {return a;}
};void vCallFunction(A* obj) {obj->vfunction();
}void callFunction(A* obj) {obj->function();
}调用:A a;vCallFunction(&a);callFunction(&a);汇编解析,以说明 只有在运行时才能确定调用的是基类的虚函数还是派生类的虚函数:1. 静态绑定,不是虚函数,目标地址在编译阶段就确定了。
00007FF679471030 <test1. | 48:894C24 08             | mov qword ptr ss:[rsp+8],rcx                                                      | test1.cpp:35
00007FF679471035         | 48:83EC 28               | sub rsp,28                                                                        |
00007FF679471039         | 48:8B4C24 30             | mov rcx,qword ptr ss:[rsp+30]                                                     | test1.cpp:36
00007FF67947103E         | E8 BDFFFFFF              | call <test1.public: virtual int __cdecl A::vfunction(void)>                       |
00007FF679471043         | 48:83C4 28               | add rsp,28                                                                        | test1.cpp:37
00007FF679471047         | C3                       | ret                                                                               |2. 动态绑定,只能根据 rdx 的值来确定函数位置
00007FF679471010 <test1. | 48:894C24 08             | mov qword ptr ss:[rsp+8],rcx                                                      | test1.cpp:31
00007FF679471015         | 48:83EC 28               | sub rsp,28                                                                        |
00007FF679471019         | 48:8B4424 30             | mov rax,qword ptr ss:[rsp+30]                                                     | test1.cpp:32
00007FF67947101E         | 48:8B00                  | mov rax,qword ptr ds:[rax]                                                        |
00007FF679471021         | 48:8B4C24 30             | mov rcx,qword ptr ss:[rsp+30]                                                     |
00007FF679471026         | FF10                     | call qword ptr ds:[rax]                                                           |
00007FF679471028         | 48:83C4 28               | add rsp,28                                                                        | test1.cpp:33
00007FF67947102C         | C3                       | ret                                                                               |当类A有虚函数的时候它就会偷偷生成一个隐藏成员变量,它存放着虚函数表的位置,根据偏移就可以找到实际上的 vfunction 的地址,将其存在寄存器 rax 里面,随后 call[rax] 就正常调用了。

注意:
每个类都只有一个虚函数表,该类所有的对象共享这个虚函数表,而不是每个实例化对象都分别由一个虚函数表。

c++ 类的多态性是通过虚函数来实现的,如果基类通过引用或指针调用的是虚函数时,我们并不知道执行该函数的对象是什么类型的,只有在运行时才能确定调用的是基类的虚函数还是派生类的虚函数,这就是运行时多态。

虚函数表和虚函数表指针创建的时机

当我们发现某一个具体的类当中,存在 virtual 这样的字段,就会为这个类去生成虚函数表。它的内容在编译期就已经生成确定了。虚函数表存储的位置是在全局数据区的只读数据段 ,虚函数表是存放虚函数的地址的数组。

当我们为类去构建对象的时候,在构造函数中。将虚函数表的地址赋值给对象的 vptr (存放对象首地址)

继承下,虚函数表指针的复制过程:

继承下,先会调用基类的构造函数,先将基类的虚函数表地址赋值给vptr,接着调用子类的构造函数的时候又将子类的虚函数表地址赋值给vptr(这是覆盖的行为)。
在这里插入图片描述

什么函数不能是虚函数 为什么(重点)

  • 不能被继承的函数
  • 不能被重写的函数
  1. 普通函数 普通函数不属于成员函数 是不能被继承的 普通函数只能被重载 不能被重写 因此声明为虚函数没有意
    义 因为编译器会在编译时绑定函数 而多态体现在运行时绑定 通常通过基类指针指向子类对象实现多态
  2. 友元函数 友元函数不属于类的成员函数 不能被继承 对于没有继承特性的函数没有虚函数的说法
  3. 构造函数 构造函数是用来初始化对象的 假如子类可以继承基类构造函数 那么子类对象的构造将使用基类的构造
    函数 而基类构造函数并不知道子类有什么成员 显然是不符合语义的 从另外一个角度讲 多态是通过基类指针指
    向子类对象来实现多态的 在对象构造之前并没有对象产生 因此无法使用多态特性 这是矛盾的 因此构造函数不
    允许继承
  4. 内联成员函数 内联函数就是为了在代码中直接展开 减少函数调用花费的代价 也就是说内联函数是在编译时展开
    的 而虚函数是为了实现多态 是在运行时绑定的 因此内联函数和多态的特性相违背
  5. 静态成员函数 首先静态成员函数理论是可继承的 但是静态成员函数是编译时确定的 无法动态绑定 不支持多态因此不能被重写

汇编角度看类

类大小的计算

class Person {private:int age; uint64_t num;
};
大小:   
printf("%d", sizeof(Person)); // 16
这里只有类的成员,按照结构体算法,直接就是16

进阶算法,当添加一个成员函数时,计算大小:

class Person {void function(){printf("hello world");};
private:int age; uint64_t num;
};
printf("%d", sizeof(Person)); // 16
由此可见:类中的成员函数是不占用类对象内存空间的

为了验证以上说法,我们删除一个8位的成员变量,此时只剩下 int age 也就是4字节成员变量。

class Person {void function(){printf("hello world");};
private:int age;  // 4
};

练一练,计算如下大小:

class Person {
public:virtual int getAge() { //虚函数定义return age;}
private:int age;
};
大小:
printf("%d", sizeof(Person)); // sizeof(Person) = 16 (64-bit) 
这里为什么是16 字节呢,在这里由于出现了虚函数,那么编译器会初始化虚表指针
在 64 位的情况下指针占8个字节,对齐成员变量的话,就是16字节,是不是很简单。

对整体类逆向分析:

class Person {
public:
virtual int getAge() { //虚函数定义
return age;
}
virtual void setAge(int age) { //虚函数定义
this->age = age;
}
private:
int age;
};
int main(int argc, char* argv[]) {
Person person;
return 0;
}
反汇编: 
------------------------ main ------------------------
00007FF76A17117C  | 48:8D4C24 20             | lea rcx,qword ptr ss:[rsp+20]  ;获取对象首地址
00007FF76A171181  | E8 1A000000              | call 0x00007FF76A1711A0  ;调用构造函数:<test1.public: __cdecl Person::Person(void)>------------------------ 构造函数(Person::Person())------------------------
00007FF76A1711A0 < | 48:894C24 08             | mov qword ptr ss:[rsp+8],rcx                                                      
00007FF76A1711A5   | 48:8B4424 08             | mov rax,qword ptr ss:[rsp+8] ; this 指针 rax = 0000004AF30FF7B0                                                  
00007FF76A1711AA   | 48:8D0D CF200000         | lea rcx, ds:[0x00007FF76A173280] ; 给 虚函数表指针 <const Person::`vftable'> 地址  rcx = 00007FF76A173280
00007FF76A1711B1   | 48:8908                  | mov qword ptr ds:[rax],rcx ; 取虚表的首地址,保存至虚表指针中   
00007FF76A1711B4   | 48:8B4424 08             | mov rax,qword ptr ss:[rsp+8]; 返回对象首地址,rax = 0000004AF30FF7B0 
00007FF76A1711B9   | C3                       | ret             内存<test1.const Person::`vftable'>00007FF76A173280 <public: virtual int __cdecl Person::getAge(void)>    00007FF76A171120   .....  test1.public: virtual int __cdecl Person::getAge(void)
00007FF76A173288 <public: virtual void __cdecl Person::setAge(int)>    00007FF76A171130  0.....  test1.public: virtual void __cdecl Person::setAge(int)
地址所在区段:
地址=00007FF76A173000
大小=0000000000002000
页面信息=".rdata"

当前类由于存在虚函数,那么编译器为 Person 生成了默认构造函数。该默认构造函数首先取得虚表的首地址,然后赋值到虚表指针中。

查看 内存 可见,虚表指针中存放了两个函数地址,分别是虚函数 getAge 和虚函数 setAge 的地址。因此,得到虚表指针就相当于得到了类中所有虚函数的首地址。

因为虚表信息在编译后会被链接到对应的执行文件中,所以获得的虚表地址是一个相对固定的地址。虚表中虚函数的地址排列顺序因虚表函数在类中的声明顺序而定,先声明的虚函数的地址会被排列在虚表靠前的位置。第一个被声明的虚函数地址在虚表首地址处。

在虚表指针初始化的过程中,对象执行了构造函数后,就得到了虚表指针,当其他代码访问这个对象的虚函数时,会根据对象的首地址,取出对应的虚表元素,当函数被调用时,会间接访问虚表,得到对应的虚函数首地址并调用执行。这种调用方式是一个间接的调用过程。需要多次寻址才能完成。

    Person person;person.setAge(0x11);printf("Age = 0x%X", person.getAge());
汇编:
00007FF6A339125C                             | 48:8D4C24 20             | lea rcx,qword ptr ss:[rsp+20]                                                     | test1.cpp:89
00007FF6A3391261                             | E8 4A000000              | call <test1.public: __cdecl Person::Person(void)>                                 |
00007FF6A3391266                             | BA 11000000              | mov edx,11                                                                        | test1.cpp:90
00007FF6A339126B                             | 48:8D4C24 20             | lea rcx,qword ptr ss:[rsp+20]                                                     |
00007FF6A3391270                             | E8 9BFFFFFF              | call <test1.public: virtual void __cdecl Person::setAge(int)>                     |
00007FF6A3391275                             | 48:8D4C24 20             | lea rcx,qword ptr ss:[rsp+20]                                                     | test1.cpp:91
00007FF6A339127A                             | E8 81FFFFFF              | call <test1.public: virtual int __cdecl Person::getAge(void)>                     |
00007FF6A339127F                             | 8BD0                     | mov edx,eax                                                                       |
00007FF6A3391281                             | 48:8D0D F81F0000         | lea rcx,qword ptr ds:[7FF6A3393280]                                               | 00007FF6A3393280:"Age = 0x%X"
00007FF6A3391288                             | E8 13FEFFFF              | call <test1.printf>                                                               |

上述通过虚表间接寻址访问的情况,只有在使用对象的指针或引用,调用虚函数的时候才会出现。当直接使用对象调用自身虚函数时,没必要查表访问,因为已经明确调用的是自身成员函数,根本没有构成多态性。查询虚表只会画蛇添足,降低程序执行效率,所以将这种情况处理为直接调用。

析构函数操作:
添加析构代码:~Person() { printf("~Person() \n");}汇编:
00007FF6DCBB1230 < | 48:894C24 08             | mov qword ptr ss:[rsp+8],rcx                                                      | test1.cpp:85, [rsp+08]:"Age = 0x%X \n"
00007FF6DCBB1235   | 48:83EC 28               | sub rsp,28                                                                        |
00007FF6DCBB1239   | 48:8B4424 30             | mov rax,qword ptr ss:[rsp+30]                                                      | [rsp+30]:拿到 this 指针也是虚表的位置
00007FF6DCBB123E   | 48:8D0D 63200000         | lea rcx,qword ptr ds:[<const Person::`vftable'>]                                  |将当前类虚表首地址赋值到虚表指针中
00007FF6DCBB1245   | 48:8908                  | mov qword ptr ds:[rax],rcx                                                        |
00007FF6DCBB1248   | 48:8D0D 31200000         | lea rcx,qword ptr ds:[<"~Person() \n"...>]                                        | 00007FF6DCBB3280:"~Person() \n"
00007FF6DCBB124F   | E8 4CFEFFFF              | call <test1.printf>                                                               |
00007FF6DCBB1254   | 48:83C4 28               | add rsp,28                                                                        |
00007FF6DCBB1258   | C3                       | ret                                                                               |

在汇编中识别析构函数的条件是,写入虚表指针,对象的虚表指针可能是有效的,已经指向了正确的虚函数表,将对象的虚表指针重新赋值后,其指针可能指向了另一个虚表,虚表内容不一定和原来的意义。

继承

相关文章:

C/C++ 虚函数

虚函数的定义 虚函数是指在基类内部声明的成员函数前面添加关键字 virtual 指明的函数虚函数存在的意义是为了实现多态&#xff0c;让派生类能够重写(override)其基类的成员函数派生类重写基类的虚函数时&#xff0c;可以添加 virtual 关键字&#xff0c;但不是必须这么做虚函…...

【3GPP】【5G】注销流程(Deregistration procedures)

1. 欢迎大家订阅和关注,精讲3GPP通信协议(2G/3G/4G/5G/IMS)知识点,专栏会持续更新中.....敬请期待! 目录 3.1.2 Deregistration procedures 3.1.2.1 UE-initiated Deregistration 3.1.2.2 Network-initiated Deregistration 3.1.2 Deregistration procedures 注销流程…...

【小游戏篇】三子棋游戏

硬控我一上午&#xff0c;小编还是太菜了&#xff0c;大家可以自行升级电脑难度&#xff0c;也可以升级游戏到五子棋 1.game.h #pragma once #include<stdio.h> #include<stdlib.h> #include<time.h> #define ROW 3 #define COL 3//初始化棋盘 void InitBoa…...

7-Zip Mark-of-the-Web绕过漏洞复现(CVE-2025-0411)

免责申明: 本文所描述的漏洞及其复现步骤仅供网络安全研究与教育目的使用。任何人不得将本文提供的信息用于非法目的或未经授权的系统测试。作者不对任何由于使用本文信息而导致的直接或间接损害承担责任。如涉及侵权,请及时与我们联系,我们将尽快处理并删除相关内容。 0x0…...

2025年国产化推进.NET跨平台应用框架推荐

2025年国产化推进.NET跨平台应用框架推荐 1. .NET MAUI NET MAUI是一个开源、免费&#xff08;MIT License&#xff09;的跨平台框架&#xff08;支持Android、iOS、macOS 和 Windows多平台运行&#xff09;&#xff0c;是 Xamarin.Forms 的进化版&#xff0c;从移动场景扩展到…...

关于ARM和汇编语言

一图流 ARM 计算机组成 输入设备 输出设备 存储设备 运算器 控制器 处理器读取内存程序执行的过程 取指阶段&#xff1a;控制器器通过地址总线向存储器发送想要获取的指令的地址编号&#xff0c;存储器将指定的指令发送给处理器 译码阶段&#xff1a;控制器对指令进行分…...

2024人工智能AI+制造业应用落地研究报告汇总PDF洞察(附原数据表)

原文链接&#xff1a; https://tecdat.cn/?p39068 本报告合集洞察深入剖析当前技术应用的现状&#xff0c;关键技术 创新方向&#xff0c;以及行业应用的具体情况&#xff0c;通过制造业具体场景的典型 案例揭示人工智能如何助力制造业研发设计、生产制造、运营管理 和产品服…...

QTableView和QTableWidget的关系与区别

QTableView 和 QTableWidget 都是 Qt 框架中用于显示表格数据的控件&#xff0c;但它们在设计和使用上有一些重要的区别。 QTableView 模型-视图架构&#xff1a;QTableView 是 Qt 模型-视图架构的一部分&#xff0c;它与模型&#xff08;如 QStandardItemModel 或自定义的 QA…...

Java导出通过Word模板导出docx文件并通过QQ邮箱发送

一、创建Word模板 {{company}}{{Date}}服务器运行情况报告一、服务器&#xff1a;总告警次数&#xff1a;{{ServerTotal}} 服务器IP:{{IPA}}&#xff0c;总共告警次数:{{ServerATotal}} 服务器IP:{{IPB}}&#xff0c;总共告警次数:{{ServerBTotal}} 服务器IP:{{IPC}}&#x…...

ESP8266 MQTT服务器+阿里云

MQTT私有平台搭建&#xff08;EMQX 阿里云&#xff09; 阿里云服务器 EMQX 搭建私有MQTT平台 1、搜索EMQX开源版本 2、查看各版本EMQX支持的UBUNTU版本 3、查看服务器Ubuntu版本 4、使用APT安装模式 5、按照官网指示安装并启动 6、下载安装MQTTX测试工具 7、设置云服务…...

css动画水球图

由于echarts水球图动画会导致ios卡顿&#xff0c;所以纯css模拟 展示效果 组件 <template><div class"water-box"><div class"water"><div class"progress" :style"{ --newProgress: newProgress % }"><…...

【设计模式-行为型】状态模式

一、什么是状态模式 什么是状态模式呢&#xff0c;这里我举一个例子来说明&#xff0c;在自动挡汽车中&#xff0c;挡位的切换是根据驾驶条件&#xff08;如车速、油门踏板位置、刹车状态等&#xff09;自动完成的。这种自动切换挡位的过程可以很好地用状态模式来描述。状态模式…...

2024.1.22 安全周报

政策/标准/指南最新动态 01 工信部印发《关于加强互联网数据中心客户数据安全保护的通知》 原文: https://www.secrss.com/articles/74673 互联网数据中心作为新一代信息基础设施&#xff0c;承载着千行百业的海量客户数据&#xff0c;是关系国民经济命脉的重要战略资源。…...

idea修改模块名导致程序编译出错

本文简单描述分别用Idea菜单、pom.xml文件管理项目模块module 踩过的坑&#xff1a; 通过idea菜单创建模块&#xff0c;并用idea菜单修改模块名&#xff0c;结构程序编译报错&#xff0c;出错的代码莫名奇妙。双击maven弹窗clean时&#xff0c;还是报错。因为模块是新建的&am…...

root用户Linux银河麒麟服务器安装vnc服务

安装必要桌面环境组件 yum install mate-session-manager -y mate-session #确定是否安装成功安装vnc服务器 yum install tigervnc-server -y切换到root为root得vnc设置密码 su root vncpasswd给root用户设置vnc服务器文件 vi /etc/systemd/system/vncserver:1.service [Un…...

CentOS 7使用RPM安装MySQL

MySQL是一个开源的关系型数据库管理系统&#xff08;RDBMS&#xff09;&#xff0c;允许用户高效地存储、管理和检索数据。它被广泛用于各种应用&#xff0c;从小型的web应用到大型企业解决方案。 MySQL提供了丰富的功能&#xff0c;包括支持多个存储引擎、事务能力、数据完整…...

OpenCV imread函数读取图像__实例详解

OpenCV imread函数读取图像__实例详解 本文目录&#xff1a; 零、时光宝盒 一、imread函数定义 二、imread函数支持的文件格式 三、imread函数flags参数详解 &#xff08;3.1&#xff09;、Flags-1时&#xff0c;样返回加载的图像&#xff08;使用alpha通道&#xff0c;否…...

激光线扫相机无2D图像的标定方案

方案一&#xff1a;基于运动控制平台的标定 适用场景&#xff1a;若激光线扫相机安装在可控运动平台&#xff08;如机械臂、平移台、旋转台&#xff09;上&#xff0c;且平台的运动精度已知&#xff08;例如通过编码器或高精度步进电机控制&#xff09;。 步骤&#xff1a; 标…...

【安当产品应用案例100集】034-安当KSP支持密评中存储数据的机密性和完整性

安当KSP是一套获得国密证书的专业的密钥管理系统。KSP的系统功能扩展图示如下&#xff1a; 我们知道商用密码应用安全性评估中&#xff0c;需要确保存储的数据不被篡改、删除或者破坏&#xff0c;必须采用合适的安全方案来确保存储数据的机密性和完整性。KSP能否满足这个需求呢…...

08.七种排序算法实现(C语言)

目录 一.排序的基本概念 1.1 排序的概念 1.2 常见的排序算法 二.常见排序算法的实现 2.1 插入排序&#xff08;直接&#xff09; 1.基本思想 2.直接插入排序的特性 3.代码实现 2.2 希尔排序 1.基本思想 2.希尔插入排序的特性 3.代码实现 2.3 选择排序 1.基本思想 2…...

Alibaba Spring Cloud 一 核心组件、特性

Alibaba Spring Cloud 是 Alibaba 基于 Spring Cloud 的分布式微服务解决方案&#xff0c;提供了一套高性能、高可靠的微服务开发和运维工具。它扩展了 Spring Cloud 的功能&#xff0c;并优化了许多在生产环境中的实践场景&#xff0c;例如服务发现、配置管理、熔断限流等。 …...

kafka学习笔记7 性能测试 —— 筑梦之路

kafka 不同的参数配置对 kafka 性能都会造成影响&#xff0c;通常情况下集群性能受分区、磁盘和线程等影响因素&#xff0c;因此需要进行性能测试&#xff0c;找出集群性能瓶颈和最佳参数。 # 生产者和消费者的性能测试工具 kafka-producer-perf-test.sh kafka-consumer-perf-t…...

HQChart使用教程30-K线图如何对接第3方数据45- DRAWRADAR数据结构

HQChart使用教程30-K线图如何对接第3方数据45- DRAWRADAR数据结构 效果图DRAWRADARHQChart代码地址后台数据对接说明示例数据数据结构说明效果图 DRAWRADAR DRAWRADAR是hqchart插件独有的绘制雷达图函数,可以通过麦语法脚本来绘制一个简单的雷达图数据。 雷达图显示的位置固定…...

Java集合学习:HashMap的原理

一、HashMap里的Hash是什么&#xff1f; 首先&#xff0c;我们先要搞清楚HashMap里的的Hash是啥意思。 当我们在编程过程中&#xff0c;往往需要对线性表进行查找操作。 在顺序表中查找时&#xff0c;需要从表头开始&#xff0c;依次遍历比较a[i]与key的值是否相等&#xff…...

ETLCloud在iPaas中的是关键角色?

在当今的数字化时代&#xff0c;企业越来越依赖于其处理和分析数据的能力。为了实现这一目标&#xff0c;企业需要将各种异构的应用和数据源集成在一起&#xff0c;形成一个统一的数据视图。在这一过程中&#xff0c;ETL&#xff08;Extract, Transform, Load&#xff09;和iPa…...

Docker Hub 全面解析及应对策略

在现代 DevOps 和容器化应用开发中&#xff0c;Docker Hub 是一个不可或缺的工具。然而&#xff0c;一些地区或企业对 Docker Hub 的访问受到限制&#xff0c;甚至全面禁止。这种现象引发了开发者和运维人员的广泛关注。那么&#xff0c;为什么 Docker Hub 会被禁用&#xff1f…...

第五天 Labview数据记录(5.1 INI配置文件读写)

5.1 INI配置文件读写 INI配置文件是一种简单的文本文件&#xff0c;通常用于存储软件的配置信息。它具有以下作用&#xff1a; 存储软件配置参数方便软件的维护和更新提高软件的灵活性和可扩展性便于用户修改和共享配置 5.1.1 前面板 1&#xff09;新建项目SaveData_Exampl…...

【算法】经典博弈论问题——巴什博弈 python

目录 前言巴什博弈(Bash Game)小试牛刀PN分析实战检验总结 前言 博弈类问题大致分为&#xff1a; 公平组合游戏、非公平组合游戏&#xff08;绝大多数的棋类游戏&#xff09;和 反常游戏 巴什博弈(Bash Game) 一共有n颗石子&#xff0c;两个人轮流拿&#xff0c;每次可以拿1~m颗…...

ES6语法

一、Let、const、var变量定义 1.let 声明的变量有严格局部作用域 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"&g…...

窥探QCC518x-308x系列与手机之间的蓝牙HCI记录与分析 - 耳机篇

上一篇是介绍如何窥探手机端Bluetooth的HCI log, 本次介绍是如何窥探Bluetooth的HCI log-耳机篇. 这次跟QCC518x/QCC308x测试的手机是Samsung S23 Ultra. QCC518x/QCC308x透过HCI界面取得Log教学. 步骤1: 开启QMDE -> 选择ADK r1102 QCC3083 Headset workspace.步骤2: 点…...