从汇编角度看c++20 协程

从汇编角度看c++20 协程

背景:

在学习c++20 协程的时候,总对协程里边的局部成员存储,以及协程栈恢复有很多疑问,本次从过年arm64角度来分析下,具体情况,本例子也是从腾讯开源库libco 找到一些灵感,期望对初学者理解协程有所帮助

首先看下代码:

c++ 代码

C++
#include <experimental/coroutine>
#include <exception>
#include <iostream>
#include <thread>
#include <memory>
struct Generator {
 
  class ExhaustedException: std::exception { };
 
  struct promise_type {
    int value;
    bool is_ready = false;
      int result;
    std::experimental::suspend_never initial_suspend() { return {}; };
 
    std::experimental::suspend_always final_suspend() noexcept { return {}; }
 
    std::experimental::suspend_always await_transform(int value) {
      this->value = value;
      is_ready = true;
        std::cout<<"value="<<value<<std::endl;
      return {};
    }
 
    void unhandled_exception() {
 
    }
 
    Generator get_return_object() {
      return Generator{ std::experimental::coroutine_handle<promise_type>::from_promise(*this) };
    }
 
    //void return_void() { }
      void return_value(int res) {
          result = res;
         
      };
  };
 
  std::experimental::coroutine_handle<promise_type> handle;
 
  bool has_next() {
    if (handle.done()) {
      return false;
    }
      std::cout<<"has_next start"<<std::endl;
    if (!handle.promise().is_ready) {
      handle.resume();
    }
      std::cout<<"has_next end"<<std::endl;
    if (handle.done()) {
      return false;
    } else {
      return true;
    }
  }
 
  int next() {
    if (has_next()) {
      handle.promise().is_ready = false;
      return handle.promise().value;
    }
    throw ExhaustedException();
  }
  int getResult()
  {
      return handle.promise().result;
  }
  explicit Generator(std::experimental::coroutine_handle<promise_type> handle) noexcept
      : handle(handle) {}
 
  Generator(Generator &&generator) noexcept
      : handle(std::exchange(generator.handle, {})) {}
 
  Generator(Generator &) = delete;
  Generator &operator=(Generator &) = delete;
 
  ~Generator() {
    if (handle) handle.destroy();
  }
};
 
Generator sequence() {
  int i = 2;
    int b = 0x55;
    int c = 0x66;
    int d = 0x77;
  while (i < 5) {
    co_await i++;
  }
    co_return 5;
}
 Generator returns_generator() {
  auto g = sequence();
  if (g.has_next()) {
    std::cout << g.next() << std::endl;
  }
  return g;
}
int main() {
  auto generator = returns_generator();
  for (int i = 0; i < 5; ++i) {
    if (generator.has_next()) {
      std::cout << generator.next() << " result = "<< generator.getResult()<<std::endl;
    } else {
      break;
    }
  }
  return 0;
}

代码相对来说比较简单,一个序列生成器,协程比较经典的一个应用场景,大概我们来看下什么意思 sequence() 返回一个协程,这个协程刚开始initial_suspend() 返回的是never,不挂起协程,协程就会执行到sequence()方法里边, 然后执行到了 co_await i++; 代码走到了await_transform,返回了suspend_always 挂起,这时候代码会走到主程序中进行值的打印,接下来我们就分析下汇编代码

首先我们先看下i , a, b,c 存储在什么地方

汇编

