Deciphering some RISC-V assembly code - Page 1
Forum section: electronics / general

author: HwAoRrDk
author_rank: Super Contributor
timestamp: November 25, 2023, 02:42:52 am
content: I'm trying to decipher the following RISC-V assembly code:Code:000003d0 <verify_crc>:3d0:cd1ff2efjalt0,a0 <__riscv_save_0>3d4:6405luis0,0x13d6:87aamva5,a03d8:1171addsp,sp,-43da:84aemvs1,a13dc:1471adds0,s0,-4 # ffc <main+0x190>3de:557dlia0,-13e0:/-> 4581lia1,03e2:| c03eswa5,0(sp)3e4:| 147dadds0,s0,-13e6:| 37e9jal3b0 <crc32_update>3e8:| 4782lwa5,0(sp)3ea:\-- f87dbnezs0,3e0 <verify_crc+0x10>3ec:fff54513nota0,a03f0:/----- c399beqza5,3f6 <verify_crc+0x26>3f2:| 0007a023swzero,0(a5)3f6:\--/-X c091beqzs1,3fa <verify_crc+0x2a>3f8:| c088swa0,0(s1)3fa:\-> 00153513seqza0,a03fe:0111addsp,sp,4400:b16djaa <__riscv_restore_0>Further to my discovery inanother threadof a technique whereby I can specify a static constant is assigned to its own custom output section, and then the contents of that section updated with that of an arbitrary external file post-build (using objcopy), I decided to employ this technique on another project where I previously had a 4KB blob included as an array in my C source.I have some code that runs at startup which verifies that this blob's data is not corrupt, by checking a CRC32 checksum. That function's disassembled code is above, and the corresponding C code is as follows:Code:static const uint8_t data __attribute__((aligned(64), section(".my_data"), used, retain));bool verify_crc(uint32_t *crc_expect, uint32_t *crc_calc) {uint32_t crc_data, crc_new;// Read the CRC embedded in the last 4 bytes of the data.crc_data = *(uint32_t *)&data[(sizeof(data) / sizeof(data)) - sizeof(uint32_t)];// Calculate CRC for all of the data except the last 4 bytes.crc_new = crc32_init(); // Actually a macro that resolves to 0xFFFFFFFFfor(size_t i = 0; i < ((sizeof(data) / sizeof(data)) - sizeof(uint32_t)); i++) {crc_new = crc32_update(crc_new, data);}crc_new = crc32_final(crc_new); // Another macro that resolves to arg ^ 0xFFFFFFFF// Output the expected and calculated CRCs if pointers given.if(crc_expect != NULL) *crc_expect = crc_data;if(crc_calc != NULL) *crc_calc = crc_new;return (crc_new == crc_data);}The problem is that I am not sure the assembly code is actually doing what it's supposed to any more after this linker section change. It seems to me via my RISC-V-noob eyes that the assembly code will never calculate the CRC32 correctly, because it appears to just callcrc32_update()with a fixed second argument of zero ("li a1,0") on every iteration of the loop! Is this correct? If this is true, I guess the compiler is assuming that the array is full of zeroes because it doesn't have any initialisation values.How can I fix this? How can I tell the compiler not to assume that thedataarray contains any specific values? I guess I could slapvolatileon the declaration, but that somehow seems wrong to me forstatic const...

author: brucehoult
author_rank: Super Contributor
timestamp: November 25, 2023, 03:16:33 am
content: Code:static const uint8_t data __attribute__((aligned(64), section(".my_data"), used, retain));Thestaticinstructs the compiler that the namedatais not to be exported from this compilation unit, and this a different variable than any variable calleddatain some other compilation unit.Therefore, yes, the compiler knows that it contains all 0s.Nothing at all to do with RISC-V, that's just C.

author: SiliconWizard
author_rank: Super Contributor
timestamp: November 25, 2023, 03:40:38 am
content: Yes, the only way is to qualify it volatile. There is no link between static and volatile, as Bruce explained.There is no clash between const and volatile either, although that one may look less obvious. const only instructs the compiler that your code can't modify the content of the array. volatile instructs it not to assume anything about what could happen to it outside of your code, and thus will emit code that will access it everywhere you explicitely access it, no matter what.

