Weird strcat() behaviour - Page 1
Forum section: electronics / general

author: peter-h
author_rank: Super Contributor
timestamp: July 23, 2024, 09:10:14 pm
content: STM32 32F417 Cube IDE 1.14.1 GCC C v11char buffer;buffer=0;Now buffer contains a 0x00 and then random data.strcat(buffer, "\r\n");this writes just the two chars to buffer, not a \0 as wellstrcat(buffer, "\r\n\0");this does it correctlybutstrcat(buffer, "test-string");does write the test-string and a \0 which is correct.What is the difference?\r\n are control characters... Googling suggests that the resulting string is null terminated only if the strings are "string literals" and \r\n isn't.

author: T3sl4co1l
author_rank: Super Contributor
timestamp: July 23, 2024, 09:52:01 pm
content: https://en.cppreference.com/w/c/language/string_literalEscape codes are treated as characters just like any other. The string"\r\n\0"is the array(char) {0x0d, 0x0a, 0, 0}.The confusion (or bug) lies instrcat(), or the declaration ofbuffer. Check the sectionbufferis placed in: it's most likely.bsswhich is zeroed on startup. You might miss the added zero due to the existing zero padding.If it's in.noinit(or a platform-specific equivalent), or not a global but local in a function or other scope (an automatic variable), it may contain data from prior execution, old stack contents, etc.; in short, the value is undefined.Tim

author: peter-h
author_rank: Super Contributor
timestamp: July 23, 2024, 09:55:26 pm
content: The buffer is local to a function, so on a stack.I have now sorted this specific issue by filling it with zeroes, but I suspect something else is going on...

author: SiliconWizard
author_rank: Super Contributor
timestamp: July 23, 2024, 09:59:56 pm
content: Yes, we'll need to know what exactly you are observing here and in which context. It's 100% unlikely that the effect after the strcat call gets you a string with a non-zero value at index 2. But the optimizer is free to do whatever it likes to get you to that result - keep in mind that most "string" calls (str functions, memcpy, memset, etc) in GCC are inlined when the size is small enough and known in advance. If the compiler knows that the array contains a zero at initialization at index 2, then it could happily only copy the 2 characters. So, what did you observe here?Is the 3rd byte (index 2), in your first test: "strcat(buffer, "\r\n");" non-zero? That would indeed be a severe bug.But if the issue is that the code does explicitely writes only the first two bytes and the third is already zero, then it's "normal" behavior for an optimizing compiler.So, we'll need to know a bit more.

