概要

前提:
本篇android AVB2.0学习总结系统的第三篇,前面两篇分别介绍了AVB2.0的配置,和Uboot中的AVB校验流程。
本篇将介绍Android Init阶段如何校验,以及相关会涉及的技术知识点。
本篇文档是基于android R版本代码。

============
**原创不易,转载就注明出处:https://blog.csdn.net/jackone12347/article/details/120088394

一、Init中哪个阶段校验及校验哪些分区?

在引导boot kernel后进入init,主要是在init的first stage(第一阶段)进行AVB校验的。在android需要挂载的分区挂载前执行校验。

需要校验的分区是在fstab中配置的,详细配置请查看AVB编译配置

二、Init代码分析

FirstStageMount流程

我们整体上了解一下Android是怎么设计把AVB校验流程穿插进来的,重点是理解google的设计思想和理解它的巧妙之处。

Init第一阶段的执行代码,整体流程上有点长,希望能耐心地研究一下代码。

FirstStageMountVBootV2校验流程

先从init的main.cpp开始,init的第一阶段主要是校验和挂载system/vendor等大分区,init的第二阶段才会去处理userdata等分区。

因为system/vendor分区未挂载,需要借助于dts或者rootfs中的fstab配置才知道system/vendor分区的描述信息,比如当前需要校验哪几个分区,这几个分区是否使能verity flag等,
和是否有super动态分区有点关系,一般如果没有打开super分区,会借助于dts的配置;如果打开了super分区,会借助于fstab中的flag

好了,下面开始从init的第一个执行的地方开始分析:

 #### main.cppint main(int argc, char** argv) {#if __has_feature(address_sanitizer)__asan_set_error_report_callback(AsanReportCallback);#endifif (!strcmp(basename(argv[0]), "ueventd")) {return ueventd_main(argc, argv);  ###处理uevent流程相关的}if (argc > 1) {if (!strcmp(argv[1], "subcontext")) {android::base::InitLogging(argv, &android::base::KernelLogger);const BuiltinFunctionMap& function_map = GetBuiltinFunctionMap();return SubcontextMain(argc, argv, &function_map);}if (!strcmp(argv[1], "selinux_setup")) {return SetupSelinux(argv);  ###selinux相关的}if (!strcmp(argv[1], "second_stage")) {return SecondStageMain(argc, argv);  ## 第二阶段}}return FirstStageMain(argc, argv);  ## 第一阶段}

main.cpp中调用FirstStageMain()函数,执行第一阶段流程

 int FirstStageMain(int argc, char** argv) {…if (!DoFirstStageMount()) {LOG(FATAL) << "Failed to mount required partitions early ...";}…}bool DoFirstStageMount() {…std::unique_ptr<FirstStageMount> handle = FirstStageMount::Create();if (!handle) {LOG(ERROR) << "Failed to create FirstStageMount";return false;}return handle->DoFirstStageMount();}