author: golden_labels
author_rank: Super Contributor
timestamp: November 25, 2023, 03:57:51 am
content: To be even more specific: it’s not even “inaccessibility” of the variable, which makes it be “seen” as zeros. It is because that form of a declaration makes a request to fill it with zeros:(1)QuoteThe language has no idea about the linker or any postprocessing you put the program through. The compiler assumes exactly what is being written. And from its perspective it’s written: generate an array of 4096 octets with unsigned 2’s complement representation, set them all to 0, then calculate CRC32 from them.Aside from what SiliconWizard said, you can also make it non-static. After all, it is kind of visible to the outside, if you modify it during linking.(1)9899:2011 6.7.9§10, repeated in working draft of C2x.

author: ejeffrey
author_rank: Super Contributor
timestamp: November 25, 2023, 04:11:46 am
content: What I generally do here is to declare the symbol extern, then define it in an assembly file or just exporting a symbol directly from the linker script. This ensures that C initialization rules do not apply.

author: SiliconWizard
author_rank: Super Contributor
timestamp: November 25, 2023, 05:10:15 am
content: Quote from: ejeffrey on November 25, 2023, 04:11:46 amThat works as well. The difference with 'static volatile' is a matter of style.In your case, you make the object visible everywhere, in the case of 'static volatile', it's only visible to the compilation unit it's declared in.Decide depending on the use case and style.

author: HwAoRrDk
author_rank: Super Contributor
timestamp: November 25, 2023, 05:32:36 am
content: Yes, I know that an un-initialised static variable will be initialised to zero. I had a feeling that if the assembly did indeed show that it was using a fixed value of zero, that's where it came from. I wanted to confirm that my interpretation of the assembly code was correct. I'm not especially familiar with RISC-V assembly code... yet.(And the operands forswbeing backwards from how you expect - with destination last - still throws me for loop every time.)Adding avolatilequalifier seems to solve the problem. I don't think I will make itextern(which is of course, saying this var is defined elsewhere - why that wasn't my first thought I don't know, duh!), because that'll involve messing about with, as suggested, either assembly files or diving into the linker script, neither of which I particularly feel like doing.The compiled assembly code for the function is now:Code:000003d0 <verify_crc>:3d0:cd1ff2efjalt0,a0 <__riscv_save_0>3d4:6689luia3,0x23d6:00068613mva2,a33da:6785luia5,0x13dc:97b2adda5,a5,a23de:6685luia3,0x13e0:ffc7a403lws0,-4(a5) # ffc <main+0x138>3e4:872amva4,a03e6:1151addsp,sp,-123e8:84aemvs1,a13ea:4781lia5,03ec:557dlia0,-13ee:16f1adda3,a3,-4 # ffc <main+0x138>3f0:/-> 00f605b3adda1,a2,a53f4:| 0005c583lbua1,0(a1)3f8:| c436swa3,8(sp)3fa:| c23aswa4,4(sp)3fc:| c03eswa5,0(sp)3fe:| 3f4djal3b0 <crc32_update>400:| 4782lwa5,0(sp)402:| 46a2lwa3,8(sp)404:| 6709luia4,0x2406:| 0785adda5,a5,1408:| 00070613mva2,a440c:| 4712lwa4,4(sp)40e:\-- fed791e3bnea5,a3,3f0 <verify_crc+0x20>412:fff54793nota5,a0416:/-- c311beqza4,41a <verify_crc+0x4a>418:| c300sws0,0(a4)41a:/--\-X c091beqzs1,41e <verify_crc+0x4e>41c:| c09cswa5,0(s1)41e:\----> 40f40533suba0,s0,a5422:00153513seqza0,a0426:0131addsp,sp,12428:b149jaa <__riscv_restore_0>I think it's doing the right thing now. I think that "lui a3,0x2" is loading a value of 0x2000 into a3 ('loading upper' is shifting the immediate value 12 places into the higher bits, right?), which corresponds with the address where the linker map says the new section is located. And then it seems to grab thecrc_datavalue by adding 0x1000 to that and loading from a -4 offset from that address.Thanks all.

author: brucehoult
author_rank: Super Contributor
timestamp: November 25, 2023, 05:52:28 am
content: Quote from: HwAoRrDk on November 25, 2023, 05:32:36 amRISC-V assembly language operand order for load and store is the same as for every other ISA I know of that has "load" and "store" mnemonics rather than "move". This includes but is not limited to all four Arm ISAs, MIPS, SPARC, PowerPC, AVR.Where RISC-V differs from those is that the binary encoding consistently puts instruction "source" register always in the same bit field in the instruction if it exists (it doesn't for load) and "destination" register always in the same bit field (different to the "source" bit field) if it exists (it doesn't for store). Others tend to put the "source" register for store and the "destination" register for load in the same bit field, which complicates the hardware. But this is not visible to the assembly language programmer.