C++
    0x100001670 <+484>:  mov    w8, #0x2
    0x100001674 <+488>:  str    w8, [x9, #0x1c]
    0x100001678 <+492>:  mov    w8, #0x55
->  0x10000167c <+496>:  str    w8, [x9, #0x20]
    0x100001680 <+500>:  mov    w8, #0x66
    0x100001684 <+504>:  str    w8, [x9, #0x24]
    0x100001688 <+508>:  mov    w8, #0x77
    0x10000168c <+512>:  str    w8, [x9, #0x28]
 

读取寄存器的值

Lldb:register read

C++
 
        x9 = 0x0000600000c081b0
     
        sp = 0x000000016fdff200
    

根据上边汇编x9的存放的地址是0x0000600000c081b0,它对应的内容如下,

C++
F4 2E 00 00 01 00 00 00 F0 32 00 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
02 00 00 00 55 00 00 00 66 00 00 00 77 00 00

可以看的的出来栈地址是0x000000016fdff200,而存放变量的地址是0x0000600000c081b0,两个差别很大,可以说明存放变量的在堆空间,在使用从堆空间拷贝到寄存器上,用与还原协程的栈空间

紧接着我们看下协程的挂起与恢复

挂起:

C++
  
   0x1000016bc <+560>:  bl     0x100001ac4               ; Generator::promise_type::await_transform at main.cpp:136
    0x1000016c0 <+564>:  b      0x1000016c4               ; <+568> at main.cpp
    0x1000016c4 <+568>:  ldr    x0, [sp, #0x60]
    0x1000016c8 <+572>:  bl     0x100001b20               ; std::experimental::coroutines_v1::suspend_always::await_ready at coroutine:311
    0x1000016cc <+576>:  tbnz   w0, #0x0, 0x10000175c     ; <+720> at main.cpp
->  0x1000016d0 <+580>:  b      0x1000016d4               ; <+584> at main.cpp
0x1000016c0 <+564>:  b      0x1000016c4               ; <+568> at main.cpp
    0x1000016c4 <+568>:  ldr    x0, [sp, #0x60]
    0x1000016c8 <+572>:  bl     0x100001b20               ; std::experimental::coroutines_v1::suspend_always::await_ready at coroutine:311
    0x1000016cc <+576>:  tbnz   w0, #0x0, 0x10000175c     ; <+720> at main.cpp
    0x1000016d0 <+580>:  b      0x1000016d4               ; <+584> at main.cpp
    0x1000016d4 <+584>:  ldr    x9, [sp, #0x78]
    0x1000016d8 <+588>:  mov    w8, #0x1
    0x1000016dc <+592>:  and    w8, w8, #0x3
    0x1000016e0 <+596>:  strb   w8, [x9, #0x2c]
    0x1000016e4 <+600>:  b      0x1000016e8               ; <+604> at main.cpp
    0x1000016e8 <+604>:  ldr    x0, [sp, #0x78]
    0x1000016ec <+608>:  bl     0x100001a84               ; std::experimental::coroutines_v1::coroutine_handle<Generator::promise_type>::from_address at coroutine:220
    0x1000016f0 <+612>:  mov    x8, x0
    0x1000016f4 <+616>:  ldr    x0, [sp, #0x60]
    0x1000016f8 <+620>:  stur   x8, [x29, #-0x70]
    0x1000016fc <+624>:  ldur   x8, [x29, #-0x70]
    0x100001700 <+628>:  stur   x8, [x29, #-0x68]
    0x100001704 <+632>:  ldur   x1, [x29, #-0x68]
    0x100001708 <+636>:  bl     0x100001b38               ; std::experimental::coroutines_v1::suspend_always::await_suspend at coroutine:313
    0x10000170c <+640>:  b      0x100001710               ; <+644> at main.cpp:207:5
    0x100001710 <+644>:  b      0x100001714               ; <+648> at main.cpp:207:5
    0x100001714 <+648>:  b      0x100001718               ; <+652> at main.cpp
    0x100001718 <+652>:  mov    w8, #0xff
    0x10000171c <+656>:  cbz    w8, 0x10000175c           ; <+720> at main.cpp
    0x100001720 <+660>:  b      0x100001724               ; <+664> at main.cpp
    0x100001724 <+664>:  mov    w8, #0xff
    0x100001728 <+668>:  subs   w8, w8, #0x1
    0x10000172c <+672>:  b.ne   0x100001944               ; <+1208> at main.cpp:201:11
    0x100001730 <+676>:  b      0x100001734               ; <+680> at main.cpp
    0x100001734 <+680>:  mov    w8, #0x2
    0x100001738 <+684>:  str    w8, [sp, #0x3c]
    0x10000173c <+688>:  b      0x100001740               ; <+692> at main.cpp
    0x100001740 <+692>:  ldr    w8, [sp, #0x3c]
    0x100001744 <+696>:  str    w8, [sp, #0x38]
    0x100001748 <+700>:  b      0x100001784               ; <+760> at main.cpp
    0x10000174c <+704>:  mov    x8, x1
    0x100001750 <+708>:  stur   x0, [x29, #-0x30]
    0x100001754 <+712>:  stur   w8, [x29, #-0x34]
    0x100001758 <+716>:  b      0x1000017f0               ; <+868> at main.cpp:210:1
    0x10000175c <+720>:  ldr    x0, [sp, #0x60]
    0x100001760 <+724>:  mov    w8, #0x0
    0x100001764 <+728>:  str    w8, [sp, #0x30]
    0x100001768 <+732>:  bl     0x100001b4c               ; std::experimental::coroutines_v1::suspend_always::await_resume at coroutine:315
    0x10000176c <+736>:  ldr    w8, [sp, #0x30]
    0x100001770 <+740>:  str    w8, [sp, #0x34]
    0x100001774 <+744>:  b      0x100001778               ; <+748> at main.cpp
    0x100001778 <+748>:  ldr    w8, [sp, #0x34]
    0x10000177c <+752>:  str    w8, [sp, #0x38]
    0x100001780 <+756>:  b      0x100001784               ; <+760> at main.cpp
    0x100001784 <+760>:  ldr    w8, [sp, #0x38]
    0x100001788 <+764>:  mov    x9, x8
    0x10000178c <+768>:  str    w9, [sp, #0x2c]
    0x100001790 <+772>:  cbz    w8, 0x1000017a4           ; <+792> at main.cpp:206:3
    0x100001794 <+776>:  b      0x100001798               ; <+780> at main.cpp
    0x100001798 <+780>:  ldr    w8, [sp, #0x2c]
    0x10000179c <+784>:  str    w8, [sp, #0x28]
    0x1000017a0 <+788>:  b      0x1000017d0               ; <+836> at main.cpp
    0x1000017a4 <+792>:  b      0x100001694               ; <+520> at main.cpp
    0x1000017a8 <+796>:  ldr    x0, [sp, #0x70]
    0x1000017ac <+800>:  mov    w1, #0x5
    0x1000017b0 <+804>:  bl     0x100001b5c               ; Generator::promise_type::return_value at main.cpp:152
    0x1000017b4 <+808>:  b      0x1000017b8               ; <+812> at main.cpp
    0x1000017b8 <+812>:  mov    w8, #0x3
    0x1000017bc <+816>:  str    w8, [sp, #0x24]
    0x1000017c0 <+820>:  b      0x1000017c4               ; <+824> at main.cpp
    0x1000017c4 <+824>:  ldr    w8, [sp, #0x24]
    0x1000017c8 <+828>:  str    w8, [sp, #0x28]
    0x1000017cc <+832>:  b      0x1000017d0               ; <+836> at main.cpp
    0x1000017d0 <+836>:  ldr    w8, [sp, #0x28]
    0x1000017d4 <+840>:  subs   w9, w8, #0x3
    0x1000017d8 <+844>:  str    w8, [sp, #0x20]
    0x1000017dc <+848>:  b.eq   0x100001818               ; <+908> at main.cpp
    0x1000017e0 <+852>:  b      0x1000017e4               ; <+856> at main.cpp
    0x1000017e4 <+856>:  ldr    w8, [sp, #0x20]
    0x1000017e8 <+860>:  str    w8, [sp, #0x40]
    0x1000017ec <+864>:  b      0x100001910               ; <+1156> at main.cpp
    0x1000017f0 <+868>:  b      0x1000017f4               ; <+872> at main.cpp:210:1
    0x1000017f4 <+872>:  ldur   x0, [x29, #-0x30]
    0x1000017f8 <+876>:  bl     0x100003c54               ; symbol stub for: __cxa_begin_catch
    0x1000017fc <+880>:  ldr    x0, [sp, #0x70]
    0x100001800 <+884>:  bl     0x100001b7c               ; Generator::promise_type::unhandled_exception at main.cpp:143
    0x100001804 <+888>:  b      0x100001808               ; <+892> at main.cpp:201:11
    0x100001808 <+892>:  bl     0x100003c60               ; symbol stub for: __cxa_end_catch
    0x10000180c <+896>:  b      0x100001810               ; <+900> at main.cpp:201:11
    0x100001810 <+900>:  b      0x100001814               ; <+904> at main.cpp:201:11
    0x100001814 <+904>:  b      0x100001818               ; <+908> at main.cpp
    0x100001818 <+908>:  ldr    x0, [sp, #0x70]
    0x10000181c <+912>:  bl     0x100001b98               ; Generator::promise_type::final_suspend at main.cpp:134
    0x100001820 <+916>:  ldr    x0, [sp, #0x68]
    0x100001824 <+920>:  bl     0x100001b20               ; std::experimental::coroutines_v1::suspend_always::await_ready at coroutine:311
    0x100001828 <+924>:  tbnz   w0, #0x0, 0x1000018b0     ; <+1060> at main.cpp
    0x10000182c <+928>:  b      0x100001830               ; <+932> at main.cpp
    0x100001830 <+932>:  ldr    x8, [sp, #0x78]
    0x100001834 <+936>:  str    xzr, [x8]
    0x100001838 <+940>:  b      0x10000183c               ; <+944> at main.cpp
    0x10000183c <+944>:  ldr    x0, [sp, #0x78]
    0x100001840 <+948>:  bl     0x100001a84               ; std::experimental::coroutines_v1::coroutine_handle<Generator::promise_type>::from_address at coroutine:220
    0x100001844 <+952>:  mov    x8, x0
    0x100001848 <+956>:  ldr    x0, [sp, #0x68]
    0x10000184c <+960>:  stur   x8, [x29, #-0x88]
    0x100001850 <+964>:  ldur   x8, [x29, #-0x88]
    0x100001854 <+968>:  stur   x8, [x29, #-0x80]
    0x100001858 <+972>:  ldur   x1, [x29, #-0x80]
    0x10000185c <+976>:  bl     0x100001b38               ; std::experimental::coroutines_v1::suspend_always::await_suspend at coroutine:313
    0x100001860 <+980>:  b      0x100001864               ; <+984> at main.cpp:201:11
    0x100001864 <+984>:  b      0x100001868               ; <+988> at main.cpp:201:11
    0x100001868 <+988>:  b      0x10000186c               ; <+992> at main.cpp
    0x10000186c <+992>:  mov    w8, #0xff
    0x100001870 <+996>:  cbz    w8, 0x1000018b0           ; <+1060> at main.cpp
    0x100001874 <+1000>: b      0x100001878               ; <+1004> at main.cpp
    0x100001878 <+1004>: mov    w8, #0xff
    0x10000187c <+1008>: subs   w8, w8, #0x1
    0x100001880 <+1012>: b.ne   0x100001944               ; <+1208> at main.cpp:201:11
    0x100001884 <+1016>: b      0x100001888               ; <+1020> at main.cpp
    0x100001888 <+1020>: mov    w8, #0x2
    0x10000188c <+1024>: str    w8, [sp, #0x1c]
    0x100001890 <+1028>: b      0x100001894               ; <+1032> at main.cpp
    0x100001894 <+1032>: ldr    w8, [sp, #0x1c]
    0x100001898 <+1036>: str    w8, [sp, #0x18]
    0x10000189c <+1040>: b      0x1000018d8               ; <+1100> at main.cpp
    0x1000018a0 <+1044>: mov    x8, x1
    0x1000018a4 <+1048>: stur   x0, [x29, #-0x30]
    0x1000018a8 <+1052>: stur   w8, [x29, #-0x34]
    0x1000018ac <+1056>: b      0x100001968               ; <+1244> at main.cpp:201:11
    0x1000018b0 <+1060>: ldr    x0, [sp, #0x68]
    0x1000018b4 <+1064>: mov    w8, #0x0
    0x1000018b8 <+1068>: str    w8, [sp, #0x10]
    0x1000018bc <+1072>: bl     0x100001b4c               ; std::experimental::coroutines_v1::suspend_always::await_resume at coroutine:315
    0x1000018c0 <+1076>: ldr    w8, [sp, #0x10]
    0x1000018c4 <+1080>: str    w8, [sp, #0x14]
    0x1000018c8 <+1084>: b      0x1000018cc               ; <+1088> at main.cpp
    0x1000018cc <+1088>: ldr    w8, [sp, #0x14]
    0x1000018d0 <+1092>: str    w8, [sp, #0x18]
    0x1000018d4 <+1096>: b      0x1000018d8               ; <+1100> at main.cpp
    0x1000018d8 <+1100>: ldr    w8, [sp, #0x18]
    0x1000018dc <+1104>: mov    x9, x8
    0x1000018e0 <+1108>: str    w9, [sp, #0xc]
    0x1000018e4 <+1112>: cbz    w8, 0x1000018f8           ; <+1132> at main.cpp
    0x1000018e8 <+1116>: b      0x1000018ec               ; <+1120> at main.cpp
    0x1000018ec <+1120>: ldr    w8, [sp, #0xc]
    0x1000018f0 <+1124>: str    w8, [sp, #0x40]
    0x1000018f4 <+1128>: b      0x100001910               ; <+1156> at main.cpp
    0x1000018f8 <+1132>: mov    w8, #0x0
    0x1000018fc <+1136>: str    w8, [sp, #0x8]
    0x100001900 <+1140>: b      0x100001904               ; <+1144> at main.cpp
    0x100001904 <+1144>: ldr    w8, [sp, #0x8]
    0x100001908 <+1148>: str    w8, [sp, #0x40]
    0x10000190c <+1152>: b      0x100001910               ; <+1156> at main.cpp
    0x100001910 <+1156>: ldr    x8, [sp, #0x78]
    0x100001914 <+1160>: ldr    w9, [sp, #0x40]
    0x100001918 <+1164>: str    w9, [sp, #0x4]
    0x10000191c <+1168>: cbz    x8, 0x100001930           ; <+1188> at main.cpp
    0x100001920 <+1172>: b      0x100001924               ; <+1176> at main.cpp
    0x100001924 <+1176>: ldr    x0, [sp, #0x78]
    0x100001928 <+1180>: bl     0x100003c30               ; symbol stub for: operator delete(void*)
    0x10000192c <+1184>: b      0x100001930               ; <+1188> at main.cpp
    0x100001930 <+1188>: ldr    w8, [sp, #0x4]
    0x100001934 <+1192>: cbz    w8, 0x100001940           ; <+1204> at main.cpp:201:11
    0x100001938 <+1196>: b      0x10000193c               ; <+1200> at main.cpp
    0x10000193c <+1200>: b      0x100001944               ; <+1208> at main.cpp:201:11
    0x100001940 <+1204>: b      0x100001944               ; <+1208> at main.cpp:201:11
    0x100001944 <+1208>: b      0x100001948               ; <+1212> at main.cpp:201:11
    0x100001948 <+1212>: b      0x10000194c               ; <+1216> at main.cpp
    0x10000194c <+1216>: mov    w8, #0x1
    0x100001950 <+1220>: and    w8, w8, #0x1
    0x100001954 <+1224>: and    w8, w8, #0x1
    0x100001958 <+1228>: sturb  w8, [x29, #-0x1a]
    0x10000195c <+1232>: ldurb  w8, [x29, #-0x19]
    0x100001960 <+1236>: tbnz   w8, #0x0, 0x100001998     ; <+1292> at main.cpp:210:1
    0x100001964 <+1240>: b      0x1000019b4               ; <+1320> at main.cpp:210:1
    0x100001968 <+1244>: b      0x10000196c               ; <+1248> at main.cpp:201:11
    0x10000196c <+1248>: b      0x100001970               ; <+1252> at main.cpp:201:11
    0x100001970 <+1252>: b      0x100001974               ; <+1256> at main.cpp
    0x100001974 <+1256>: ldr    x8, [sp, #0x78]
    0x100001978 <+1260>: cbz    x8, 0x10000198c           ; <+1280> at main.cpp:210:1
    0x10000197c <+1264>: b      0x100001980               ; <+1268> at main.cpp
    0x100001980 <+1268>: ldr    x0, [sp, #0x78]
    0x100001984 <+1272>: bl     0x100003c30               ; symbol stub for: operator delete(void*)
    0x100001988 <+1276>: b      0x10000198c               ; <+1280> at main.cpp:210:1
    0x10000198c <+1280>: ldurb  w8, [x29, #-0x19]
    0x100001990 <+1284>: tbnz   w8, #0x0, 0x1000019c4     ; <+1336> at main.cpp
    0x100001994 <+1288>: b      0x1000019d0               ; <+1348> at main.cpp:210:1
    0x100001998 <+1292>: ldurb  w8, [x29, #-0x1a]
    0x10000199c <+1296>: tbnz   w8, #0x0, 0x1000019b0     ; <+1316> at main.cpp:210:1
    0x1000019a0 <+1300>: b      0x1000019a4               ; <+1304> at main.cpp
    0x1000019a4 <+1304>: ldr    x0, [sp, #0x90]
    0x1000019a8 <+1308>: bl     0x100001ba8               ; Generator::~Generator at main.cpp:196
    0x1000019ac <+1312>: b      0x1000019b0               ; <+1316> at main.cpp:210:1
    0x1000019b0 <+1316>: b      0x1000019b4               ; <+1320> at main.cpp:210:1
    0x1000019b4 <+1320>: ldp    x29, x30, [sp, #0x130]
    0x1000019b8 <+1324>: ldp    x28, x27, [sp, #0x120]
    0x1000019bc <+1328>: add    sp, sp, #0x140
    0x1000019c0 <+1332>: ret   
    0x1000019c4 <+1336>: ldr    x0, [sp, #0x90]
    0x1000019c8 <+1340>: bl     0x100001ba8               ; Generator::~Generator at main.cpp:196
    0x1000019cc <+1344>: b      0x1000019d0               ; <+1348> at main.cpp:210:1
    0x1000019d0 <+1348>: b      0x1000019d4               ; <+1352> at main.cpp:201:11
    0x1000019d4 <+1352>: ldur   x0, [x29, #-0x30]
    0x1000019d8 <+1356>: bl     0x100003b58               ; symbol stub for: _Unwind_Resume
 

这个摘录上边代码一段汇编,看下协程挂起的过程中到底发生了什么事情 ,bl     0x100001b20  跳转到std::experimental::coroutines_v1::suspend_always::await_ready,判断ready 状态, 接着,tbnz   w0, #0x0, 0x10000175c  来看下tbnz 意思是判断#0x0位,跟0进行比较,如果非0 则跳转到0x10000175c,否则不调转,因为我们await_ready 是false所以这里tbnz判断失败,不跳转,则直接到了

 0x1000016d0    这里就是final_suspend 接着下来 就是要挂起了

绿色显示了汇编跳转的过程,可以看到最终调用ret,返回了调用的地方

恢复:

上边已经看了挂起的过程,接下来看下恢复协程怎么来的,首先看下resume相关汇编

C++
test`std::experimental::coroutines_v1::coroutine_handle<void>::resume:
    0x1000022ac <+0>:  sub    sp, sp, #0x20
    0x1000022b0 <+4>:  stp    x29, x30, [sp, #0x10]
    0x1000022b4 <+8>:  add    x29, sp, #0x10
    0x1000022b8 <+12>: str    x0, [sp, #0x8]
    0x1000022bc <+16>: ldr    x8, [sp, #0x8]
    0x1000022c0 <+20>: ldr    x0, [x8]
    0x1000022c4 <+24>: ldr    x8, [x0]
    0x1000022c8 <+28>: blr    x8
->  0x1000022cc <+32>: ldp    x29, x30, [sp, #0x10]
    0x1000022d0 <+36>: add    sp, sp, #0x20
    0x1000022d4 <+40>: ret   
 

然后我们读取下相关寄存器的值

C++
x0 = 0x0000600000c08000
x8 = 0x0000000100002ef4  test`sequence() at main.cpp:201

从上边绿色两行汇编可以看出来,x0存放的协程资源的堆地址,而x8则是存放协程代码的函数地址,接着blr x8 会跳转到协程,ret保存着 0x1000022cc这个地址,等协程挂起的时候会回到这个0x1000022cc地址上去,

总结:

从上边分析可以看出,c++协程实现还是比较复杂的,采用无栈协程,这个腾讯开源的libco的协程实现不太一样,协程资源存在栈资源, 切换与恢复协程通过跳转的功能实现的

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值