author: Nominal Animal
author_rank: Super Contributor
timestamp: July 23, 2024, 10:00:43 pm
content: The C standards (C23: 7.26.3.1, C17: 7.24.3.1, C11: 7.24.3.1, C99: 7.21.3.1) definestrcat(s1,s2)thus:QuoteThus, it would be a bug in thestrcat()implementation to behave differently forstrcat(buffer,"\r\n")andstrcat(buffer,"\r\n\0"). In C, a string is a character array terminated by the null character\0, and if you examine past that, the parameter is no longer a string (it'd be a character array, or otherwise described as not being an ordinary string in the function definition).The only way I can imagine getting the results you have described ifstrcat()is defined as a macro along the lines of#define strcat(s1, s2) memcpy(s1 + strlen(s1), s2, (sizeof s2) - 1)which is obviously an error, as the terminating null character is not copied. It can be (and often is) implemented asCode:char *strcat(char *restrict s1, const char *restrict s2) {memcpy(s1 + strlen(s1), s2, strlen(s2) + 1);return s1;}although personally I'd prefer to usestrncat()instead and include some checks (especially when the concatenated string does not completely fit ins1, as standardstrncat()does not include the terminating null characrer in that case). In general, I prefer the BSDstrlcat()semantics:Code:char *strlcat(char *s1, const char *s2, const size_t size) {const size_t n1 = strnlen(s1, size);const size_t n2 = strnlen(s2, size);if (n1 >= size) {s1[size - 1] = '\0';} elseif (n1 + n2 > size) {memcpy(s1 + n1, s2, size - n1 - 1);s1[size - 1] = '\0';} elseif (n2 > 0) {memcpy(s1 + n1, s2, n2);s1[n1 + n2] = '\0';}return s1;}

author: IanB
author_rank: Super Contributor
timestamp: July 23, 2024, 10:13:57 pm
content: Quote from: peter-h on July 23, 2024, 09:10:14 pmTry the following:Code:char buffer[] = "abcdefghijklmnopqrstuvwxyz";buffer = 0;strcat(buffer, "\r\n");What are the contents of buffer after this?

author: peter-h
author_rank: Super Contributor
timestamp: July 23, 2024, 10:36:24 pm
content: QuoteI agree but then I would have never spotted itNo; doing strcat \r\n put in just the \r\n (2 bytes) and the next byte was as it was before (\07 in this case).IanB - I will try that tomorrow, in isolation. Going to bed nowSpent all day chasing weird issues.The optimisation is -Og i.e. nowhere near the highest.

author: nctnico
author_rank: Super Contributor
timestamp: July 23, 2024, 11:39:36 pm
content: You are better off using snprintf instead of strcat because snprintf results in a valid string which does not exceed the provided buffer length by definition. I never use strcat (or strncat) because it can result in an unterminated string (or buffer overflow) which can lead to all sorts of bugs.

author: SiliconWizard
author_rank: Super Contributor
timestamp: July 23, 2024, 11:41:09 pm
content: Quote from: peter-h on July 23, 2024, 10:36:24 pmSo I suppose you ran into a bug and tracked it down to this using the debugger.Initializing the buffer with known values, as IanB showed you, is a good start. It will help you see if any part of the buffer gets overwritten - it may come from another part of the code altogether. Since it's on the stack, it may get corrupted by something else, or, if you're sure it isn't, last resort is that you may be at the very bottom of the stack - so, a stack "overflow" - in this particular function.

author: brucehoult
author_rank: Super Contributor
timestamp: July 24, 2024, 01:09:49 am
content: Quote from: peter-h on July 23, 2024, 09:10:14 pm99.999% you are mistaken. 0.001% you have a buggy compiler and/or libc.Please show the generated code.Certainly it looks fine here ...https://godbolt.org/z/WW1TeehMM... as you would expect.Stupid to inline thestrcat(), and still do astrlen()call instead, but whatever.You can clearly see that it copies the '\r\n' withldrh/strh, then the null withldrb/strbCode:.LC0:.ascii "\015\012\000"foo:push {lr}sub sp, sp, #1004movs r3, #0strb r3, mov r0, spbl strlenadd r2, sp, r0movw r3, #:lower16:.LC0movt r3, #:upper16:.LC0ldrh r1,  @ unalignedldrb r3, [r3, #2] @ zero_extendqisi2strh r1, [sp, r0] @ unalignedstrb r3, [r2, #2]ldrb r0, [sp, #2] @ zero_extendqisi2add sp, sp, #1004ldr pc, , #4Interesting that for RISC-V gcc 11 (and current trunk too) dispense with the string constant entirely and just load the 13 and 10 as constants, and use the zero register for the trailing null:Code:li a5,13sb a5,0(a0)li a5,10sb a5,1(a0)sb zero,2(a0)Regardless, both Arm and RISC-V are correctly storing the trailing null.

author: peter-h
author_rank: Super Contributor
timestamp: July 24, 2024, 05:52:00 am
content: QuoteAccording to above, strcat should always append a \0 so if adding e.g. "\r\n" to a buffer, it should reliably search that buffer until it finds a \0 and then replace that \0 with \r\n\0.I am continuing the work and now \r\n is adding the \0 correctly. It's a complete mystery. I just filled the whole buffer with zeroes first. But when I filled it with 'A' it also worked. So it is something else.EDIT:Working one step at a time from a working version, I found that replacingchar appnamestring [] = "appname_1.1";withchar appnamestring [] = {"appname_1.1\0"};makes it work. The 2nd one is a more explicit initialisation. The context is:Code:// Function returns a pointer to the customer application string.#include "appname.ini"char * get_appname(void){return (appnamestring);}and appname.ini containschar appnamestring [] = {"appname_1.1\0"};The string is outside a function so should end up as initialised data which gets copied to RAM at startup. It should probably be "const". But that blows it up.Funny thing is that I have another (rare) case of Cube IDE SWD interface getting buggered-up by C code in the project!

author: brucehoult
author_rank: Super Contributor
timestamp: July 24, 2024, 07:03:40 am
content: Quote from: peter-h on July 24, 2024, 05:52:00 amI don't agree with the above advice because you can get the length argument to snprintf wrong just as easily as you can misallocate the space in the buffer.QuoteIt absolutely should. It is almost inconceivable that you have a compiler/library that doesn't work. The 99.9999% possibility is that you're doing something wrong afterwards.QuoteThat makes no sense whatsoever Neither the braces not the \0.

author: Siwastaja
author_rank: Super Contributor
timestamp: July 24, 2024, 07:27:53 am
content: Hard to believe such bug exist.From your initial minimum example reproducing the problem:Code:char buffer;buffer=0;strcat(buffer, "\r\n");Please try this:Code:char buffer;buffer=0;strcat(buffer, "\r\n");for(int i=0; i<3; i++)printf("%02x ", buffer);printf("\n");and post the result.

author: Nominal Animal
author_rank: Super Contributor
timestamp: July 24, 2024, 07:46:37 am
content: Quote from: peter-h on July 24, 2024, 05:52:00 amAha! I wager you are trying to append to an object in initialized data, without reserving enough room for the modified version.If you are effectively doingstrcat(appnamestring, "\r\n");(say viachar *p = get_appname(); strcat(p, "\r\n");), you are overwriting initialized data following theappnamestringobject!If you haveconst char appnamestring [] = "appname_1.1";then the object is stored in Flash, and if you dochar *p = get_appname(); strcat(p, "\r\n");you are effectively trying to modify Flash memory contents, which will obviously fail.In practice, you should instead create a copy of the string, reserving enough memory for the modified string, and use that:Code:void foo() {const char *name = get_appname();const size_t namelen = (name) ? strlen(name) : 0;// Temporary copy on stackchar modified[namelen + 2 + 1]; // +2 for "\r\n", +1 for final "\0"#if 1if (namelen > 0)memcpy(modified, name, namelen);modified[namelen + 0] = '\r';modified[namelen + 1] = '\n';modified[namelen + 2] = '\0';#elsemodified = '\0';if (namelen > 0)strcat(modified, name);strcat(modified, "\r\n");#endif//// Do something with the 'modified' string//// Since 'modified' was on stack, it will not exist after we return from this function}Here, the temporary copy is on stack. The#if 1 ... #else ... #endifshows the two ways to create the copy and append the CR-LF newline, but since it is just two fixed characters, I recommend using the first.(The alternate method is to use a preprocessor macro containing the application name as a string, and reserve enough space for a suffix:#define APPNAME "appname_1.1"char appnamestring[sizeof (APPNAME) + 4] = APPNAME;which initializes the array to the expected value, including a trailing null character (viasizeofstring-literal), plus reserves space for four more characters. That is, you can dostrcat(appnamestring, "abcd");safely exactly once. Because this permanently changes the application name, I do not recommend doing this.)

author: peter-h
author_rank: Super Contributor
timestamp: July 24, 2024, 08:02:22 am
content: There is certainly something weird going on. I will investigate whether there is a stack space issue in the RTOS tasks. This was already carefully checked but...No; those strings are used read-only.Stepping through the \r\n strcat code it does actually work IF certain other things are not happening elsewhere. It almost looks like an alignment issue but the 32F417 handles unaligned objects. And looking at the .map file, the linker is aligning them anyway.I can recreate the problem reliably just by removing those curly brackets and the \0. I checked in the .map file that the stored string (in RAM) is the same although, as expected, the \0 adds an extra \0 to the end of each one.So, it's gonna be a fun day todayThank you all for your tips.

author: Nominal Animal
author_rank: Super Contributor
timestamp: July 24, 2024, 08:23:09 am
content: Quote from: peter-h on July 24, 2024, 08:02:22 amThen, mind explaining howget_appname()/appnamestring[]modification affects yourstrcat()operation? That is, exactly howbuffer[],get_appname(), andappnamestring[]are used; the entire chain, not just snippets and assumed "sufficiently similar code".

author: Siwastaja
author_rank: Super Contributor
timestamp: July 24, 2024, 08:27:47 am
content: And, important question, are youactuallyhaving a problem, i.e. C code seeing wrong results (lack of terminating zero), or are you just seeing a problem in adebugger?Remember, debuggers are very nasty and require nearly wizard level understanding of everything, otherwise they sometimes produce false positives i.e. make you think you have a problem when you do not.

author: peter-h
author_rank: Super Contributor
timestamp: July 24, 2024, 08:42:53 am
content: Yes, these small changes break a lot of stuff. Some, like removing the curly brackets, produces a working box but ETH and USB are dead. I got to the strcat checking when I started working through the code from the start.It's really complicated because the RTOS needs to be running for any meaningful functionality.Nominal Animal - if I knew the answer then I would have a fix

author: brucehoult
author_rank: Super Contributor
timestamp: July 24, 2024, 08:45:05 am
content: Quote from: peter-h on July 24, 2024, 08:02:22 amThe braces have certainly got zero do do with it, as they will never affect the generated code in any way.What is your evidence that you even have a problem?Please present a COMPLETE code example that fails, sufficient for us to duplicate the problem.

author: nctnico
author_rank: Super Contributor
timestamp: July 24, 2024, 09:03:06 am
content: Quote from: brucehoult on July 24, 2024, 07:03:40 amNo. Sizeof() will give you the correct length for the buffer you provide to snprintf. It is bullet-proof. On top of that, modern GCC compilers will throw a warning if you potentially can put more text into a buffer than will fit.

author: Nominal Animal
author_rank: Super Contributor
timestamp: July 24, 2024, 09:06:48 am
content: Quote from: peter-h on July 24, 2024, 08:42:53 amHaving a Minimal, Complete, Verifiable Example of the problem is the next step.We already know that current compilers generate correct code (and do not replicate the behaviour you have described), and it is unlikely yourstrcat()implementation is buggy (although we could check if we knew the exact version of your compiler and base C library, almost certainly newlib). Thus, the most likely cause of the bug is an interaction.Most likely such an interaction causing the bug is an overflow, i.e. something overflows causing interaction between values of different variables.Just because you allocate an array of 1000 bytes on stack on a microcontroller, it does not mean there was that much room available on the stack; it could overflow into other data sections, and cause all sorts of odd issues. I recommend that after allocatingbuffer[]on stack, you check the value of(intptr_t)((char *)pxStack - buffer)to see how many bytes of stack are used at that point. (Outside of FreeRTOS tasks, one could check(intptr_t)((char *)&_estack - buffer),_estackbeing a symbol whose address is the initial stack pointer, assuming stack grows towards smaller addresses.)

author: Nominal Animal
author_rank: Super Contributor
timestamp: July 24, 2024, 09:22:49 am
content: Quote from: nctnico on July 24, 2024, 09:03:06 amOnly when the destination buffer is an array whose size is known in the current scope. For example,char buffer;int n = snprintf(buffer, sizeof buffer, "%s\r\n", get_appname());if (n >= 0 && (size_t)n < sizeof buffer) {//You have valid contents in buffer}When the destination buffer is a pointer, this no longer works.When such a buffer is passed as a parameter to a function, the function should take the size of the character array as a parameter first, then the character array or a pointer to the first character in the character array, i.e.returntypefunctionname(size_t size, char buffer) {noting that an array function parameter will always decay to a pointer to its first element.If the function is passed a pointer to the buffer and the size of said buffer, sayreturntypefunctionname(char *buffer, size_t size) {then thesnprintf()expression must use the size and notsizeof, i.e.int n = snprintf(buffer, size, "%s\r\n", get_appname());I've seen enough code failing here, especially one-off errors like assumingn == (int)sizeof bufferorn == (int)sizeindicating valid results (it actually indicates truncation, last non-null character being omitted from the result, although buffer will contain a proper string with a terminating null character), that I wouldn't call it bullet-proof.

author: brucehoult
author_rank: Super Contributor
timestamp: July 24, 2024, 10:43:23 am
content: Quote from: nctnico on July 24, 2024, 09:03:06 amIt's not. And that that doesn't work when you're writing string data you're being passed.https://godbolt.org/z/9xb7Yj7KoOK, there's a warning there. You might write 7+ bytes in a 5 byte buffer.Now change the buffer size from 5 to 10 (or even 7). The warning goes away.Now pass "I'm a very jolly message" as the function argument. BOOM.Programming is hard. Think carefully.

author: DiTBho
author_rank: Super Contributor
timestamp: July 24, 2024, 11:03:28 am
content: Safestring is the best way

author: peter-h
author_rank: Super Contributor
timestamp: July 24, 2024, 11:45:47 am
content: This is a piece of a 256 byte bufferand after doingstrcat(buffer, "\r\n");I get thiswhich has \07 instead of \0.Now, unsurprisingly if I prefill the buffer with 0s then the \07 is not there anymore.But even the prefill does not make the general bombing problem go away. So I think my best bet is to try to trace down why strcat is failing to append the \0 (an earlier one in the same function is not failing).The .map file shows strcat at 805892cand this is itbut that is not the code which gets run when strcat gets called above!On a breakpoint on the strcat line I end up herewhich calls strlen and then does some weird stuff I can't work out. Maybe they decided that the \r\n can fit into a register so they are not doing any string copy? Maybe not though because if I make that add-on string much longerstrcat(buffer, "\r\n\0\0\0\0\0");the disassembly looks the same.Maybe strcat is being inlined here? Must be.

author: peter-h
author_rank: Super Contributor
timestamp: July 24, 2024, 12:36:57 pm
content: And as usual somebody has been there before, and as usual (especially for the almost useless ST forum) didn't bother to post his solutionhttps://community.st.com/t5/stm32cubeide-mcus/why-are-calls-to-strcat-not-actually-calling-strcat-and-why-do-i/td-p/158592So, without help with the above asm, I can either globally disable inlining, or globaly #define a replacement strcat().What is weird is that this has not come up before. This project has hundreds of strcats. But maybe not hundreds lending themselves to register replacement. And in the very function I have one which works and below it one which doesn't add the zero.Where is the global inlining disable switch?Unlike the other guy, I am not getting hard faults.BTW I realised that strcat with \r\n0000000 will still only move \r\n\0, so I tried it with strcat(buffer, "\r\naaaaaaaaaaaab"); and get much the same weird code.Now testing -fno-inline-small-functions - does not work on GCC v11, evidently. Testing -fno-inline; also doesn't work.

author: brucehoult
author_rank: Super Contributor
timestamp: July 24, 2024, 01:13:11 pm
content: Quote from: peter-h on July 24, 2024, 12:36:57 pmInlining is not the problem. The generated code is perfectly correct IFF the CPU and memory region supports unaligned accesses. And if not, then you should tell the compiler that in the compiler options.And can people please, for the love of all that is holy, turn on a little optimisation ... I HATE having to wade through things like "mov r3,r0; mov r2,r3; add.w r3, r7, #12".Try this:https://godbolt.org/z/YdTerE3f1Anyway, the code is fine. Both strcat are copying the null.QuoteYou have yet to show any evidence that you have strcat code that is not copying the null. The code at the link you show correctly copies the null, both times.QuoteA thoroughly stupid "solution" that addresses a non problem. (Well, assuming you don't care about code size -- if you care about code size then use -Os)QuoteThen your issue -- which you have not demonstrated exists -- is different.He, apparently has a CPU such as a Cortex-M0 or a Cortex-M33 depending on options chosen that doesn't support unaligned accesses. That problem is solved by telling GCC which CPU you have e.g.https://godbolt.org/z/eGMjxndsjThat makes GCC inline srcat as a strlan followed by memcpy lol.QuoteYes, both inlined code and non-inlined will stop at the first null.

author: peter-h
author_rank: Super Contributor
timestamp: July 24, 2024, 01:26:01 pm
content: OK I get it. The \07 is \0 and '7'So it is working.

author: Nominal Animal
author_rank: Super Contributor
timestamp: July 24, 2024, 01:49:49 pm
content: Even if strcat() was inlined, we can look atgcc/gimple-fold.cc:gimple_fold_builtin_strcat()orgcc/gimple-fold.c:gimple_fold_builtin_strcat()prior to GCC switching from C to C++ orgcc/builtins.c:fold_builtin_strcat()in older GCC versions, to see what code it generates.Givenstrcat(s1,s2)wheres2is a non-empty string literal, it generatesmemcpy(s1+strlen(s1),s2,strlen(s2)+(size_t)1), although this itself is then subject to further optimizations.Thus, it isextremelyunlikely strcat() optimization is the reason.Quote from: peter-h on July 24, 2024, 01:26:01 pmPerhaps. You see, in C,"\7","\07", and"\007"are all one-character strings, or two chars, 0x07 and 0x00. (See C99-C23 6.4.4.4Character constants: octal character constants may use one, two, or three octal digits.)It is impossible to say what escaping scheme the IDE you took the screengrab from uses, because the above fact is an odd quirk not many people are aware of.Usually, the only one-digit escape code supported is\0, with all other octal escape codes consisting of exactly three digits. I personally prefer the hexadecimal character escapes,\xHH, because of that.

author: peter-h
author_rank: Super Contributor
timestamp: July 24, 2024, 01:51:13 pm
content: I checked the hex dump and I was wrong. The \07 is really 0x00 0x37.Let me go back to the comment on the ST forum: "Most Cortex can handle unaligned 16 or 32-bit access, all will fail with LDRD/STRD". Is that relevant here?The two modules which are not running are ETH and USB, both of which use DMA. Those buffers are explicitly aligned though...

author: brucehoult
author_rank: Super Contributor
timestamp: July 24, 2024, 01:59:08 pm
content: Quote from: peter-h on July 24, 2024, 01:51:13 pmNot unless you show us code expanded from strcat that is using LDRD/STRD to copy 8 chars at a time.If I make the strcat's string sufficiently long I see ldmia to load the string, but only str, strh, strb to store it, which are all ok on those "Most Cortex".ALIGNMENT IS NOT YOUR PROBLEM.Nor is inlining.And you STILL have yet to show us any evidence that there is actually anything wrong.

author: Nominal Animal
author_rank: Super Contributor
timestamp: July 24, 2024, 02:16:14 pm
content: Quote from: peter-h on July 24, 2024, 01:51:13 pmSo, no "weird strcat() behaviour" after all, just a misinterpretation of the data displayed in the IDE?Quote from: peter-h on July 24, 2024, 01:51:13 pmI don't think so, because GCC optimizer knows it can only use these when it knows the base address (in a register) is a multiple of 4 (making these instructions safe, never triggering an unaligned access exception on ARMv7-m).This kind of limitation is common. For example, x86 SSE has both aligned (movaps) and unaligned (movups) 128-bit four-float load/store instruction, and depending on the target architecture, GCC must only generate the aligned version when it can reliably infer at compile time that the memory address is sufficiently aligned. Thus, I do believe GCC optimizer gets this kind of thing right, or a lot of code would not compile correctly. You can find this kind of bug discussed in GCC mailing lists, but that is because they're caught relatively early in adding support for new target arch/extension/cpu/mcu.

author: peter-h
author_rank: Super Contributor
timestamp: July 24, 2024, 02:18:03 pm
content: If I had "evidence" I would have fixed it without posting hereThe 1 extra byte makes the difference between major parts of the device not working. They are extremely complex. Buffer alignment (both ETH and USB use DMA) has been checked, and anyway would have come up before.I tried renaming that #include file to .c (which produced duplicate symbol warnings between .c and .o which I was unable to fix) and to .h (which did exactly the same as .ini).It is an alignment issue because by moving appnamestring upwards in address, I can make the problem disappear. But this is very complex to do because the linkfile allocates objects as it finds them, possibly in alpha order.

author: IanB
author_rank: Super Contributor
timestamp: July 24, 2024, 05:47:49 pm
content: Quote from: peter-h on July 24, 2024, 02:18:03 pmThe include file is handled by the pre-processor, which is a purely textual operation and has no knowledge of the code. Therefore the name of the include file should make no difference to the outcome where the pre-processor is concerned. On the other hand, if you rename a file with a .c extension it might be picked up by the build tool, which might attempt to compile it as a project source file. This would certainly cause chaos.Correct alignment of objects in memory is one of the core jobs of the compiler and loader. If you correctly tell your tools the architecture you are compiling for, they should get this right.With the things you are describing in this thread, it suggests you have some deep problems in your code and your project design that you have not understood yet.

author: IanB
author_rank: Super Contributor
timestamp: July 24, 2024, 06:00:51 pm
content: Quote from: peter-h on July 24, 2024, 05:52:00 amWhy not something like this?appname.hCode:#define APPNAME "appname_1.1"appname function:Code:#include "appname.h"const char* get_appname(void){static const char appname[] = APPNAME;return appname;}

author: peter-h
author_rank: Super Contributor
timestamp: July 24, 2024, 06:11:19 pm
content: Indeed; these strings should be static const. In fact one of them is and the other two aren't. Most likely I can make this problem go away by making them const which avoids the data being copied from flash to ram, but I want to find out what exactly is going on here, because it should not be happening.QuoteWell, yes, which is why I am asking all you expertsActually, after many hours, I think it is some subtle linker issue, maybe in the form of a Cube bug related to #including files in the sources. I can blow it up by making those strings longer, but the strange thing is that- both ETH (with LWIP) and USB die at the same point- both use DMA- both are unbelievably complex, although casually setting breakpoints shows everything working as it should, superficiallyThis product has been rock solid for past 3 years plus, although notably the issues are confined to adding strings etc in main.c, which is why this was not previously noticed; relatively huge RTOS applications were developed which are all rock solid, but the linkfile loads main.o first, followed by some specific low level modules.I am suspecting alignment issues caused by an error in the linkfile. This is the relevant part. I am keen to 4-align each module and its data (both data and bss). It looks ok but maybe something is missingCode:/* === Rest of code, loaded at base+32k, starting with a stub and then the real XXX_main() === */.XXX_main_stub :{. = ALIGN(4);_code_base = .;KEEP(*(.XXX_main_stub))*XXX_main_stub.o (.text .text* .rodata .rodata*). = ALIGN(4);} >FLASH_APP/* app code vector table *//* most of the KEEP directives do nothing but this one is necessary becausenothing points to the vector table so it would get optimised away */.isr_vector2 :{. = ALIGN(0x200);KEEP(*(.isr_vector2))*isr_vector2.o (.text .text* .rodata .rodata*). = ALIGN(4);} >FLASH_APP.XXX_main :{. = ALIGN(4);KEEP(*(.XXX_main))*XXX_main.o (.text .text* .rodata .rodata*). = ALIGN(4);} >FLASH_APP/* This is for __libc_init_array() *//* See https://community.st.com/t5/stm32cubeide-mcus/the-mysterious-libc-init-array-in-cube-ide-is-it-only-for-c/td-p/652829 */.preinit_array :{PROVIDE_HIDDEN (__preinit_array_start = .);KEEP (*(.preinit_array*))PROVIDE_HIDDEN (__preinit_array_end = .);} >FLASH_APP.init_array :{PROVIDE_HIDDEN (__init_array_start = .);KEEP (*(SORT(.init_array.*)))KEEP (*(.init_array*))PROVIDE_HIDDEN (__init_array_end = .);} >FLASH_APP.fini_array :{PROVIDE_HIDDEN (__fini_array_start = .);KEEP (*(.fini_array*))KEEP (*(SORT(.fini_array.*)))PROVIDE_HIDDEN (__fini_array_end = .);} >FLASH_APP/* Initialized data sections for non boot block code. These go into RAM. LMA copy is loaded after code. *//* This stuff is copied from FLASH to RAM by C code in the main stub */. = ALIGN(4);/* main.c DATA */.XXX_main_data :{. = ALIGN(4);_s_nonboot_data = .;/* create a global symbol at data start */KEEP(*(.XXX_main))*XXX_main.o (.data .data*)/* .data sections */. = ALIGN(4);} >RAM AT >FLASH_APP/* Remaining DATA */.XXX_other_data :{. = ALIGN(4);*(.data .data*)/* .data sections */. = ALIGN(4);_e_nonboot_data = .;/* define a global symbol at data end */} >RAM AT >FLASH_APP/* used by the main stub C code to initialize data */_si_nonboot_data = LOADADDR(.XXX_main_data);/* Uninitialized data section (BSS) for non block boot code *//* This stuff is zeroed by C code in the main stub */.XXX_main_bss :{. = ALIGN(4);_s_nonboot_bss = .;/* create a global symbol at BSS start */KEEP(*(.XXX_main))*XXX_main.o (.bss .bss* .COMMON .common .common*) /* .bss sections */. = ALIGN(4);} >RAM/* Remaining BSS */.XXX_other_bss :{. = ALIGN(4);*(.bss .bss* .COMMON .common .common*). = ALIGN(4);_e_nonboot_bss = .;/* define a global symbol at BSS end */} >RAM/* This collects all other stuff, which gets loaded into FLASH */.code_constants_etc :{. = ALIGN(4);*(.text) /* .text sections (code) */*(.text*) /* .text* sections (code) */*(.rodata) /* .rodata sections (constants, strings, etc.) */*(.rodata*) /* .rodata* sections (constants, strings, etc.) */*(.glue_7) /* glue arm to thumb code */*(.glue_7t) /* glue thumb to arm code */*(.eh_frame)KEEP (*(.init))KEEP (*(.fini)). = ALIGN(4);_e_code_constants_etc = .; /* define a global symbol at end of code */} >FLASH_APP

author: T3sl4co1l
author_rank: Super Contributor
timestamp: July 24, 2024, 10:24:15 pm
content: Curious about your conception of "rock solid" -- what fuzzing has been done on these systems?If very little, or none, and mostly expected interactions from users/network/etc., that means actually very little!In particular, with so many hardware and software functions interacting, I would be frankly shocked there aren't, for example, timing interactions, race conditions, packet collision and parsing, those sorts of things, going on. Who knows -- these can be wildly hard to find, even given all the tools to do it (it's no accident hackers expect (and normally receive) a modest payoff when discovering and reporting such a bug in mainstream infrastructure!). So it's not like such a thing might be critical, if it exists at all, here -- but just to say, you can't do a minimum of testing, to the average and expected case, and infer total success from that.Tim

author: peter-h
author_rank: Super Contributor
timestamp: July 24, 2024, 10:31:15 pm
content: I've updated my post above.About 10 units have been running, 24/7, doing pretty complex ETH transactions, with MbedTLS, etc. That is as horrible as it gets!This is not a subtle issue. It looks like a load of code has vanished! But it hasn't; it's there and I can trace through it. ETH and USB blow up exactly concurrently, too, suggesting it is something basic. But like I say these are very complex modules which do nothing until there is traffic, and the traffic is incredibly complex. It would not surprise me if some buffer was simply deleted b the linker.

author: ejeffrey
author_rank: Super Contributor
timestamp: July 25, 2024, 12:51:04 am
content: The linker doesn't just delete buffers! Almost certainly if you actually have a problem (which isn't even clear) you just have a bug in your C code. Probably a buffer overflow of some kind. It could be almost anywhere in your code.

author: brucehoult
author_rank: Super Contributor
timestamp: July 25, 2024, 02:52:43 am
content: Quote from: ejeffrey on July 25, 2024, 12:51:04 amAnd the "experts" have been given absolutely nothing to go on except whatever red herring OP has latched onto at that moment. Without being given access to a reproducible test case it's just a huge waste of time.

author: Nominal Animal
author_rank: Super Contributor
timestamp: July 25, 2024, 04:15:05 am
content: Quote from: brucehoult on July 25, 2024, 02:52:43 amI wouldn't be that harsh. True, without a MCVE, we have no way to delve further into the issue. Yet, this thread also shows what can be done without one (elimination of causes by examining the assembly code generated and the sources for the functions used, for example), so at least as educational material to anyone else looking for help with "weird behaviour" in their code, the thread has some utility and value.

author: DiTBho
author_rank: Super Contributor
timestamp: July 25, 2024, 07:42:42 am
content: Quote from: T3sl4co1l on July 24, 2024, 10:24:15 pmas far as I'm concerned, the only solid things are those that emerge from a series of exhaustive verification sessions, done with different techniques, from module-testing, with and without instrumentation, to tests in system integration, and also considering the coverage, that is ' how many if/else branches are actually verified under all condition, and to design all the test-cases during the testbench we go fromnormaltoabnormalstimulus, so out of range and weird inputs, to test how the system reacts and how it is doing with error propagation.... and this is still not enough, but we are satisfied enough to say "good enough"Then there are those who ... leave a system running for 24 hours, perhaps always with the same stimulus, and if it doesn't crash, then it'srock-solid

author: DiTBho
author_rank: Super Contributor
timestamp: July 25, 2024, 07:50:15 am
content: Quote from: brucehoult on July 25, 2024, 02:52:43 amPrecisely.Education, perhaps for the things that can be recommended as an approach.

author: peter-h
author_rank: Super Contributor
timestamp: July 25, 2024, 08:28:07 am
content: You said you blacklisted me, DiTBhoI would post a simple case if I had a simple case to post. The code in this, minus MbedTLS which is not needed to show it, is about 200k.I would put money in the linkfile doing something (posted it above) because it is only when I add stuff to main.c that this happens. Adding code to any other module is fine, which is why this has only just come to light. And the linkfile does main.o first and then a separate section(s) is used for the rest. Main.c has been constant for years - until I decided to put in those two functions to return those strings.I wonder whether the align 4 directives are done right?

author: peter-h
author_rank: Super Contributor
timestamp: July 25, 2024, 02:33:38 pm
content: I am looking for debugging suggestions.I have a system which works perfectly. I am #including a file, appname.ini, containingstatic char appnamestring [] = "appname_1.1"; // max 32 bytesIt is returned by this functionCode:// Function returns a pointer to the customer application string.// This is the customer application name - max 32 byteschar * get_appname(void){#include "appname.ini"return (char*) (appnamestring);}It is read-only so it would be sensible to make it "static const" but that blows up large chunks of the project, namely ETH and USB.I know what "const" does: it leaves the string in flash and does not copy it to ram. So all the workspace locations in RAM shift down by 8 bytes (in this case - I used a shorter string)Doing a diff on two .map files, this is all I see. Everything is there on the duff version. Just the addresses are lower on the duff one (the upper one).The weird thing is that both ETH and USB die at the same time. All buffers that should be aligned are 4-aligned; in fact lots of stuff is 4-aligned (see the linkfile above). This box has been in development (in various guises; starting with the ST DISCO board) for about 8 years. Other issues e.g. DMA cannot access CCM (on the 32F4xx) should be OK by a) design and b) extensive devt over time.I am sure this is something very simple, but ETH and USB cannot be debugged from first principles, with breakpoints, partly due to protocol timeouts and partly due to sheer complexity. ETH, like LWIP, runs as an RTOS task. USB is entirely interrupt-driven and there is an RTOS task which implements a VCP and MSC (the latter with FatFS).Simplistically, the use of "const" breaks a huge amount of stuff. But there is another function which works on a similar string, but longer, which uses "static const", and that works. I also use const all over the place, for R/O strings.Putting debugs in ETH shows no incoming data is seen by this function, which is pretty basic! Data is seen fine otherwiseCode:HAL_StatusTypeDef IF_HAL_ETH_GetReceivedFrame(ETH_HandleTypeDef *heth){uint32_t framelength = 0U;/* Check if segment is not owned by DMA *//* if (((heth->RxDesc->Status & ETH_DMARXDESC_OWN) == (uint32_t)RESET) && ((heth->RxDesc->Status & ETH_DMARXDESC_LS) != (uint32_t)RESET)) *///__DMB();if(((heth->RxDesc->Status & ETH_DMARXDESC_OWN) == (uint32_t)RESET)){/* Check if last segment */if(((heth->RxDesc->Status & ETH_DMARXDESC_LS) != (uint32_t)RESET)){/* increment segment count */(heth->RxFrameInfos).SegCount++;/* Check if last segment is first segment: one segment contains the frame */if ((heth->RxFrameInfos).SegCount == 1U){(heth->RxFrameInfos).FSRxDesc =heth->RxDesc;}heth->RxFrameInfos.LSRxDesc = heth->RxDesc;/* Get the Frame Length of the received packet: substruct 4 bytes of the CRC */framelength = (((heth->RxDesc)->Status & ETH_DMARXDESC_FL) >> ETH_DMARXDESC_FRAMELENGTHSHIFT) - 4U;heth->RxFrameInfos.length = framelength;/* Get the address of the buffer start address */heth->RxFrameInfos.buffer = ((heth->RxFrameInfos).FSRxDesc)->Buffer1Addr;/* point to next descriptor */heth->RxDesc = (ETH_DMADescTypeDef*) ((heth->RxDesc)->Buffer2NextDescAddr);/* Return function status */return HAL_OK;}/* Check if first segment */else if((heth->RxDesc->Status & ETH_DMARXDESC_FS) != (uint32_t)RESET){(heth->RxFrameInfos).FSRxDesc = heth->RxDesc;(heth->RxFrameInfos).LSRxDesc = NULL;(heth->RxFrameInfos).SegCount = 1U;/* Point to next descriptor */heth->RxDesc = (ETH_DMADescTypeDef*) (heth->RxDesc->Buffer2NextDescAddr);}/* Check if intermediate segment */else{(heth->RxFrameInfos).SegCount++;/* Point to next descriptor */heth->RxDesc = (ETH_DMADescTypeDef*) (heth->RxDesc->Buffer2NextDescAddr);}}/* Return function status */return HAL_ERROR;}That suggests that ETH is just broken in some way. I am suspecting DMA...Obviously I will poke about lots more but is this correct GCC format for alignment?Code:__ALIGN_BEGIN ETH_DMADescTypeDef DMARxDscrTab __ALIGN_END;/* Ethernet Rx MA Descriptor */__ALIGN_BEGIN ETH_DMADescTypeDef DMATxDscrTab __ALIGN_END;/* Ethernet Tx DMA Descriptor */__ALIGN_BEGIN uint8_t Rx_Buff __ALIGN_END; /* Ethernet Receive Buffer */__ALIGN_BEGIN uint8_t Tx_Buff __ALIGN_END; /* Ethernet Transmit Buffer */

author: IanB
author_rank: Super Contributor
timestamp: July 25, 2024, 02:44:50 pm
content: You are defining and initializing an object in a header file. This is not something that is normally done. Normally, header files contain shared declarations, but not definitions. If you include such a header file in more than one place it may cause problems.This is not necessarily the cause of your problem, but it is a sign of an unusual code pattern that may be indicative of other anti-patterns in the code.

author: peter-h
author_rank: Super Contributor
timestamp: July 25, 2024, 02:49:46 pm
content: OK; I took that #include out and nothing changes.Also there is another function just below the appname one which does a "static const" with a much bigger string - about 1k) and that's working fine. Been there for years. So it doesn't point to a problem with the compiler disliking an initialised string in an include file.This suggests the DMA buffers etc are indeed 4-alignedA couple more data points:The Build Analyser shows just the expected difference in ram size.Putting that static const string at the end of another very similar file containing another static const string doesn't bomb it, suggesting that any movement in memory addresses is not the problem.I started another thread under Microcontrollers in the hope that I could check some ETH config register bits.

author: ajb
author_rank: Super Contributor
timestamp: July 26, 2024, 03:36:18 pm
content: Quote from: peter-h on July 25, 2024, 02:49:46 pmCorrect, the problem isn't where the string is in memory. The problem is that something is corrupting your memory, but the symptoms depend on what data is getting corrupted, which depends on how the program is linked. A shift of a single byte may cause wildly different behavior, which can have you chasing after all kinds of wild geese for days until you figure out what is actually causing the corruption.Quote from: peter-h on July 25, 2024, 02:33:38 pmChanging the qualification of this object isn't blowing up ETH and USB. It's just causing the linker to put the string somewhere else, and now whatever was corrupting your string is instead corrupting eth/usb data, because that's what the linker put where the string used to be.Fortunately, if you are seeing that string be consistently corrupted a certain way, then you are relatively well positioned to identify the culprit, for example by setting a data watchpoint on the address (or address range) in question. Other kinds of corruption can be much harder to pin down -- like if some uninitialized data is used as an offset for a subsequent access, the behavior would be much more chaotic.

author: peter-h
author_rank: Super Contributor
timestamp: July 26, 2024, 03:51:45 pm
content: I have narrowed it down to a simpler case of USB almost certainly not getting initialised properly. The whole thing is interrupt-driven. Various registers are loaded with constant values and I suspect one or more of these are done incorrectly. The same process would also bugger up ETH but I have sidestepped that since it happens the same with just USB.

author: T3sl4co1l
author_rank: Super Contributor
timestamp: July 27, 2024, 01:42:25 am
content: Might as well cross-link this here too, FYI:https://www.eevblog.com/forum/programming/the-use-of-the-const-qualifier-gcc/Tim