author: golden_labels
author_rank: Super Contributor
timestamp: November 25, 2023, 06:54:27 am
content: In this codevolatileis going to work. My gripe, with using it to solve such problems, are the limitations it adds. Most array-operating functions become useless. The program receives a bunch of restrictions, which neither have anything to do with the original intent nor fulfill any purpose. Less people can maintain the fragment of code, as it can no longer be given to less experienced programmers.I’m not sayingvolatileis smelly or generally wrong. Just that — if other options are available — I would rather go for them.

author: Noloader
author_rank: Newbie
timestamp: November 25, 2023, 09:41:50 am
content: Quote from: golden_labels on November 25, 2023, 06:54:27 amI do a fair amount of work with cryptography and security libraries. Between the language specifications (C and C++) and the compiler behaviors (Clang and GCC), it makes it difficult to implement algorithms properly. Nowadays, I feel like it is a crap shot if we are going to get expected code generated by the compilers. It is getting especially bad when C++ is the language. And it is getting even worse with the linker due to LTO.

author: golden_labels
author_rank: Super Contributor
timestamp: November 27, 2023, 03:00:06 am
content: “I made a discovery today. I found a computer. Wait a second, this is cool. It does what I want it to. If it makes a mistake, it's because I screwed it up. Not because it doesn't like me... Or feels threatened by me.. Or thinks I'm a smart ass.. Or doesn't like teaching and shouldn't be here...”— from “The Conscience of a Hacker” byLoyd BlankenshipCompilers almost never generate wrong code. Bugtrackers of GCC and Clang indicate that there are exceptions, but they are very rare and happen primarily with major new feature sets. Almost exclusively it is the programmer having wrong expectations from the code.With C and C++, as well as any parallelized code, in my experience this is usually caused by failure to understand program’s meaning. Code has meaning attributed to it, which was never in that code. Typically because the programmer never formally learned the tool. Instead the knowledge is replaced with unfounded guesses: by comparison to similar constructs from elsewhere (e.g. basic arithmetic) or incidental behavior observed in the past.This is not limited to the tools mentioned above, but it is most visible there. In C and C++ it seems to me, that it’s caused by three factors playing together:The languages are particularly abstract and detached from hardware, and having very complex model at the same time. This bites hard, if the actual meaning is replaced with guesses. This is also the primary source of security problems attributed to language itself, leading even seemingly experienced programmers (like Linus Torvalds) to madness. The problem is greatly inflated by the lack of easily available sources to study the language, even if you want to devote years of your life to learning just a single tool. In microcontroller world made even worse by vendor’s proprietary tools, which claim to be “flavors of C”, but invent their own meanings.Undefined behavior can be found(1)in any Turing-complete imperative or functional language, but C and C++ are infamous for experiencing it at the most basic level. Explicitly clarifying it in the specs is not really helping, if nobody knows them in the first place.(2)The result is, that even a simple snippet of code may not have any defined behavior. In Java I must at least e.g. usejava.util.HashSet, add something to it and then try to retrieve to get an UB. In C I can simply add two numbers together. Since it happens at a much more basic level, the consequences may also be much more severe.Both languages are very bare and boast high statement-level performance. Which leads to toolchains being focused on optimizing at this level.(3)If you have a lot of code with no well-defined behavior at the basic level and optimization focused at the same area, what you get is a lot of behavior that differs from what the programmer imagined it would be. Unlike the previous two, this is not a shortcoming of the language. It’s just probability conspiring against us. While in other languages poorly constrained code has high chance of matching expectations (even if expectations are wrong) and errors being in general mild in consequences, in C and C++ these chances are particularly low and the consequences are often catastrophic.(1)This is strong “can”, not mere “may”.(2)Even worse, it led to theories representing these clarifications as UB being intentionally added as a feature. From what I observed, usually by misinterpreting the lack of such clarification in other languages’ specs.(3)<snide>Because otherwise the only thing keeping them in business would be the lack of better alternatives and inertia of the industry.</snide>