Simics Analyzer provides a full symbolic C/C++ debugger with a command line interface. You can read all about it in Simics Analyzer User’s Guide . This can be used in parallel with the low-level features described in this chapter.
This chapter explains Simics’s powerful, but low-level, breakpoint support.
Simics can set breakpoints on code and data. Unlike most debuggers, Simics breakpoints are not limited by what the hardware can support; for example, there is no restriction on the number of read/write breakpoints (also known as watchpoints).
In Simics you can set breakpoints on, for instance:
Simics is fully deterministic, and breakpoints in Simics are fully non-intrusive. This makes it possible to narrow down the location of difficult bugs by re-running the exact same run as many times as you need.
Simics has a breakpoint manager which stores information about all breakpoints, of all types. It has commands for listing breakpoints, enabling, deleting and other common operations. Breakpoint creation is specific to the different types, and is described in the following chapters. The breakpoint manager is the object bp.
In general, breakpoint creation commands return an ID, which can be used to later delete the breakpoint, or obtain information about it. The most important generic commands are
bp.list, which lists all breakpoints.bp.delete, which removes a particular breakpoint.bp.show, which shows more information about a particular breakpoint.A memory breakpoint stops the simulation whenever a memory location in a specified address interval is accessed. The address interval can be of arbitrary length and the type of memory access can be specified as any combination of read, write, and execute.
The easiest way to set memory breakpoints is to use the bp.memory.break command:
simics> output-radix 16
simics> bp.memory.break p:0x10000
Breakpoint 1 set on address 0x10000 in 'board.mb.cpu0.mem[0][0]' with access mode 'x'
Prefix the address with p: or v: to get a physical or virtual address, respectively. As you can see in the following example, Simics defaults to interpreting a breakpoint address as virtual if you do not specify otherwise:
simics> board.mb.cpu0.core[0][0]->current_virtual_context = "board.cell_context"
simics> bp.memory.break v:0x4711
[board.mb.cpu0.core[0][0] info] VMP not engaged. Reason: virtual breakpoint.
Breakpoint 2 set on address 0x4711 in 'board.cell_context' with access mode 'x'
simics> bp.memory.break p:0x4711
Breakpoint 3 set on address 0x4711 in 'board.mb.cpu0.mem[0][0]' with access mode 'x'
simics> bp.memory.break 0x4711
Breakpoint 4 set on address 0x4711 in 'board.cell_context' with access mode 'x'
Note: overlaps with breakpoint 2
This way of setting breakpoints will attach them to the memory space (physical address) or context (virtual address) connected to the current processor. If the current processor is not the processor you are interested in, it can be changed using the pselect command:
simics> pselect board.mb.cpu0.core[0][0]
Without an argument, pselect prints the current processor:
simics> pselect
"board.mb.cpu0.core[0][0]"
Also useful is the cpu object alias, which returns the current processor and can be used to expand child objects, commands and aliases on the current processor.
simics> cpu # type cpu<tab>
cpu-> cpu-pages-dump cpu-switch-time cpu. cpu.vtime.
Physical memory breakpoints are handled by memory space objects. A memory space represents a physical address space; they sit between the processor and the actual hardware devices, for example RAM, that can be accessed with read and write instructions. Breakpoints are created with the memory space’s break command:
simics> board.mb.cpu0.core[0][0]->physical_memory
"board.mb.cpu0.mem[0][0]"
simics> board.mb.cpu0.mem[0][0].bp-break-memory address = 0x10000 length = 16 -w
Breakpoint 1 set on address 0x10000 in 'board.mb.cpu0.mem[0][0]', length 16 with access mode 'w'
Virtual memory breakpoints are handled by context objects. A context represents a virtual address space; you can learn more about them in chapter 3.2.2. Essentially, they provide a level of indirection between processors and virtual memory breakpoints; a processor has a current context, which in turn has virtual breakpoints:
simics> board.mb.cpu0.core[0][0]->current_context
"board.cell_context"
simics> board.cell_context.bp-break-memory 0x1ff00
Breakpoint 2 set on address 0x1ff00 in 'board.cell_context' with access mode 'x'
Note that by default, all simulated processors in a cell share one context (celln_context). If you want a virtual breakpoint to apply only to a subset of the processors, create a new context just for them:
simics> new-context foo
simics> board.mb.cpu0.core[0][0].set-context foo
simics> foo.bp-break-memory 0xffffffffbfc008b8
When creating execution breakpoints, it is possible to specify filter rules to only trigger when instructions match certain syntactical criteria. This feature is mainly useful with breakpoints covering large areas of memory. The available parameters to the break command are prefix (to match the start of an instruction), substr (to match a particular substring), and pattern (to match the bit pattern of the instruction).
For example, to stop when an instruction with the name add is executed in a memory range from 0x10000 to 0x12000, use the following commands:
simics> bp.memory.break 0x10000 0x2000 -x prefix = "add"
Breakpoint 1 set on address 0x10000 in 'board.cell_context', length 8192 with access mode 'x'
Simics will now break on the first add instruction encountered. For more information, see the
breakpoints section in the Simics Reference Manual
or use the help bp.memory.break command.
Unlike an ordinary debugger, Simics can handle temporal breakpoints, i.e., breakpoints in time. Since the concept of time is based on steps and cycles, to be precise a temporal breakpoint refers to a specific step or a cycle count as measured by a given processor, but one can also specify breakpoints in seconds of virtual time:
simics> bp.cycle.break cpu0 100
simics> bp.step.break cpu0 100
simics> bp.time.break cpu0 1
In the example above, the breakpoints are specified relative to the current time. It is also possible to give temporal breakpoints in absolute time (where 0 refers to the time when the original configuration was set up in Simics).
simics> board.mb.cpu0.core[0][0].bp-break-cycle -absolute 100
simics> board.mb.cpu0.core[0][0].bp-break-step -absolute 100
simics> board.mb.cpu0.core[0][0].bp-break-time -absolute 1
The commands bp.cycle.break, bp.step.break and bp.time.break, can be given without providing the CPU ad argument. This will set a breakpoint for the current processor.
A control register breakpoint triggers when the selected control register is accessed. The access type is determined by the break command parameters used.
To break on a control register write, use bp.control_register.break. For example, to break when the register cr0 is written to:
simics> bp.control_register.break name = cr0
To break on a control register read, use bp.control_register.break -r. For example, to break when the register cr0 is read:
simics> bp.control_register.break name = cr0 -r
A list of available control registers can be obtained by tab-completing the name argument. See the documentation for bp.control_register.break in the Simics Reference Manual for more information.
An I/O breakpoint is always connected to a specific device object. The breakpoint is triggered when that device is accessed. The breakpoint is set using the break-io command, which take the device name as a parameter. For example, to break on accesses to a device called dma, we would use the following syntax:
simics> break-io device = board.mb.sb.dma
A list of devices can be obtained by tab-completing the device argument.
Many simulated machines have a text console—a terminal window hooked up to a serial port on the target machine, so that you can type commands to the target and get replies. In particular x86 systems also often have a graphics console—a window acting as a monitor to the machine, hooked up to a graphics device on the target machine, and usually also to keyboard and mouse devices.
A text console can halt the simulation on the occurrence of a given character sequence in the output; this is called a text output breakpoint. This is also possible on a graphics console when it is running in text mode graphics, assuming that the graphics device implements this.
To set a breakpoint, use the command bp.console_string.break console string, which returns a breakpoint id. Simics will stop when string appears in the output.
To find out if a specific simulated machine uses these consoles, look for objects of class
textconorgraphconin the list provided bylist-objectsonce the configuration is loaded.
If your target machine has a graphical display (as opposed to just a text console), you can set graphical breakpoints on it. A graphical breakpoint is a (small or large) bitmap image and a pair of coordinates; when the pixels at those coordinates on the simulated display exactly match the breakpoint image, the simulation will halt.
Graphics breakpoints can be created from the console GUI window, but the following commands can also be used to save and set breakpoints for a graphics console:
gfx-console.save-break-xy filename left top right bottomgfx-console.bp-break-gfx filenamebp.delete idFor each simulated processor architecture, a special nop (no-operation) instruction has been chosen to be a magic instruction for the simulator. When the simulator executes such an instruction, it triggers a Core_Magic_Instruction hap and calls all the callbacks functions registered on this hap.
Since magic instructions are just no-operation instructions on hardware, you can run code containing magic instructions on hardware as well as in the simulator, but you will not get any of the extra behavior Simics implements for the magic instruction.
If the architecture makes it possible, a magic instruction parameter is encoded as an immediate value in the magic instruction. When the hap is triggered, this value is passed as an argument to the hap handlers. This provides the user with a rudimentary way of passing information from the simulated system to the hap handler.
Magic instructions have to be compiled into the binary files that are executed on the target. The file magic-instruction.h in [simics]/src/include/simics/ defines a MAGIC(n) macro that can be used to place magic instructions in your program, where n is the magic instruction parameter value to encode. Some parameter values are reserved for internal use; see figure 29 and figure 30.
A complete list of magic instructions and the range of the parameter n is provided in figure 29.
The declaration of the macros are heavily dependent on the compiler used, so you may get an error message telling you that your compiler is not supported. In that case, you will have to write the inline assembly corresponding to the magic instruction you want to use. The GCC compiler should always be supported.
The magic instruction macro is directly usable only from C and C++; if your program is written in another language, you will have to call a C function that uses the macro, or an assembly function that includes the magic instruction. (If the language supports inline assembly, that can of course be used as well.) For example, in Java it would be necessary to use the JNI interface. Check your compiler and language documentation for details.
(x86 specific) The magic instruction parameter is passed through the
eaxregister on x86, as this value:0x4711 | (n << 16), wherenis the magic parameter. Theeaxregister content is changed tonwhen the magic instruction is emulated by Simics. Thus theeaxregister value can be used just after the magic instruction to extract the magic parameter. There is also a local variable defined in the scope of the MAGIC macro for x86, calledsimics_magic_instr_dummy. Software debuggers can read out the magic parameter, after a magic instruction, by reading thesimics_magic_instr_dummyvariable, as it is bound to the output of the eax register. You have to make sure that the compiler does not optimize away this variable if you intend to use it.The values of the
ebx,ecxandedxregisters are undefined after the magic instruction. Earlier Simics versions had different definitions of magic instructions for x86. The old magic instruction can still be used on x86 if the processor is not running in VMP mode.
(PowerPC specific) There are two different encodings of the
rlwimi-based magic instruction on PowerPC. On 64-bit models, the new encoding is always used; it is also the one generated by theMAGIC()andMAGIC_BREAKPOINT()macros inmagic-instruction.hwhen compiling 64-bit PowerPC code. The old encoding is used on 32-bit models when theold_rlwimi_magicattribute is set. When compiling 32-bit PowerPC code, the macros will use the old encoding unless the preprocessor symbolSIM_NEW_RLWIMI_MAGIChas been defined.It is recommended that the new encoding is used with 32-bit PowerPC models and code by setting the appropriate attribute and preprocessor symbol.
| Target | Magic instruction | Conditions on |
|
| ARC | mov 0, n | 1 ≤ n < 64 | |
| ARM | orr rn, rn, rn | 0 ≤ n ≤ 14 | |
| ARMv8 | orr xn, xn, xn | 0 ≤ n ≤ 31 | |
| ARM Thumb-2 | orr.w rn, rn, rn | 0 ≤ n ≤ 12 | |
| H8300 | brn n | -128 ≤ n ≤ 127 | |
| M680x0 | dbt dx,y | 0 ≤ n < 0x3ffff | |
| x=n[17:15], y=n[14:0] * 2 | |||
| MIPS | li %zero, n | 0 ≤ n < 0x10000 | |
| Nios II | or rN, rN, rN, | 0 ≤ N < 32 | |
| PowerPC | rlwimi x,x,0,y,z | 0 ≤ n < 8192 | new encoding |
| x=n[12:8], y=n[7:4], z=n[3:0]|16 | |||
| PowerPC | rlwimi x,x,0,y,z | 0 ≤ n < 32768 | old encoding |
| x=n[14:10], y=n[9:5], z=n[4:0] | |||
| RISC-V | srai zero, zero, n | 0 ≤ n ≤ 31 | |
| SH | mov rn, rn | 0 ≤ n < 16 | |
| SPARC | sethi n, %g0 | 1 ≤ n < 0x400000 | |
| x86 | cpuid | 0 ≤ n < 0x10000 | |
| with eax = 0x4711 + n * 216 |
| Reserved Magic Numbers |
| 0 |
| 12 |
Here is a simple example of how to use magic instructions:
#include "magic-instruction.h"
int main(int argc, char **argv)
{
initialize();
MAGIC(1); // tell the simulator to start
// the cache simulation
do_something_important();
MAGIC(2); // tell the simulator to stop
// the cache simulation
clean_up();
}
This code needs to be coupled with a callback registered on the magic instruction hap to handle what happens when the simulator encounters a magic instruction with the arguments 1 or 2 (in this example, to start and stop the cache simulation).
Simics implements a special handling of magic instructions called magic breakpoints. A magic breakpoint occurs if magic breakpoints are enabled and if the parameter n of a magic instruction matches a special condition. When a magic breakpoint is triggered, the simulation stops and returns to the prompt.
Magic breakpoints can be enabled with the command bp.magic.break number = n . The command takes the number n and corresponds to the parameter passed to the MAGIC macro above. If the number is omitted, it defaults to 0. You can also pass -all that will break on all possible arguments.
Note that the value 0 is included for architectures where no immediate can be specified. The file magic-instruction.h defines a macro called MAGIC_BREAKPOINT that places a magic instruction with a correct parameter value in your program, depending on the architecture.
Sometimes a simple magic instruction is not enough—for example, if you want to communicate with the target code through some channel. To do that, you can define your own magic instruction for the target that reserves memory or registers for communication. In our example below, we will reserve a register for passing a character to the magic handler. The purpose is to be able to capture text output; i.e., we do not want the program running on the target to write to standard output. Instead, we will grab the characters, issue a log message, and then inform the target program that it should not process the character normally. To illustrate this, we will using the x86 architecture.
The normal MAGIC macro for x86, as defined in magic-instruction.h looks like this:
#define MAGIC(n) do { \
int simics_magic_instr_dummy; \
__MAGIC_CASSERT((unsigned)(n) < 0x10000); \
__asm__ __volatile__ ("cpuid" \
: "=a" (simics_magic_instr_dummy) \
: "a" (0x4711 | ((unsigned)(n) << 16)) \
: "ecx", "edx", "ebx"); \
} while (0)
The asm volatile syntax is used to insert assembler instructions in our target program. The x86 architecture uses the cpuid instruction as a magic instruction as described above. This instruction normally uses eax and ecx registers as input/output and also writes to the ebx and edx registers. As a magic instruction however, Simics only uses eax as input for the magic parameter, but mangled as “(0x4711 | ((unsigned)(n) << 16))”. Here, “a” means to bound the expression to the eax register. We are using an undefined leaf node for cpuid to communicate this value, which will be the result of the expression. A normal processor will just ignore this, but can write random values to the output registers (ecx, edx, and ebx), so we cannot use them as a communication channel. In Simics, if the magic leaf node is used, then the output value of eax is defined to the magic number itself, i.e., eax will be set to n, so we cannot use that register either.
To overcome this we use another register-edi for our communication-so we make a special version of the MAGIC macro that will fit our needs. This version will have two parameters, one for the magic parameter, and one for the character, chr, that we want to capture.
#define MAGIC_CHAR(n, chr) \
asm volatile ( \
"cpuid\n\t" \
: "+D" (chr) /* edi as input/output */ \
: "a" (0x4711 | ((unsigned)(n) << 16)) \
: "ebx", "ecx", "edx" \
);
The syntax “+D (chr)” means that the chr parameter to the macro will be bound to the edi register. See the documentation of the cpuid instruction and GCC’s extended inline assembly for more information.
The code in the target program can be written like this:
static int putmagicchar(int c)
{
MAGIC_CHAR(113, c);
return c;
}
static bool
captured_message(char *buf, int len)
{
for (int i = 0; i < len; i++) {
int chr = buf[i];
if (putmagicchar(chr) == chr)
return false; // no simulation involvement
}
return true;
}
void putstr(char *buf, int len)
{
if (captured_message(buf, len))
return;
for (int i = 0; i < len; i++) {
putchar(buf[i]);
}
}
Here, we modify the standard putstr function in our code—assuming this is the low-level output function we intend to manipulate—and add an extra if statement to check whether the string has already been captured. If it hasn’t, we proceed with the usual loop that calls putchar (not shown) for each character. If the function returns true, indicating successful capture, we skip the rest of the code since the string has already been handled. This allows us to avoid executing the putchar logic, which is likely complex and may involve OS-level operations.
The special logic is controlled by the captured_message function. In this function, a similar output loop is executed, but for each character, the putmagicchar function is called. This function uses the magic instruction to communicate with the simulator. When the magic instruction is executed, the corresponding hap callback will be called if installed (see below). The callback can then read the input character by inspecting the edi register on the target machine. The character may be redirected to an alternate destination, such as a log output message.
Within the callback, the edi value can also be modified to communicate back to the target program. If it is set to -1 (i.e., an illegal character), this signals that the character has been captured, and the loop in captured_message will continue to process the next character. Conversely, if the character (i.e., edi) is not modified, the loop will exit and captured_message will return false, indicating that no capturing was performed and the normal output code should proceed.
The target code cannot determine whether it is running on a simulator with an inactive magic instruction or on actual hardware. If the magic instruction is activated and the cpuid instruction is run in the simulator, it can be observed that the edi register has been modified—magically. This code serves as an illustration and does not handle the case where the magic instruction is disabled in the middle of a string.
A corresponding callback for the magic instruction can now be defined, as shown below. The cpu_info_t variable ci, which is passed as user data, contains information about the CPU on which the callback is installed. For example, the int_register interface pointer has been cached in ci->cpu.ir_iface to enable efficient reading and writing of the edi register. This caching is performed in a code snippet not shown here. The callback is defined as follows:
void magic_cb(void *user_data, conf_object_t *cpu, void *param) {
cpu_info_t *ci = user_data;
// The character input to the magic instruction is placed in edi
int chr = ci->cpu.ir_iface->read(ci->cpu.obj, ci->cpu.edi_num);
SIM_LOG_INFO(4, ci->obj, 0, "capture char: %c", chr);
// Skip the rest of the code in the captured_message function
// by setting edi to -1
ci->cpu.ir_iface->write(ci->cpu.obj, ci->cpu.edi_num, 0xffffffff);
}
The callback is installed with the following code and that also switches on the magic instruction (ci->cpu.obj holds the CPU object that we want to use):
SIM_hap_add_callback_obj_index(
"Core_Magic_Instruction",
cpu_info->cpu.obj, 0, (obj_hap_func_t)magic_cb, cpu_info,
113); // our magic number
This example code is available in the magic_character_capture extension module in the Base package. It is slightly more advanced than the version shown here and allows the captured output to be sent to another textcon object, enabling live updates of characters in a designated area.
The reason for capturing only one character at a time, instead of an entire string—which might be faster—is that it is significantly simpler. There is no need to read from memory or handle logical-to-physical address translations. Additionally, capturing a full string could lead to issues if the character buffer crosses a page boundary where the second page is not yet mapped, potentially causing a page fault on the target machine. Passing a single character as an integer avoids these complications. If a page fault occurs on the target machine, the target operating system will handle it and then proceed to call the next character.
If inline assembler is not available in your compiler—as is the case with Microsoft Visual C++—you may need to implement the MAGIC_CHAR macro as a function written directly in assembly language.
Below is an example written in assembly, assuming the 64-bit Windows API:
.code
MagicChar proc
; rcx = n (unsigned int), first input
; rdx = chr (unsigned int), second input
push rbx ; save callee saved register
push rdi ; save callee saved register
; Construct eax = 0x4711 | (n << 16)
mov eax, 4711h
mov rbx, rcx
shl rbx, 16
or eax, ebx
; Set edi = chr
mov edi, edx
cpuid
; Return edi in eax
mov eax, edi
pop rdi ; restore
pop rbx ; restore
ret
MagicChar endp
end
This code can be used when the asm syntax is not available in the compiler or when you need better control over the code. A similar version for the 32-bit ABI can also be implemented.
A vital part of a debugger’s task is to understand the system being debugged at a higher level than just machine instructions and memory contents. The user thinks in terms of processes, functions, and named variables, so the debugger presents a view of the software that matches these concepts. This view is even more important in Simics, where the user has access to the whole system and not only user processes. To handle this, Simics Analyzer provides a full symbolic debugger, which you can read about in Simics Analyzer User’s Guide.
Simics provides only some primitive building blocks for basic debugging like context objects.
A context object represents a virtual address space. Each processor in the simulated system has a current context, which represents the virtual address space currently visible to code running on the processor. Virtual-address breakpoints are properties of contexts; different context objects have separate sets of virtual breakpoints, and by changing a processor’s current context, you change its set of virtual breakpoints.
The correctness of the simulation does not depend on contexts in any way; the concept of multiple virtual address spaces is useful for understanding the simulated software, but not necessary for running it. What contexts to create and how to use them is entirely your business; Simics does not care.
By default, every processor in a simulation cell use the same default context. You may create new contexts and switch between them at any time. This allows you, for example, to maintain separate debugging symbols and breakpoints for different processes in your target machine. When a context is used in this manner (active when and only when a certain simulated process is active), the context is said to follow the process.
Simics Analyzer will help you coordinate contexts for processes running on the target system.