Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implementing callbacks in sail-riscv for state-changing events #494

Open
wants to merge 2 commits into
base: master
Choose a base branch
from

Conversation

kseniadobrovolskaya
Copy link

@kseniadobrovolskaya kseniadobrovolskaya commented Jun 11, 2024

I have an initial implementation for the suggestion described in #449 . However I still have some questions related to the implementation details and would like to get some advise (if possible).

  1. I have provided a default implementation of notification callbacks for the C language. I use "weak" attributes, so users can override these default implementations. However, it is also necessary to provide this implementation for the Ocaml backend, where as I understand these attributes do not exist. Could someone advise how to resolve this?
  2. In memory callback I'd like to set the changed bytes. But as I understood these bytes are stored in the internal type of sail lbits. Can you tell how I can convert this type to a ordinary type (e.g. const char *) so that users can provide implementation of callbacks without relying on lbits?

Copy link
Collaborator

@Timmmm Timmmm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks along the lines I was thinking, though I think we will still need to have RVFI support in the C emulator, so you still need the flags. It will just need to be implemented via the callbacks.

I probably wouldn't use __attribute__((weak)). If anyone is modifying the callbacks they'll have their own fork of the emulator anyway and can just delete the default ones. That's what we (Codasip) will do.

I'll get you some code to read lbits.

if rv_enable_callbacks then {
csr_update_callback("vtype", vtype.bits);
csr_update_callback("vl", vl);
};
print_reg("CSR vtype <- " ^ BitStr(vtype.bits));
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess you can remove these print_reg() calls too since the csr_update_callback() can do that?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, deleted this function.

@Timmmm
Copy link
Collaborator

Timmmm commented Jun 11, 2024

It's something like this in C++ (adapted from our code, so this exact code isn't tested).

std::vector<uint8_t> readLbits(lbits val) {
    std::vector<uint8_t> data(val.len);

    // We should really double check the length of `val.bits` to ensure it
    // isn't actually greated than `val.len`, otherwise we are at risk of
    // a buffer overflow.
    // I think you can maybe use (mpz_sizeinbase(val.bits, 2) + 7) / 8.

    // Export the lbits. Note this doesn't pad with zeros but that's ok
    // because data is prefilled with zeros.
    mpz_export(
        // Destination buffer.
        data.data(),
        // Where to write the output size (we ignore this).
        nullptr,
        // Endianness of words (-1 = little).
        -1,
        // Size of words in bytes.
        1,
        // Endianness *within* words (0 = native). Doesn't matter in this case.
        0,
        // Number of "nail" bits (upper bits to clear in each word).
        0,
        // The GMP value. Note the sign is ignored but it should always be positive.
        val.bits);

    return data;
}

In crappy C you will have to deal with malloc manually rather than using vector.

@@ -154,6 +154,7 @@ function process_vlsegff (nf, vm, vd, load_width_bytes, rs1, EMUL_pow, num_elem)
if i == 0 then { ext_handle_data_check_error(e); return RETIRE_FAIL }
else {
vl = to_bits(sizeof(xlen), i);
if rv_enable_callbacks then csr_update_callback("vl", vl);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having to add these manually is rather error prone and makes the code less readable. Can we do some fancy sail overloading tricks to automatically call these functions for CSR writes (e.g. something like CSR(vl) = ... ?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Various CSRs have a set_foo() function. We could put it in there. Though that doesn't significantly reduce the risk because people can always forget to use the setter.

I ran into a similar issue recently - trying to memoize/cache a value. Difficult to guarantee that all the code will use setter that updates the cache without some support for private.

IMO there are few enough of these that we can probably ignore this issue for the moment, though I agree it's not ideal.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps it would be better to get Sail to generate the callbacks automatically?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah we discussed this before. Might be nice, but I'm not 100% sure it would do exactly what you want, e.g. you probably want distinct callbacks for writes to sstatus and mstatus, but under the hood they write to the same register. Also there are very few places this callback is needed so the manual burden is not that high, and I suspect Alasdair has enough on his plate already!

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added the ext_notification_write_CSR (and ext_notification_read_CSR) function. It helps to simplify writing to a CSR with a call to a callback.

@Timmmm
Copy link
Collaborator

Timmmm commented Jun 11, 2024

mpz_sizeinbase(val.bits, 2) + 7) / 8

Actually reading this back, I wonder if mpz_sizeinbase(val.bits, 256) would work (and not be really slow).

@kseniadobrovolskaya
Copy link
Author

I would like to point out that the callback functions are annotated as pure. But in reality we have no way to guarantee that users will not actually change the state of the model in these functions. Should we really keep this attribute or should we remove it so that we don't fool anyone?

@Timmmm
Copy link
Collaborator

Timmmm commented Jun 13, 2024

I would like to point out that the callback functions are annotated as pure. But in reality we have no way to guarantee that users will not actually change the state of the model in these functions. Should we really keep this attribute or should we remove it so that we don't fool anyone?

I would say if someone edits the code and changes the callback to be impure then it's up to them to also change the Sail code to mark them as impure.

Copy link
Collaborator

@arichardson arichardson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you keep the RVFI-DII code around? Alternatively, a good demonstrator of the callbacks would be to implement the RVFI-DII socket interface using those callbacks?

@Timmmm
Copy link
Collaborator

Timmmm commented Jun 13, 2024

Yeah to be clear I don't think these callbacks should be user defined. We should have hard-coded callbacks that support RVFI, logging, etc.

@kseniadobrovolskaya kseniadobrovolskaya force-pushed the Callbacks_for_state-changing_events branch 3 times, most recently from 10ae49b to 6e0f187 Compare July 1, 2024 12:55
@kseniadobrovolskaya
Copy link
Author

@arichardson, @Timmmm, what do you think of the current version?

Copy link
Collaborator

@Timmmm Timmmm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah this is pretty much exactly what I was thinking. I put some minor comments but I it looks like the right direction to me.

}
int xreg_update_callback(unsigned reg, uint64_t value) { }
int freg_update_callback(unsigned reg, uint64_t value) { }
int csr_update_callback(const char *reg_name, uint64_t value) { }
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Better to use the CSR index than the name. If this function wants to access the name it can call zcsr_name_map_forwards() or whatever.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, now here's the argument is unsigned reg, and in the sail -- csreg.

