GEM5教程--修改和拓展gem5(二)
三、调试GEM5
gem5通过调试标志提供对printf样式的代码跟踪/调试的支持。 这些标志允许每个组件具有许多调试打印语句,而无需同时启用所有这些语句。 运行gem5时,您可以从命令行指定要启用的调试标志。
1、使用调试标志(debug flags)
(1)
例如,运行“创建简单配置脚本”中的第一个simple.py脚本时,如果启用DRAM调试标志,则会得到以下输出。 请注意,这会向控制台生成大量输出(大约7 MB)。
build/X86/gem5.opt --debug-flags=DRAM configs/learning_gem5/part1/simple.py | head -n 50
(2)
或者,您可能想根据CPU正在执行的确切指令进行调试。 为此,Exec
调试标志可能会很有用。 该调试标志显示了模拟CPU如何执行每条指令的详细信息。
build/X86/gem5.opt --debug-flags=Exec configs/learning_gem5/part1/simple.py | head -n 50
(3)
实际上,Exec标志实际上是多个调试标志的集合。 通过使用–debug-help参数运行gem5,可以看到此信息以及所有可用的调试标志。
build/X86/gem5.opt --debug-help
2、添加一个新的调试标志
(1)在前面的部分中,我们使用了一个简单的std :: cout从SimObject中进行打印。 虽然可以在gem5中使用普通的C / C ++ I / O,但强烈建议不要这样做。 因此,我们现在将替换它,而使用gem5的调试工具。
创建新的调试标志时,我们首先必须在SConscript文件中声明它。使用hello对象代码(src/learning_gem5/)将以下内容添加到目录中的SConscript文件中。
DebugFlag('Hello')
这声明了一个调试标志“Hello”。现在,我们可以在SimObject的调试语句中使用它。
通过在SConscript文件中声明标志,将自动生成一个调试头,允许我们使用调试标志。头文件位于调试目录中,与我们在SConscript文件中声明的名称(和大写)相同。因此,我们需要在计划使用调试标志的任何文件中包含自动生成的头文件。
在hello_object.cc文件中,我们需要包含头文件:
#include "debug/Hello.hh"
现在我们已经包含了必要的头文件,让我们用这样的调试语句替换std::cout调用。
DPRINTF(Hello, "Created the hello object\n");
DPRINTF是C++宏。第一个参数是在SConscript文件中声明的调试标志。我们可以使用Hello标志,因为我们在src/learning_gem5/SConscript文件中声明了它。其余的参数是可变的,可以是传递给printf语句的任何参数。现在,如果重新编译gem5并使用“Hello”调试标志运行它,将得到以下结果。:
build/X86/gem5.opt --debug-flags=Hello configs/learning_gem5/part2/run_hello.py
3、调试输出
对于每个动态DPRINTF执行,有三件事被打印到stdout。首先,执行DPRINTF时的当前刻度。第二,名为DPRINTF的SimObject的名称。此名称通常是Python配置文件中的Python变量名。但是,名称是SimObject name()函数返回的任何内容。最后,您将看到传递给DPRINTF函数的任何格式字符串。
可以使用–debug file参数控制调试输出的位置。默认情况下,所有调试输出都打印到stdout。但是,可以将输出重定向到任何文件。文件是相对于主gem5输出目录而不是当前工作目录存储的。
4、使用DPRINTF以外的函数
DPRINTF是gem5中最常用的调试函数。然而,gem5提供了一些在特定情况下有用的其他功能。
(1)
DPRINTF(Flag, VA_ARGS):
获取标志、格式字符串和任何格式参数。此函数要求当前作用域中存在name()函数(例如,从SimObject成员函数调用)。仅在启用标志时打印格式化字符串。
(2)
DTRACE(Flag):
如果启用了标志(标志),则返回true,否则返回false。只有在启用调试标志(标志)时,这对于执行某些代码才有用。
(3)
DDUMP(Flag, data, count):
打印长度计数字节的二进制数据(数据)。它以十六进制格式以用户可读的方式格式化。此宏还假设调用范围包含name()函数。
(4)
DPRINTFS(Flag, SimObject, VA_ARGS):
与DPRINTF()类似,除了接受一个额外的参数,该参数是一个具有name()函数的对象,通常是一个SimObject。此函数用于从SimObject的私有子类(具有指向其所有者的指针)进行调试。
(5)
DPRINTFR(Flag, VA_ARGS):
此函数输出调试语句而不打印名称。这对于在不是没有name()函数的SimObjects的对象中使用调试语句非常有用。
(6)
DDUMPN(data, count),DPRINTFN(VA_ARGS),DPRINTFNR(VA_ARGS):
这些函数与前面的函数DDUMP()、DPRINTF()和DPRINTFR()类似,只是它们不接受标志作为参数。因此,每当启用调试时,这些语句都将始终打印。
只有在“opt”或“debug”模式下编译gem5时,才会启用所有这些函数。所有其他模式对上述函数使用空的占位符宏。因此,如果要使用调试标志,必须使用“gem5.opt”或“gem5.debug”。
四、事件驱动编程
gem5是一个事件驱动的模拟器。在本章中,我们将探讨如何创建和安排事件。我们将从简单的HelloObject,来自“创建一个非常简单的SimObject”。
1、创建简单事件回调
在gem5的事件驱动模型中,每个事件都有一个回调函数来处理事件。通常,这是一个从事件继承的类。然而,gem5提供了一个用于创建简单事件的包装函数。
(1)
在HelloObject的头文件中,我们只需要声明一个新函数,每次事件触发时我们都要执行它(processEvent())。此函数不能接受任何参数,也不能返回任何内容。
(2)
接下来,我们添加一个事件实例。在本例中,我们将使用EventFunctionWrapper,它允许我们执行任何函数。 我们还添加了一个startup()函数,将在下面进行解释。
class HelloObject : public SimObject
{
private:
void processEvent();
EventFunctionWrapper event;
public:
HelloObject(HelloObjectParams *p);
void startup();
};
(3)
接下来,我们必须在HelloObject的构造函数中构造此事件。eventFunctionWrapper有两个参数,一个要执行的函数和一个名称。名称通常是拥有事件的SimObject的名称。打印名称时,将在名称末尾附加一个自动的“.wrapped_function_event”。
第一个参数只是一个不接受任何参数且没有返回值的函数(std::function<void(void)>)。通常,这是一个调用成员函数的简单lambda函数。但是,它可以是任何你想要的功能。下面,我们在lambda([this])中对其进行大写,这样就可以调用类实例的成员函数。
HelloObject::HelloObject(HelloObjectParams *params) :
SimObject(params), event([this]{processEvent();}, name())
{
DPRINTF(Hello, "Created the hello object\n");
}
2、安排事件
最后,要处理事件,我们首先必须安排事件。为此,我们使用schedule()函数。此函数用于在将来的一段时间内安排事件的某些实例(事件驱动的模拟不允许在过去执行事件)。
schedule(Event *event, Tick when):计划在时间执行的事件(事件)。此函数将事件放入事件队列,并在勾选时执行事件。
我们将首先在添加到HelloObject类的startup()函数中安排事件。函数的作用是允许SimObjects安排内部事件。直到模拟第一次开始(即从Python配置文件调用simulate()函数),它才会被执行。
void
HelloObject::startup()
{
schedule(event, 100);
}
在这里,我们只是将事件安排在tick 100执行。通常,您会使用curTick()的一些偏移量,但是由于我们知道startup()函数是在当前时间为0时调用的,所以我们可以使用显式的tick值。
3、更多事件调度
我们还可以在事件流程动作中安排新事件。例如,我们将向HelloObject添加一个延迟参数,并为触发事件的次数添加一个参数。在下一部分中,我们将从Python配置文件中访问这些参数。
(1)
在HelloObject类声明中,为延迟和要触发的次数添加一个成员变量。
class HelloObject : public SimObject
{
private:
void processEvent();
EventFunctionWrapper event;
Tick latency;
int timesLeft;
public:
HelloObject(HelloObjectParams *p);
void startup();
};
(2)
然后,在构造函数中添加延迟和剩余时间的默认值。
HelloObject::HelloObject(HelloObjectParams *params) :
SimObject(params), event([this]{processEvent();}, name()),
latency(100), timesLeft(10)
{
DPRINTF(Hello, "Created the hello object\n");
}
(3)
最后,更新startup()和processEvent()。
void
HelloObject::startup()
{
schedule(event, latency);
}
void
HelloObject::processEvent()
{
timesLeft--;
DPRINTF(Hello, "Hello world! Processing the event! %d left\n", timesLeft);
if (timesLeft <= 0) {
DPRINTF(Hello, "Done firing!\n");
} else {
schedule(event, curTick() + latency);
}
}