在使用pdb对某Python程序进行debug时,出现通过l或ll命令,无法获得代码上下文的情况,如下图:
所以我决定深究一下pdb代码是怎么写的,为啥有时候获取不到上下文代码。
pdb是Python内置的调试器,其源码由Python实现,基于cmd和bdb这两个内置库实现,多数情况下,pdb还是很好用的,虽说如此,但PyCharm、Vscode这些都没有使用标准的pdb,而是自己开发了Python调试器来配合IDE。
为了直观理解pdb运行流程,这里构建一下最小实例,将pdb运行起来:
- import pdb
-
-
- def fib(n):
- a, b = 1, 1
- # 下断点
- pdb.set_trace()
- for i in range(n - 1):
- a, b = b, a + b
-
- return a
-
-
- fib(10)
我在pycharm中运行上面代码,然后debug起来。
在调用pdb.set_trace()方法时,第一步便是实例化pdb对象:
- def set_trace(*, header=None):
- # 实例化
- pdb = Pdb()
- if header is not None:
- pdb.message(header)
- pdb.set_trace(sys._getframe().f_back)
实例化会调用__init__方法:
- class Pdb(bdb.Bdb, cmd.Cmd):
-
- _previous_sigint_handler = None
-
- def __init__(self, completekey='tab', stdin=None, stdout=None, skip=None,
- nosigint=False, readrc=True):
- bdb.Bdb.__init__(self, skip=skip)
- cmd.Cmd.__init__(self, completekey, stdin, stdout)
- sys.audit("pdb.Pdb")
- # ... 省略
从Pdb类可知,Pdb继承了bdb和cmd。
bdb内置模块是Python提供调试能力的核心框架,它基于sys.setrace方法提供的动态插桩能力,实现对代码的单步调试。而cmd模块主要用于实现交互式命令的,是常用模块,并不是为pdb专门设计的。
先从简单的cmd开始讨论。
cmd是Python内置的模块,主要用于实现交互式shell,我们可以基于cmd轻松实现一个自己的交互式shell,这里简单演示一下cmd的使用(因为不是本文重点,便不去深究了):
- from cmd import Cmd
-
-
- class MyCmd(Cmd):
- def __init__(self):
- Cmd.__init__(self)
-
- def do_name(self, name):
- print(f'Hello, {name}')
-
- def do_exit(self, arg):
- print('Bye!')
- return True
-
-
- if __name__ == '__main__':
- mycmd = MyCmd()
- mycmd.cmdloop()
上述代码中,定义了MyCmd类,继承于Cmd类,然后实现了do_name方法和do_exit方法,这两个方法分别会匹配上name命令和exit命令,然后通过cmdloop方法开始运mycmd,效果如下:
回顾一下set_trace方法:
- def set_trace(*, header=None):
- pdb = Pdb()
- if header is not None:
- pdb.message(header)
- pdb.set_trace(sys._getframe().f_back)
实例化完后,会通过sys._getframe().f_back获得frame对象,然后传递给pdb.set_trace方法。
其中sys._getframe()方法会获得当前的frame(栈帧)。
当我们运行Python代码时,解释器会创建相应的PyFrameObject对象(即上面我们说的frame)。从Python源码中,我们可以翻出PyFrameObject的定义,如下:
- typedef struct _frame {
- PyObject_VAR_HEAD
- struct _frame *f_back; /* previous frame, or NULL */
- PyCodeObject *f_code; /* code segment */
- PyObject *f_builtins; /* builtin symbol table (PyDictObject) */
- PyObject *f_globals; /* global symbol table (PyDictObject) */
- PyObject *f_locals; /* local symbol table (any mapping) */
- PyObject **f_valuestack; /* points after the last local */
- /* Next free slot in f_valuestack. Frame creation sets to f_valuestack.
- Frame evaluation usually NULLs it, but a frame that yields sets it
- to the current stack top. */
- PyObject **f_stacktop;
- ...
- int f_lasti; /* Last instruction if called */
- /* Call PyFrame_GetLineNumber() instead of reading this field
- directly. As of 2.3 f_lineno is only valid when tracing is
- active (i.e. when f_trace is set). At other times we use
- PyCode_Addr2Line to calculate the line from the current
- bytecode index. */
- int f_lineno; /* Current line number */
- int f_iblock; /* index in f_blockstack */
- char f_executing; /* whether the frame is still executing */
- PyTryBlock f_blockstack[CO_MAXBLOCKS]; /* for try and loop blocks */
- PyObject *f_localsplus[1]; /* locals+stack, dynamically sized */
- } PyFrameObject;
Python实际执行中,会产生很多PyFrameObject对象,这些对象会链接起来,构成执行链表,解释器训练处理链表上的栈帧对象,处理时就入栈,处理完便出栈。
通过PyFrameObject定义代码中的注释可知:
f_back:获得执行环境链表中的上一个栈帧,使新的栈帧在结束后还能回到旧的栈帧对象中。
f_code:存放PyCodeObject对象
f_builtins、f_globals、f_locals:符号表对象(字典类型)
f_valuestack:运行时栈的栈底(解释器会循环处理执行环境链表中的frame,将其入栈处理,f_valuestack指向这个栈的栈底)
f_stacktop:运行时栈的栈顶
f_lasti:当前以及执行过的字节码指令的位置
f_lineno:当前执行的PyCodeObject对象的行号
在Python中可以查看一下上面的属性:
- In [1]: import sys
-
- In [2]: frame = sys._getframe()
-
- In [3]: frame
- Out[3]: <frame at 0x000001A8F2874850, file '<ipython-input-2-8c3916095986>', line 1, code <cell line: 1>>
-
- In [4]: frame.f_back
- Out[4]: <frame at 0x000001A8F384C620, file 'C:\\ProgramData\\Anaconda3\\lib\\site-packages\\IPython\\core\\interactiveshell.py', line 3389, code run_code>
-
- In [5]: frame.f_code
- Out[5]: <code object <cell line: 1> at 0x000001A8F0933190, file "<ipython-input-2-8c3916095986>", line 1>
PyFrameObject是可变长对象,即占用的内存大小是动态分配的,到底要分配多少,主要取决于PyFrameObject对象关联的PyCodeObject对象,PyCodeObject对象对应着具体的代码。
通过dis内置库,我们可以将PyCodeObject对象反编译为指令码(人类可读的),如下:
- (Pdb) l
- 2
- 3
- 4 def fib(n):
- 5 a, b = 1, 1
- 6 pdb.set_trace()
- 7 -> for i in range(n - 1):
- 8 a, b = b, a + b
- 9
- 10 return a
- 11
- 12
- (Pdb) import sys
- (Pdb) sys._getframe()
- <frame at 0x000001CF3758FCC0, file '<stdin>', line 1, code <module>>
- (Pdb) frame = sys._getframe()
- (Pdb) frame.f_code
- <code object <module> at 0x000001CF38ECDDF0, file "<stdin>", line 1>
- (Pdb) frame.f_code.co_code
- b'e\x00\xa0\x01\xa1\x00Z\x02d\x00S\x00'
- (Pdb) import dis
- (Pdb) dis.dis(frame.f_code)
- 1 0 LOAD_NAME 0 (sys)
- 2 LOAD_METHOD 1 (_getframe)
- 4 CALL_METHOD 0
- 6 STORE_NAME 2 (frame)
- 8 LOAD_CONST 0 (None)
- 10 RETURN_VALUE
上述代码中,通过sys._getframe()获得当前frame,然后通过frame的f_code获得PyCodeObject对象,然后利用dis.dis方法将f_code的内容反编译成指令码。
经调试,当我们自己调用pdb.set_trace时,最终会来到bdb的set_trace方法,代码如下:
- def set_trace(self, frame=None):
- """Start debugging from frame.
- If frame is not specified, debugging starts from caller's frame.
- """
- if frame is None:
- frame = sys._getframe().f_back
- self.reset()
- while frame:
- frame.f_trace = self.trace_dispatch
- self.botframe = frame
- frame = frame.f_back
- self.set_step()
- sys.settrace(self.trace_dispatch)
其中的sys.setrace方法便是关键,前文也提了,bdb模块正是基于sys.setrace方法提供的动态插桩能力之上实现代码单步调试的。看一下Python文档对sys.setrace方法的解释:
sys.settrace(tracefunc) 设置系统的跟踪函数,使得用户在 Python 中就可以实现 Python 源代码调试器。
该函数是特定于单个线程的,所以要让调试器支持多线程,必须为正在调试的每个线程都用 settrace() 注册一个跟踪函数,或使用 threading.settrace()。跟踪函数应接收三个参数:frame、event 和 arg。
更多参考:https://roohui.com/help/tutorial/python_3_9/library/sys.html#sys.settrace
为了理解sys.setrace方法,先简单使用一下:
- import sys
- import inspect
-
-
- class Tracer:
-
- def trace(self, frame, event, arg):
- # PyCodeObject
- code = frame.f_code
- module = inspect.getmodule(code)
- # module路径
- module_path = module.__file__
- # module名
- module_name = module.__name__
- print(f'event: {event} '
- f'module_path: {module_path} '
- f'module_name: {module_name} '
- f'code_name: {code.co_name} '
- f'lineno: {frame.f_lineno} '
- f'locals: {frame.f_locals} '
- f'args: {arg}')
-
- return self.trace
-
- def collect(self, func, *args):
- sys.settrace(self.trace)
- func(*args)
-
-
- def add(a, b):
- c = 3
- d = 4
- e = c + d
- return a + b + e
-
-
- if __name__ == '__main__':
- t = Tracer()
- t.collect(add, 1, 2)
上述代码中,在Tracer类的collect方法中使用了sys.settrace方法,传入了trace方法作为跟踪函数,trace方法必须有frame, event, arg三个参数,其含义如下:
frame:当前栈帧
event:事件类型,是str类型,有call、line、return等类型
arg:与event有关
运行上述代码,得如下结果:
- event: call module_path: C:/Users/admin/workplace/play_py/play_tracer.py module_name: __main__ code_name: add lineno: 31 locals: {'a': 1, 'b': 2} args: None
- event: line module_path: C:/Users/admin/workplace/play_py/play_tracer.py module_name: __main__ code_name: add lineno: 32 locals: {'a': 1, 'b': 2} args: None
- event: line module_path: C:/Users/admin/workplace/play_py/play_tracer.py module_name: __main__ code_name: add lineno: 33 locals: {'a': 1, 'b': 2, 'c': 3} args: None
- event: line module_path: C:/Users/admin/workplace/play_py/play_tracer.py module_name: __main__ code_name: add lineno: 34 locals: {'a': 1, 'b': 2, 'c': 3, 'd': 4} args: None
- event: line module_path: C:/Users/admin/workplace/play_py/play_tracer.py module_name: __main__ code_name: add lineno: 35 locals: {'a': 1, 'b': 2, 'c': 3, 'd': 4, 'e': 7} args: None
- event: return module_path: C:/Users/admin/workplace/play_py/play_tracer.py module_name: __main__ code_name: add lineno: 35 locals: {'a': 1, 'b': 2, 'c': 3, 'd': 4, 'e': 7} args: 10
从这个例子可知,sys.setrace方法可以在Python每次执行时都调用跟踪方法(即上面的trace方法)。这个方法很有意思,后面的文章,还会深入到其c源码进行分析。
回到pdb,当我们通过pdb.set_trace()启动pdb调试时,会来到bdb的set_trace方法,该方法会通过sys.settrace方法实现单步调用,其传入的跟踪方法为bdb中的trace_dispatch方法,代码如下:
- def trace_dispatch(self, frame, event, arg):
- """
- 根据不同的event调用不同的跟踪方法(trace function)实现debug
- """
- if self.quitting:
- return # None
- if event == 'line':
- return self.dispatch_line(frame)
- if event == 'call':
- return self.dispatch_call(frame, arg)
- if event == 'return':
- return self.dispatch_return(frame, arg)
- if event == 'exception':
- return self.dispatch_exception(frame, arg)
- if event == 'c_call':
- return self.trace_dispatch
- if event == 'c_exception':
- return self.trace_dispatch
- if event == 'c_return':
- return self.trace_dispatch
- print('bdb.Bdb.dispatch: unknown debugging event:', repr(event))
- return self.trace_dispatch
从代码可知,trace_dispatch方法针对sys.settrace不同的事件类型调用了不同的处理函数,通过Python文档,可以轻松知道这些事件的含义:
call:调用函数时会触发call事件
line:解释器执行新一行代码时,会触发该事件
return:从函数返回时,会触发该事件
exception:发生异常时,会触发该事件
opcode:执行新的操作码时会触发该事件
当然,我们可以通过继承改写的方式,为trace_dispatch方法加上一些输出,可以更直观的理解这些事件,代码如下:
- from pdb import Pdb
-
-
- class MyPdb(Pdb):
- def trace_dispatch(self, frame, event, arg):
- # 加了一行打印
- print(f"[trace_dispatch] frame: {frame}, event: {event}, arg: {arg}")
- return super(MyPdb, self).trace_dispatch(frame, event, arg)
-
-
- def func(a, b):
- c = a + b
- e = 6
- return c + e
-
-
- if __name__ == '__main__':
- MyPdb().set_trace()
- func(1, 2)
代码很简单,就是加了一行print输出,然后我们启动程序,在pdb交互式环境中,进行如下操作:
- # 查看断点位置
- (Pdb) l
- 13 return c + e
- 14
- 15
- 16 if __name__ == '__main__':
- 17 MyPdb().set_trace()
- 18 -> func(1, 2)
- [EOF]
- # 下一行,如果是函数,则进入函数内容,从输出看,事件为call
- (Pdb) s
- [trace_dispatch] frame: <frame at 0x0000011DA363F3A0, file 'C:/Users/admin/workplace/play_py/paly_mypdb.py', line 10, code func>, event: call, arg: None
- --Call--
- > c:\users\admin\workplace\play_py\paly_mypdb.py(10)func()
- -> def func(a, b):
- # 下一行,如果是函数,不进入,事件是line
- (Pdb) n
- [trace_dispatch] frame: <frame at 0x0000011DA363F3A0, file 'C:/Users/admin/workplace/play_py/paly_mypdb.py', line 11, code func>, event: line, arg: None
- > c:\users\admin\workplace\play_py\paly_mypdb.py(11)func()
- -> c = a + b
通过s命令与n命令,我们直观地确认了call与line事件被触发时的情况,也确认了trace_dispatch作为跟踪函数的作用,我们接着深入进去。
如果我们敲击了n命令,代码会执行到dispatch_line方法(trace_dispatch方法判断触发的是line事件,从而调用dispatch_line方法),其代码如下:
- def dispatch_line(self, frame):
- """Invoke user function and return trace function for line event.
- """
- if self.stop_here(frame) or self.break_here(frame):
- self.user_line(frame)
- if self.quitting: raise BdbQuit
- return self.trace_dispatch
在dispatch_line方法中,会通过stop_here方法与break_here方法判断是否需要中断,中断在哪一行,如果要中断,则调用user_line方法,其他dispatch_#event函数结构类似,都是判断是否要中断,然后调用user_#event函数。以dispatch_call方法验证一下:
- def dispatch_call(self, frame, arg):
-
- # XXX 'arg' is no longer used
- if self.botframe is None:
- # First call of dispatch since reset()
- self.botframe = frame.f_back # (CT) Note that this may also be None!
- return self.trace_dispatch
- if not (self.stop_here(frame) or self.break_anywhere(frame)):
- # No need to trace this function
- return # None
- # Ignore call events in generator except when stepping.
- if self.stopframe and frame.f_code.co_flags & GENERATOR_AND_COROUTINE_FLAGS:
- return self.trace_dispatch
- self.user_call(frame, arg)
- if self.quitting: raise BdbQuit
- return self.trace_dispatch
在dispatch_call方法中,如果需要处理,则调用user_call方法。
看回dispatch_line方法中使用的user_line方法,代码如下:
- def user_line(self, frame):
- """This function is called when we stop or break at this line."""
- if self._wait_for_mainpyfile:
- if (self.mainpyfile != self.canonic(frame.f_code.co_filename)
- or frame.f_lineno <= 0):
- return
- self._wait_for_mainpyfile = False
- if self.bp_commands(frame):
- self.interaction(frame, None)
user_line方法会执行interaction方法,实现frame信息的更新并在控制台打印调用栈中当前栈帧的信息,然后通过_cmdloop方法阻塞,等待用户的命令输入,代码如下:
- def interaction(self, frame, traceback):
- # Restore the previous signal handler at the Pdb prompt.
- if Pdb._previous_sigint_handler:
- try:
- signal.signal(signal.SIGINT, Pdb._previous_sigint_handler)
- except ValueError: # ValueError: signal only works in main thread
- pass
- else:
- Pdb._previous_sigint_handler = None
- # 更新frame信息
- if self.setup(frame, traceback):
- # no interaction desired at this time (happens if .pdbrc contains
- # a command like "continue")
- self.forget()
- return
- # 打印当前栈帧信息
- self.print_stack_entry(self.stack[self.curindex])
- # 等待用户输出
- self._cmdloop()
- # 清理行号、调用栈等信息
- self.forget()
至此,pdb调试Python程序时,pdb内部的执行流程就清晰了,路径如下:
pdb.set_trace() -> sys.settrace() -> dispatch_trace() -> dispatch_line() -> interaction() -> _cmdloop()
回顾上文中,关于cmd的讨论,我们定义了do_name方法、do_exit方法匹配name命令与exit命令,那pdb是否也一样呢?
答案是:一样的,Pdb类中有很多 do_#命令 方法,分别对于这我们的pdb命令,如下图:
看到do_l与do_ll方法,我眼前一亮,因为我看pdb源码的目的就是为了搞清楚本文一开始的问题,为啥pdb获得不到某Python程序中的源码。
l命令主要用于查询当前断点周围的源代码,默认是周围11行,来看看它是怎么获得的:
- def do_list(self, arg):
- # ... 省略
-
- # 从当前栈帧中获得文件名
- filename = self.curframe.f_code.co_filename
- breaklist = self.get_file_breaks(filename)
- try:
- # 通过getlines方法获得
- lines = linecache.getlines(filename, self.curframe.f_globals)
- self._print_lines(lines[first-1:last], first, breaklist,
- self.curframe)
- self.lineno = min(last, len(lines))
- if len(lines) < last:
- self.message('[EOF]')
- except KeyboardInterrupt:
- pass
- do_l = do_list
我删了do_list方法中处理arg的逻辑,主要就是计算你需要获得周围多少行的源码。
do_list方法首先会从当前栈帧对象中获得当前文件路径,然后通过getlines方法获得源代码。
getlines方法中有缓存的逻辑,第一次获取源代码会通过tokenize.open方法去打开源文件,并读取其中的代码,然后再将代码存入缓存,其关键代码在linecache.py的updatecache方法中,代码如下:
- def updatecache(filename, module_globals=None):
- # ... 省略不重要代码
- try:
- print('ayu fullname: ', fullname)
- with tokenize.open(fullname) as fp:
- lines = fp.readlines()
- print('ayu lines: ', lines)
- except OSError:
- return []
- if lines and not lines[-1].endswith('\n'):
- lines[-1] += '\n'
- size, mtime = stat.st_size, stat.st_mtime
- cache[filename] = size, mtime, lines, fullname
- return lines
为了验证一下,我依旧通过继承的方式重写了updatecache方法,加了一些print,效果如下:
- (Pdb) l
- ayu fullname: C:/Users/admin/workplace/play_py/paly_mypdb.py
- ayu lines: ['from pdb import Pdb\n', '\n', '\n', 'class MyPdb(Pdb):\n', ' def trace_dispatch(self, frame, event, arg):\n', ' print(f"[trace_dispatch] frame: {frame}, event: {event}, arg: {arg}")\n', ' return super(MyPdb, self).trace_dispatch(frame, event, arg)\n', '\n', '\n', 'def func(a, b):\n', ' c = a + b\n', ' e = 6\n', ' return c + e\n', '\n', '\n', "if __name__ == '__main__':\n", ' MyPdb().set_trace()\n', ' func(1, 2)\n']
- 13 return c + e
- 14
- 15
- 16 if __name__ == '__main__':
- 17 MyPdb().set_trace()
- 18 -> func(1, 2)
- [EOF]
- (Pdb)
ll命令可以获得当前文件所有源码,代码如下:
- def do_longlist(self, arg):
- """longlist | ll
- List the whole source code for the current function or frame.
- """
- filename = self.curframe.f_code.co_filename
- breaklist = self.get_file_breaks(filename)
- try:
- # 获得文件中所有代码
- lines, lineno = getsourcelines(self.curframe)
- except OSError as err:
- self.error(err)
- return
- self._print_lines(lines, lineno, breaklist, self.curframe)
- do_ll = do_longlist
主要通过getsourcelines方法实现这样的效果,而getsourcelines方法主要利用inspect内置模块来获取信息,代码如下:
- def getsourcelines(obj):
- lines, lineno = inspect.findsource(obj)
- if inspect.isframe(obj) and obj.f_globals is obj.f_locals:
- # must be a module frame: do not try to cut a block out of it
- return lines, 1
- elif inspect.ismodule(obj):
- return lines, 1
- return inspect.getblock(lines[lineno:]), lineno+1
至此可知,如果ll命令无法获得源代码,单独通过inspect模块中的方法,也是无法获取的。
- def do_next(self, arg):
- """n(ext)
- Continue execution until the next line in the current function
- is reached or it returns.
- """
- self.set_next(self.curframe)
- return 1
- do_n = do_next
-
- def set_next(self, frame):
- """Stop on the next line in or below the given frame."""
- self._set_stopinfo(frame, None)
其核心逻辑在_set_stopinfo方法中,该方法主要设置pdb调试过程中,需要中断在哪一帧这样的效果,代码如下:
- def _set_stopinfo(self, stopframe, returnframe, stoplineno=0):
- """Set the attributes for stopping.
- If stoplineno is greater than or equal to 0, then stop at line
- greater than or equal to the stopline. If stoplineno is -1, then
- don't stop at all.
- """
- self.stopframe = stopframe
- self.returnframe = returnframe
- self.quitting = False
- # stoplineno >= 0 means: stop at line >= the stoplineno
- # stoplineno -1 means: don't stop at all
- self.stoplineno = stoplineno
代码并不复杂,就是设置不同变量的值而已,它之所以能实现中断效果,是该方法设置的值,会影响stop_here方法。
前面提到了dispatch_#event相关的方法,都会调用stop_here方法判断这里是否需要中断,而stop_here方法会基于_set_stopinfo方法中的设置的变量来判断是否要中断。
简单而言,当我们调用n命令时,会通过_set_stopinfo方法更新断点信息,让pdb在下一行也是中断的状态。
- def do_step(self, arg):
- """s(tep)
- Execute the current line, stop at the first possible occasion
- (either in a function that is called or in the current
- function).
- """
- self.set_step()
- return 1
- do_s = do_step
-
- def set_step(self):
- """Stop after one line of code."""
- # Issue #13183: pdb skips frames after hitting a breakpoint and running
- # step commands.
- # Restore the trace function in the caller (that may not have been set
- # for performance reasons) when returning from the current frame.
- if self.frame_returning:
- caller_frame = self.frame_returning.f_back
- if caller_frame and not caller_frame.f_trace:
- # 跟踪方法赋值给f_trace
- caller_frame.f_trace = self.trace_dispatch
- self._set_stopinfo(None, None)
比n命令多了设置caller_frame的逻辑,关键的一行在于caller_frame.f_trace = self.trace_dispatch,从而让pdb可以进入函数内部进行debug。
u命令主要是回到上一栈帧,比如我们通过s命令,进入了函数体后,有些变量或方法无法使用了,我们可能会想回到上一栈帧,此时便可以通过u命令实现。
u命令代码如下:
- def _select_frame(self, number):
- assert 0 <= number < len(self.stack)
- self.curindex = number
- # 回到上一栈帧
- self.curframe = self.stack[self.curindex][0]
- self.curframe_locals = self.curframe.f_locals
- self.print_stack_entry(self.stack[self.curindex])
- self.lineno = None
-
- def do_up(self, arg):
- """u(p) [count]
- Move the current frame count (default one) levels up in the
- stack trace (to an older frame).
- """
- if self.curindex == 0:
- self.error('Oldest frame')
- return
- try:
- count = int(arg or 1)
- except ValueError:
- self.error('Invalid frame count (%s)' % arg)
- return
- if count < 0:
- newframe = 0
- else:
- newframe = max(0, self.curindex - count)
- self._select_frame(newframe)
- do_u = do_up
从代码可知,u命令回到上一个栈帧的效果主要基于self.stack列表(调用栈)实现,而之所以能获得上一栈帧中的变量与方法,是因为栈帧对象中关联着当前栈帧的局部变量、全局变量等信息。
至此,pdb的源码就剖析完了,通过本文,你应该知道个大概了,但要深入其中,还是需要靠各位自己去调试与研究。
最后,惯例卖卖自己的书,如果你或身边的朋友想学Python或Python自动化相关的内容,可以购买我的书籍,目前5折再售哟。