我们先看下这个handle,调用了create方法,创建FirstStageMountVBootV1或者FirstStageMountVBootV2实例,取决于IsDtVbmetaCompatible(fstab)的返回值,如果支持vbmeta,则使用FirstStageMountVBootV2那套流程,我们分析使能了AVB的情况。

 std::unique_ptr<FirstStageMount> FirstStageMount::Create() {auto fstab = ReadFirstStageFstab();###判断device tree中是否有vbmeta/compatible结构,值是android,vbmetaif (IsDtVbmetaCompatible(fstab)) {return std::make_unique<FirstStageMountVBootV2>(std::move(fstab));} else {return std::make_unique<FirstStageMountVBootV1>(std::move(fstab));}}

IsDtVbmetaCompatible是检查kernel dtsi中是否有配置类似如下格式的数据:

 firmware: firmware {compatible = "android,firmware";android {…};};

FirstStageMountVBootV2是FirstStageMount的子类,有三个和dm-verity相关方法InitDevicesSetUpDmVerityInitAvbHandle,这三个方法基本上就把AVB校验的事情做完了,每一个都非常关键,建议大家重点分析。
我先大致说一下这三个方法的主要作用:
InitDevices:完成device mapper的映射,就是system/vendor分区如何映身到dm-x的
SetUpDmVerity:完成hash tree的使能,即dm-verity功能是怎么设置到kernel,将来设备运行时校验分区;
InitAvbHandle:完成AVB的libavb校验,确认vbmeta/system/vendor的签名是否合法。

如果没有这三个概念,估计也很难理解google的设计思路了。
好了,了解了这接下来可以继续分析代码了。

FirstStageMountVBootV2类的构建函数
先看一下FirstStageMountVBootV2类的构造函数
功能:主要是解析device tree中的vbmeta parts节点数据,存在device_tree_vbmeta_parts指针中,然后解析数据,最后全部插入到对象的vbmeta_partitions_这个vector中保存起来。

 FirstStageMountVBootV2::FirstStageMountVBootV2(Fstab fstab): FirstStageMount(std::move(fstab)), avb_handle_(nullptr) {std::string device_tree_vbmeta_parts;read_android_dt_file("vbmeta/parts", &device_tree_vbmeta_parts);for (auto&& partition : Split(device_tree_vbmeta_parts, ",")) {if (!partition.empty()) {vbmeta_partitions_.emplace_back(std::move(partition));}}for (const auto& entry : fstab_) {if (!entry.vbmeta_partition.empty()) {vbmeta_partitions_.emplace_back(entry.vbmeta_partition);}}…}

分析完FirstStageMountVBootV2类的构造函数后,我们接着看上面的handle->DoFirstStageMount(),也就是FirstStageMountVBootV2的DoFirstStageMount()方法,但这两个子类没有重载此方法,使用的父类FirstStageMount的DoFirstStageMount()方法,如下:

bool FirstStageMount::DoFirstStageMount() {if (!IsDmLinearEnabled() && fstab_.empty()) {// Nothing to mount.LOG(INFO) << "First stage mount skipped (missing/incompatible/empty fstab in device tree)";return true;}if (!InitDevices()) return false;  if (!MountPartitions()) return false;return true;
}

其中IsDmLinearEnabled()方法也比较简单,就是判断fstab中分区是否有配置logical关键字,如果有配置表示当前分区为动态分区的逻辑子分区,andriod R是要求使用动态分区的。

bool FirstStageMount::IsDmLinearEnabled() {for (const auto& entry : fstab_) {if (entry.fs_mgr_flags.logical) return true;}return false;
}

InitDevices()函数

接下来分析第一个重点函数,InitDevices函数,这个函数主要做的一件事情就是找到super物理分区的节点,以及完成这个分区的节点映射,在dev/block下面生成super分区的节点出来。

bool FirstStageMount::InitDevices() {std::set<std::string> devices;GetSuperDeviceName(&devices);if (!GetDmVerityDevices(&devices)) {return false;}if (!InitRequiredDevices(std::move(devices))) {return false;}if (IsDmLinearEnabled()) {auto super_symlink = "/dev/block/by-name/"s + super_partition_name_;if (!android::base::Realpath(super_symlink, &super_path_)) {PLOG(ERROR) << "realpath failed: " << super_symlink;return false;}}return true;
}

那我们按顺序一个个的分析,GetSuperDeviceName()函数看下面调用关系,super_partition_name_最终就是个常量“super”,或者从kernel的cmdline传递过来的 可能带有“_a”或者“_b”,这个由boot的上一级uboot来决定的。

void FirstStageMount::GetSuperDeviceName(std::set<std::string>* devices) {// Add any additional devices required for dm-linear mappings.if (!IsDmLinearEnabled()) {return;}devices->emplace(super_partition_name_);
}std::string fs_mgr_get_super_partition_name(int slot) {// Devices upgrading to dynamic partitions are allowed to specify a super// partition name. This includes cuttlefish, which is a non-A/B device.std::string super_partition;if (fs_mgr_get_boot_config_from_kernel_cmdline("super_partition", &super_partition)) {if (fs_mgr_get_slot_suffix().empty()) {return super_partition;}std::string suffix;if (slot == 0) {suffix = "_a";} else if (slot == 1) {suffix = "_b";} else if (slot == -1) {suffix = fs_mgr_get_slot_suffix();}return super_partition + suffix;}return LP_METADATA_DEFAULT_PARTITION_NAME;
}#define LP_METADATA_DEFAULT_PARTITION_NAME "super"

接着看GetDmVerityDevices函数,这是虚函数,由子类FirstStageMountVBootV2实现了,其实就是遍历fstab配置中的带avb和logical字样的分区,把system/vendor等分区找出来。

bool FirstStageMountVBootV2::GetDmVerityDevices(std::set<std::string>* devices) {need_dm_verity_ = false;std::set<std::string> logical_partitions;// fstab_rec->blk_device has A/B suffix.for (const auto& fstab_entry : fstab_) {if (fstab_entry.fs_mgr_flags.avb) {  ##判断fstab分区中是否有avb字段need_dm_verity_ = true;}if (fstab_entry.fs_mgr_flags.logical) {// Don't try to find logical partitions via uevent regeneration.logical_partitions.emplace(basename(fstab_entry.blk_device.c_str()));} else {devices->emplace(basename(fstab_entry.blk_device.c_str()));}}if (need_dm_verity_) {if (vbmeta_partitions_.empty()) {LOG(ERROR) << "Missing vbmeta partitions";return false;}std::string ab_suffix = fs_mgr_get_slot_suffix();for (const auto& partition : vbmeta_partitions_) {std::string partition_name = partition + ab_suffix;if (logical_partitions.count(partition_name)) {continue;}devices->emplace(partition_name);}}return true;
}

接着看比较重要的InitRequiredDevices函数,这个主要的作用:底层查看system/vendor逻辑子分区的物理节点,如果10秒内没有找到system/vendor等分区,直接报异常。

bool FirstStageMount::InitRequiredDevices(std::set<std::string> devices) {if (!block_dev_init_.InitDeviceMapper()) {return false;}if (devices.empty()) {return true;}return block_dev_init_.InitDevices(std::move(devices));
}

更多的细节请见DeviceHandler类的实现细节,代码在devices.cpp中

bool BlockDevInitializer::InitDevices(std::set<std::string> devices) {auto uevent_callback = [&, this](const Uevent& uevent) -> ListenerAction {return HandleUevent(uevent, &devices);};uevent_listener_.RegenerateUevents(uevent_callback);// UeventCallback() will remove found partitions from |devices|. So if it// isn't empty here, it means some partitions are not found.if (!devices.empty()) {LOG(INFO) << __PRETTY_FUNCTION__<< ": partition(s) not found in /sys, waiting for their uevent(s): "<< android::base::Join(devices, ", ");Timer t;uevent_listener_.Poll(uevent_callback, 10s);LOG(INFO) << "Wait for partitions returned after " << t;}if (!devices.empty()) {LOG(ERROR) << __PRETTY_FUNCTION__ << ": partition(s) not found after polling timeout: "<< android::base::Join(devices, ", ");return false;}return true;
}

这里截取一部分code,其余的细节如底层怎么编译到system/vendor分区节点的,请大家自已看一下这块的代码,不然本篇文章会拉的非常长~~

void DeviceHandler::HandleUevent(const Uevent& uevent) {if (uevent.action == "add" || uevent.action == "change" || uevent.action == "online") {FixupSysPermissions(uevent.path, uevent.subsystem);}...if (uevent.subsystem == "block") {block = true;devpath = "/dev/block/" + Basename(uevent.path);if (StartsWith(uevent.path, "/devices")) {links = GetBlockDeviceSymlinks(uevent);}

好了,到此FirstStageMount::InitDevices就执行完成了,而且super_symlink成功的获取到底层的super分区节点名。

bool FirstStageMount::InitDevices() {std::set<std::string> devices;GetSuperDeviceName(&devices);if (!GetDmVerityDevices(&devices)) {return false;}if (!InitRequiredDevices(std::move(devices))) {return false;}if (IsDmLinearEnabled()) {auto super_symlink = "/dev/block/by-name/"s + super_partition_name_;if (!android::base::Realpath(super_symlink, &super_path_)) {PLOG(ERROR) << "realpath failed: " << super_symlink;return false;}}return true;
}

SetUpDmVerity函数

initDevices执行完后,执行挂载system/vendor等分区

bool FirstStageMount::DoFirstStageMount() {...if (!InitDevices()) return false;if (!MountPartitions()) return false;  ##initDevices执行完后,执行挂载分区return true;
}

MountPartitions函数里面有几个重要的函数,功能如下:
CreateLogicalPartitions:创建super子分区(system/vendor等)的dm-x节点,并设置到kernel层;
TrySwitchSystemAsRoot:将system分区挂载到设备的“/”根目录,其中也有校验和挂载system分区;
MountPartition:挂载fstab中其他子logic分区,如vendor/system_ext/product等分区,流程和system分区的校验和挂载是类似的。

bool FirstStageMount::MountPartitions() {...LOG(INFO) << "MountPartitions CreateLogicalPartitions.";if (!CreateLogicalPartitions()) return false;LOG(INFO) << "MountPartitions TrySwitchSystemAsRoot.";if (!TrySwitchSystemAsRoot()) return false;if (!SkipMountingPartitions(&fstab_)) return false;for (auto current = fstab_.begin(); current != fstab_.end();) {...Fstab::iterator end;if (!MountPartition(current, false /* erase_same_mounts */, &end)) {

那我们按顺序一个个地分析
CreateLogicalPartitions函数
函数调用中间过程,大家可以直接点击,最后调用到如下地方:

@fs_mgr_dm_linear.cpp
bool CreateLogicalPartition(CreateLogicalPartitionParams params, std::string* path) {CreateLogicalPartitionParams::OwnedData owned_data;if (!params.InitDefaults(&owned_data)) return false;DmTable table;if (!CreateDmTableInternal(params, &table)) {return false;}DeviceMapper& dm = DeviceMapper::Instance();if (!dm.CreateDevice(params.device_name, table, path, params.timeout_ms)) {return false;}LINFO << "Created logical partition " << params.device_name << " on device " << *path;return true;
}

重点是dm.CreateDevice函数,关于DeviceMapper的内容,后面我会专门写一个章节,期待下吧~~
调用到了dm.cpp,这里面有三个函数,分别是CreateDevice、LoadTableAndActivate、GetDeviceUniquePath,本质上做了三个ioctl命令:
ioctl(fd_, DM_DEV_CREATE, &io) :创建hash-table
ioctl(fd_, DM_TABLE_LOAD, io):将hash-table,映身到kernel的drivers/md驱动中保存
ioctl(fd_, DM_DEV_SUSPEND, io):suppend建立映射关系
这块的内容也比较多,后面专门搞一篇文章来介绍,在DeviceMapper中介绍吧~

bool DeviceMapper::CreateDevice(const std::string& name, const DmTable& table, std::string* path,const std::chrono::milliseconds& timeout_ms) {std::string uuid = GenerateUuid();if (!CreateDevice(name, uuid)) {return false;}
...std::string unique_path;if (!LoadTableAndActivate(name, table) || !GetDeviceUniquePath(name, &unique_path) ||!GetDmDevicePathByName(name, path)) {DeleteDevice(name);return false;}...return true;
}

讲完了CreateLogicalPartition函数后,接下来看TrySwitchSystemAsRoot函数,其实就是挂载system分区,并挂载到根目录上。

bool FirstStageMount::TrySwitchSystemAsRoot() {UseDsuIfPresent();// Preloading all AVB keys from the ramdisk before switching root to /system.PreloadAvbKeys();auto system_partition = std::find_if(fstab_.begin(), fstab_.end(), [](const auto& entry) {return entry.mount_point == "/system";});if (system_partition == fstab_.end()) return true;if (MountPartition(system_partition, false /* erase_same_mounts */)) {...SwitchRoot("/system");} else {PLOG(ERROR) << "Failed to mount /system";return false;}return true;
}void SwitchRoot(const std::string& new_root) {auto mounts = GetMounts(new_root);for (const auto& mount_path : mounts) {auto new_mount_path = new_root + mount_path;mkdir(new_mount_path.c_str(), 0755);if (mount(mount_path.c_str(), new_mount_path.c_str(), nullptr, MS_MOVE, nullptr) != 0) {PLOG(FATAL) << "Unable to move mount at '" << mount_path << "'";}}if (chdir(new_root.c_str()) != 0) {PLOG(FATAL) << "Could not chdir to new_root, '" << new_root << "'";}if (mount(new_root.c_str(), "/", nullptr, MS_MOVE, nullptr) != 0) {PLOG(FATAL) << "Unable to move root mount to new_root, '" << new_root << "'";}
...
}

分析一下MountPartition函数,后面的system/vendor/product等分区都会走到这个函数。里面比较关键的有两个函数,SetUpDmVerity和fs_mgr_do_mount_one
老规矩,先介绍这两个函数的总体功能:
SetUpDmVerity:AVB2.0的avb校验功能,基本就在这个函数中了,重头戏,所以想绕过android的dm-verity校验就可以在这里搞点东东啦~
fs_mgr_do_mount_one:这个没什么特别,就是fsck校验一下分区然后mount挂载。
所以,我们的重点是分析SetUpDmVerity函数。

bool FirstStageMount::MountPartition(const Fstab::iterator& begin, bool erase_same_mounts,Fstab::iterator* end) {...if (!SetUpDmVerity(&(*begin))) {PLOG(ERROR) << "Failed to setup verity for '" << begin->mount_point << "'";return false;}bool mounted = (fs_mgr_do_mount_one(*begin) == 0);// Try other mounts with the same mount point.Fstab::iterator current = begin + 1;for (; current != fstab_.end() && current->mount_point == begin->mount_point; current++) {if (!mounted) {// blk_device is already updated to /dev/dm-<N> by SetUpDmVerity() above.// Copy it from the begin iterator.current->blk_device = begin->blk_device;mounted = (fs_mgr_do_mount_one(*current) == 0);}}
...return mounted;
}

SetUpDmVerity也是虚函数,在子类中实现的。咋一看下面代码挺头痛,第一看我也觉得写的啥这是~~
但仔细看还是能看明白是干嘛的了,我先挑出几个重点的介绍一下:
fstab_entry->avb_keys.empty():判断fstab中是否有配置avb_keys,这个是vbmeta的公钥
InitAvbHandle:AVB调用libavb库完成分区的校验,重头戏。
avb_handle_->SetUpAvbHashtree:就是把system/vendor分区的root hashtree值设置到kernel驱动中。

bool FirstStageMountVBootV2::SetUpDmVerity(FstabEntry* fstab_entry) {AvbHashtreeResult hashtree_result;// It's possible for a fstab_entry to have both avb_keys and avb flag.// In this case, try avb_keys first, then fallback to avb flag.if (!fstab_entry->avb_keys.empty()) {if (!InitAvbHandle()) return false;// Checks if hashtree should be disabled from the top-level /vbmeta.if (avb_handle_->status() == AvbHandleStatus::kHashtreeDisabled ||avb_handle_->status() == AvbHandleStatus::kVerificationDisabled) {LOG(ERROR) << "Top-level vbmeta is disabled, skip Hashtree setup for "<< fstab_entry->mount_point;return true;  // Returns true to mount the partition directly.} else {auto avb_standalone_handle = AvbHandle::LoadAndVerifyVbmeta(*fstab_entry, preload_avb_key_blobs_[fstab_entry->avb_keys]);if (!avb_standalone_handle) {LOG(ERROR) << "Failed to load offline vbmeta for " << fstab_entry->mount_point;// Fallbacks to built-in hashtree if fs_mgr_flags.avb is set.if (!fstab_entry->fs_mgr_flags.avb) return false;LOG(INFO) << "Fallback to built-in hashtree for " << fstab_entry->mount_point;hashtree_result =avb_handle_->SetUpAvbHashtree(fstab_entry, false /* wait_for_verity_dev */);} else {// Sets up hashtree via the standalone handle.if (IsStandaloneImageRollback(*avb_handle_, *avb_standalone_handle, *fstab_entry)) {return false;}hashtree_result = avb_standalone_handle->SetUpAvbHashtree(fstab_entry, false /* wait_for_verity_dev */);}}

InitAvbHandle:AVB调用exterlan/avb/libavb库完成分区的校验,这个流程也非常的长,细节也非常的多,也是我们学习AVB的重点,需要另起一篇文章来介绍,本篇文章写不下了~

SetUpAvbHashtree也放到Dm verity文章中介绍吧,朋友们我会记得的,这两篇文章我有时间就会补上。

为方便与大家及时交流,弄了一个微信公众号,欢迎大家留言沟通~

android AVB2.0(三)Init阶段安全启动流程相关推荐

  1. Android 4.0 ICS SystemUI浅析——SystemUI启动流程

    阅读Android 4.0源码也有一段时间了,这次是针对SystemUI的一个学习过程.本文只是对SystemUI分析的一个开始--启动流程的分析,网上有很多关于2.3的SystemUI的分析,可4. ...

  2. AVB源码学习(三):AVB2.0 Init阶段安全启动流程

    参考资料 感谢前辈的blog,安全相关的资料可太少了,很详细很卓越 https://blog.csdn.net/jackone12347/article/details/116241676 一.Ini ...

  3. Android 9.0 在init.rc中启动一个服务

    现在有一个blink .bin文件,需要拷贝到/system/bin/目录下面去,然后再init.rc文件中启动该服务 一.init.rc文件中启动服务 1.在init.rc文件中启动服务 代码路径: ...

  4. android AVB2.0学习总结

    看了一阵子android AVB2.0相关的内容,准备梳理一下相关知识点.平时一般都用word整理,现在想想感觉还是用CSDN来整理看着直观些,方便自己查看的同时,也把自己学到的知识与其他人分享,共勉 ...

  5. android AVB2.0(四)libavb库介绍

    本篇android AVB2.0学习总结系统的第四篇,接上篇android AVB2.0(三)Init阶段安全启动流程, 这里介绍一下libavb库的详细实现. 请支持原创,原文链接:https:// ...

  6. android AVB2.0(二)Uboot阶段AVB2.0校验流程

    android AVB2.0学习总结传送门 本篇属于android AVB2.0学习总结系列的第二篇文章,本篇主要介绍一下UBOOT或者UEFI阶段AVB2.0的介绍. 支持原创,转载请标明链接 ht ...

  7. android AVB2.0(一)工作原理及编译配置

    android AVB2.0介绍,本篇主要介绍AVB2.0的概述和工作原理.配置和编译. 有关AVB2.0的其他子系统的介绍,请查看android AVB2.0学习总结 一.AVB2.0概述 什么是A ...

  8. Android10.0系统启动之Launcher(桌面)启动流程-[Android取经之路]

    摘要:上一节我们讲完了Android10.0的ActivityManagerService的启动流程,在AMS的最后启动了Launcher进程,今天我们就来看看Launcher的真正启动流程. 阅读本 ...

  9. android avb2.0问题解答 汇总

    前言android 系统安全内容总结 1.android vbmeta结构深入解析 2.android libavb深入解读 学习android avb2.0,看完上面结构与代码分析后,肯定对avb有 ...

最新文章

  1. html5 呼吸灯效果,jQuery仿地铁线路指示灯效果
  2. 标签传播算法(Label Propagation)及Python实现
  3. 《大众创业做电商——淘宝与微店 开店 运营 推广 一册通》一一1.3 选择创业的行业...
  4. 打造一个属于自己的应用服务自动监控警报程序
  5. STM32F1笔记(五)外部中断EXTI
  6. mysql 根据时间 获取上个月_MySQL[0]
  7. 品质LOGO模板素材|想知道平面设计师如何设计徽标的秘密吗?
  8. c语言中声明外部函数需要添加的关键字,C语言中声明和定义的区别——分析extern关键词。...
  9. 【STM32 .Net MF开发板学习-16】Zigbee遥控智能小车
  10. 冲击波病毒简介及解决方法
  11. java疯狂讲义pdf_《疯狂Java讲义(第3版)》PDF 下载
  12. Kubernets k8s中yml格式与pod yml格式
  13. [转]页面回传与js调用服务器端事件
  14. 拉卡拉遭联想控股减持:套现3亿 总经理陈烈辞职
  15. 霹雳猿教程网站正式上线
  16. 设计模式学习笔记(一)
  17. 由百家讲坛的《大隋风云-之流星王朝》想到的
  18. ufs2.2 协议扫盲(三)
  19. 成为一名CV(计算机视觉)工程师,你需要具备哪些能力?
  20. Python(Python+Qt)学习随笔:使用xlwings新建Execl文件和sheet的方法

热门文章

  1. 十大最具幸福感城市出炉 广州、深圳均榜上无名
  2. HTML input 默认值设置
  3. windows用户管理
  4. Frida安装到使用一目了然
  5. [已解决] Adding visible gpu devecies:
  6. CListCtrl控件详解
  7. 电路设计漫谈之:高频喜欢低感抗,低频喜欢低(电)阻抗 - 再谈接地
  8. 计算几何之大圆包含小圆问题
  9. Ubuntu 20.04 64位 Google Protocol ProtoV3 bufbuild buf 工具安装使用指南
  10. 怎样运行sql2008服务器,怎么开启SQL数据库服务