@@ -153,25 +153,22 @@ function process_vlsegff (nf, vm, vd, load_width_bytes, rs1, EMUL_pow, num_elem)
Ext_DataAddr_Error(e) => {
if i == 0 then { ext_handle_data_check_error(e); return RETIRE_FAIL }
else {
vl = to_bits(sizeof(xlen), i);
print_reg("CSR vl <- " ^ BitStr(vl));
ext_notification_write_CSR(csr_name_map("vl"), to_bits(sizeof(xlen), i));
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not 100% sure about this change. For now I would probably just replace the print_regs with calls to csr_update_callback. We can do this in a future PR.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. Deleted ext_notification_write_CSR and replaced print_regs with calls to csr_update_callback.

let pc_update_callback value = ()
let xreg_update_callback (reg, value) = ()
let freg_update_callback (reg, value) = ()
let csr_update_callback (reg_name, value) = ()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor naming but I'd replace update with write here. Everywhere else talks about writing registers and memory, not "updating" them.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

$ifdef RVFI_DII
register rv_enable_callbacks : bool = true
$else
register rv_enable_callbacks : bool = false
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you can probably omit this and call them unconditionally. I doubt it will make any noticeable difference to performance and it will simplify the code.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I call them here unconditionally and check rv_enable_callbacks in the callback functions.

@@ -340,8 +340,7 @@ function trap_handler(del_priv : Privilege, intr : bool, c : exc_code, pc : xlen

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a load of implicit CSR writes here that need to be tracked. We do it like this:

function track_trap(p : Privilege, intr : bool, c : exc_code) -> unit = {
  track_writeCSR(csr_name_map("mstatus"));
  match (p) {
    Machine => {
      track_writeCSR(csr_name_map("mcause"));
      track_writeCSR(csr_name_map("mtval"));
      track_writeCSR(csr_name_map("mepc"));
    },
    Supervisor => {
      track_writeCSR(csr_name_map("scause"));
      track_writeCSR(csr_name_map("stval"));
      track_writeCSR(csr_name_map("sepc"));
    },
    User => {
      track_writeCSR(csr_name_map("ucause"));
      track_writeCSR(csr_name_map("utval"));
      track_writeCSR(csr_name_map("uepc"));
    },
  };
}

if rv_enable_callbacks then
match result {
MemValue(_) => mem_update_callback(paddr, width, value, /* is_exception */ false),
MemException(_) => mem_update_callback(paddr, width, value, /* is_exception */ true)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know if it's necessarily better, but we only call out mem_read_callback() function on success MemValue(), and then we have another callback for traps in trap_handler(), where the print_reg()s are.

Maybe keep this (because you can easily ignore the exception case) but also add the trap callback.

You might want to add the exception code to the callback to make logging a bit easier:

      match result {
        MemValue(_) => mem_write_callback(paddr, width, value),
        MemException(e) => mem_exception_callback(paddr, width, num_of_ExceptionType(e)),

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, added mem_exception_callback. But the only thing is, I call it without width since num_of_ExceptionType has type {'n, (0 <= 'n < xlen). int('n)} and width has type {'n, (0 < 'n <= max_mem_access). int('n)}. To simplify the function declaration in the sail. Especially in case of write failure only the memory address is logged and the width is not needed.

@@ -157,6 +157,7 @@ function rF r = {
val wF : forall 'n, 0 <= 'n < 32. (regno('n), flenbits) -> unit
function wF (r, in_v) = {
assert(sys_enable_fdext());
if rv_enable_callbacks then freg_update_callback(regno_to_regidx(r), in_v);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO a better design is to push the enable check down from the caller rather than replicate it everywhere

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, put rv_enable_callback into the riscv_sim.c and checking it in the callbacks.

Copy link
Collaborator

@arichardson arichardson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall I think this approach looks good, but I'd push all the rv_enable_callbacks into the callee to simplify the code. Since the variable is not a compile-time constant it shouldn't matter whether it's checked in the C code or sail code. It might be easier to keep this as an internal flag to the C/Ocaml emulators.

@kseniadobrovolskaya kseniadobrovolskaya force-pushed the Callbacks_for_state-changing_events branch 2 times, most recently from 10348eb to 09acccc Compare July 24, 2024 16:04
1. Added callback functions for XRegs, FRegs, VRegs, PC, CSR, memory writes and CSR, memory reads.
2. Added implementation of callbacks for RVFI records in riscv_rvfi_callbacks.c.
3. Added default callback OCaml-implementations in the file platform.ml.
  Pushed all the rv_enable_callbacks into the callback functions.
@kseniadobrovolskaya kseniadobrovolskaya force-pushed the Callbacks_for_state-changing_events branch from 09acccc to be696b8 Compare July 26, 2024 08:51
/* TODO: will only print bits; should we print in floating point format? */
print_reg("f" ^ dec_str(r) ^ " <- " ^ FRegStr(v));

freg_write_callback(regno_to_regidx(r), in_v);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The default callback needs to print the register otherwise this removes tracing capabilities from the model.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants