Author Topic: Pointer confusion in C -language  (Read 25751 times)

0 Members and 1 Guest are viewing this topic.

Offline VekettiTopic starter

  • Regular Contributor
  • *
  • Posts: 188
  • Country: fi
Pointer confusion in C -language
« on: June 20, 2021, 01:56:16 pm »
Dear All,

Please help me to understand why does following work. It is example from page:
https://www.geeksforgeeks.org/convert-floating-point-number-string/

I'm wondering why does pointer "res" work with this syntax. First of all variable "res" is not with * inside the ftoa -function. Then in main function, res is not called with & -prefix.
Code: [Select]
// Converts a floating-point/double number to a string.
void ftoa(float n, char* res, int afterpoint)
{
    // Extract integer part
    int ipart = (int)n;
 
    // Extract floating part
    float fpart = n - (float)ipart;
 
    // convert integer part to string
    int i = intToStr(ipart, res, 0);
 
    // check for display option after point
    if (afterpoint != 0) {
        res[i] = '.'; // add dot
 
        // Get the value of fraction part upto given no.
        // of points after dot. The third parameter
        // is needed to handle cases like 233.007
        fpart = fpart * pow(10, afterpoint);
 
        intToStr((int)fpart, res + i + 1, afterpoint);
    }
}
 
// Driver program to test above function
int main()
{
    char res[20];
    float n = 233.007;
    ftoa(n, res, 4);
    printf("\"%s\"\n", res);
    return 0;
}

To my understanding this should be:
Code: [Select]
// Converts a floating-point/double number to a string.
void ftoa(float n, char *res, int afterpoint)
{
    // Extract integer part
    int ipart = (int)n;
 
    // Extract floating part
    float fpart = n - (float)ipart;
 
    // convert integer part to string
    int i = intToStr(ipart, *res, 0);
 
    // check for display option after point
    if (afterpoint != 0) {
        *res[i] = '.'; // add dot
 
        // Get the value of fraction part upto given no.
        // of points after dot. The third parameter
        // is needed to handle cases like 233.007
        fpart = fpart * pow(10, afterpoint);
 
        intToStr((int)fpart, *res + i + 1, afterpoint);
    }
}
 
// Driver program to test above function
int main()
{
    char res[20];
    float n = 233.007;
    ftoa(n, &res, 4);
    printf("\"%s\"\n", res);
    return 0;
}

Are both examples actually the same, but the first is just some simplified version that compiler also understands? Testing in STM32CubeIDE

Thank you in advance
 

Offline golden_labels

  • Super Contributor
  • ***
  • Posts: 1378
  • Country: pl
Re: Pointer confusion in C -language
« Reply #1 on: June 20, 2021, 02:07:17 pm »
First of all variable "res" is not with * inside the ftoa -function.
Because:
Code: [Select]
a[b]  == *(a + b)Quite literally, including both + and the square brackets operators being commutative.

Then in main function, res is not called with & -prefix.
Because in C arrays ot type T can be implicitly casted to T* pointing to the first element of such array.


More specifically:
In main there is a variable res. That variable is an object consisting of 20 elements of type char.
The ftoa function accepts a pointer to char as its second argument (char*).
Upon invocation, a pointer to the first element of res is taken and passed to that second argument. It is exactly equivalent of:
Code: [Select]
char res[20];
char* ptr = &res[0];
ftoa(…, ptr, …);

This should not be confused with &res, which would have a different type:
Code: [Select]
&res[0] // `res[0]` is `char`, `&res[0]` is `char*`
&res // `res` is a `char[20]`, `&res` is `char(*)[20]`
Due to how compilers are implemented, they may appear numerically equal if inspected and — ignoring compile errors/warnings — using them interchangeably may by pure coincidence seem to “work”. But they are not equal as they have different types. The latter type is also rarely seen in the wild, so don’t worry you don’t the notation now — quite likely you will not see it for the next few years.
« Last Edit: June 20, 2021, 02:27:16 pm by golden_labels »
People imagine AI as T1000. What we got so far is glorified T9.
 

Online Siwastaja

  • Super Contributor
  • ***
  • Posts: 8960
  • Country: fi
Re: Pointer confusion in C -language
« Reply #2 on: June 20, 2021, 02:10:36 pm »
Yes, as explained above, you can access a pointer like it was an array. Accessing something[0] is the same as *something. Then something[1] will be the next element, i.e., the compiler knows the size of the type and goes forward that many bytes.

You get used to it; it's very common to see
uint8_t single_variable;
uint8_t buffer[1024];
some_function(&single_variable);
some_function(buffer);

In such case, the latter would be equivalent to:
some_function(&buffer[0])
« Last Edit: June 20, 2021, 02:13:45 pm by Siwastaja »
 

Offline VekettiTopic starter

  • Regular Contributor
  • *
  • Posts: 188
  • Country: fi
Re: Pointer confusion in C -language
« Reply #3 on: June 20, 2021, 04:03:21 pm »
Thank you. One more thing. I noticed if I had in the main funtion the char array with different name eg:
Code: [Select]
    char notthesamename[20];
    float n = 233.007;
    ftoa(n, notthesamename, 4);
It did still work. Should it, as ftoa -function is still handling char* res?

However if I changed the char array size to 33, it worked for a while and then the MCU started behaving strangely.

Thanks for helping
 

Offline ataradov

  • Super Contributor
  • ***
  • Posts: 11785
  • Country: us
    • Personal site
Re: Pointer confusion in C -language
« Reply #4 on: June 20, 2021, 04:28:50 pm »
It did still work. Should it, as ftoa -function is still handling char* res?
Yes, the function is independent of the calling code.

However if I changed the char array size to 33, it worked for a while and then the MCU started behaving strangely.
How strangely? There is nothing inherently wrong with larger array size.

Your code does the work and exits, so I don't see how working "for a while" is even possible.

The only thing to keep in mind that this array is allocated on the stack, and the stack may overflow. But 33 bytes and this simple program should not cause stack overflow issues.
Alex
 

Online ejeffrey

  • Super Contributor
  • ***
  • Posts: 3946
  • Country: us
Re: Pointer confusion in C -language
« Reply #5 on: June 20, 2021, 04:45:24 pm »
Yes that will still work the same.  There is no relationship between the variable names in the caller and the formal parameter names in the callee.  Sometimes they are the same but often not.

It should also be fine to pass a pointer to a larger character array than is needed, ftoa will just fill as much as it needs.  So I suspect that whatever troubles happen later are unrelated to the ftoa call.
 

Offline SiliconWizard

  • Super Contributor
  • ***
  • Posts: 15479
  • Country: fr
Re: Pointer confusion in C -language
« Reply #6 on: June 20, 2021, 05:14:22 pm »
Just a quick look at the code. We don't know how intToStr() is implemented? So is that certain that ftoa() will zero-terminate the string in all cases? (I guess it should if intToStr() ensures that.)
 

Offline VekettiTopic starter

  • Regular Contributor
  • *
  • Posts: 188
  • Country: fi
Re: Pointer confusion in C -language
« Reply #7 on: June 20, 2021, 06:37:07 pm »
Oh, was just wishing it was pointer issue but I guess it was not then. I tested this with more complex code which had freeRtos and two tasks. The other task basically read ADC and other task manipulated the data and updated display. This was called in that data manipulation task. What I meant with that strange behavior was that the ADC data stopped either completely updating or started receiving zeros. Don’t know yet which way. However it didn’t completely lock the MCU as it is still updating the display. I’m stumbled, how come making the char array size = 33 will mess with the ADC read. Not immediately but after a minute or so running. Maybe some buffer overflow or something which writes to the same memory address where the ADC data is? Array size 20 it works flawlessly.

Thank you all for helping.
 

Offline ataradov

  • Super Contributor
  • ***
  • Posts: 11785
  • Country: us
    • Personal site
Re: Pointer confusion in C -language
« Reply #8 on: June 20, 2021, 06:48:55 pm »
If you have RTOS, then you assign the stack size to each task, so you need to check how big of a stack size you have assigned to the task that makes uses the array. You pass the stack pointer and size to the task creation function.
Alex
 

Offline VekettiTopic starter

  • Regular Contributor
  • *
  • Posts: 188
  • Country: fi
Re: Pointer confusion in C -language
« Reply #9 on: June 20, 2021, 07:58:00 pm »
Wow, that was it! So happy, thank you so much! Task size was:
.stack_size = 128 * 4
Increased it to:
.stack_size = 1024

And now it works perfectly. Never would have thought about it and probably never would have figured that out by myself.
Can I btw. use: .stack_size = sizeof(thread_name)
Can it figure it out by itself?

Sorry, this is already bit off of the pointer topic..
 

Offline ataradov

  • Super Contributor
  • ***
  • Posts: 11785
  • Country: us
    • Personal site
Re: Pointer confusion in C -language
« Reply #10 on: June 20, 2021, 08:10:01 pm »
No, there is no automatic way to figure out required stack size. Recursive functions may want to use unlimited amount of stack, for example.

A typical way to estimate the required stack size is to fill the stack area with a known value (like 0xaa) and then let your program run for a while, then look at the stack area and see what was the last address that is no longer 0xaa.

And you need to keep the stack limitation in mind when making changes. This is something you do  automatically with experience.
Alex
 

Offline SiliconWizard

  • Super Contributor
  • ***
  • Posts: 15479
  • Country: fr
Re: Pointer confusion in C -language
« Reply #11 on: June 22, 2021, 04:31:45 pm »
No, there is no automatic way to figure out required stack size. Recursive functions may want to use unlimited amount of stack, for example.

A typical way to estimate the required stack size is to fill the stack area with a known value (like 0xaa) and then let your program run for a while, then look at the stack area and see what was the last address that is no longer 0xaa.

And you need to keep the stack limitation in mind when making changes. This is something you do  automatically with experience.

Which is giving me an idea that I can try on my RISC-V core. In a typical architecture in which the stack pointer is growing downwards, we could just keep track of the *minimum* value of the stack pointer every time it changes, and store that in a dedicated register (like in a CSR.)

Is there any existing CPU/MCU implementing this kind of thing?
 

Offline ataradov

  • Super Contributor
  • ***
  • Posts: 11785
  • Country: us
    • Personal site
Re: Pointer confusion in C -language
« Reply #12 on: June 22, 2021, 04:58:11 pm »
Why would you implement it in the hardware when it can be trivially be implemented in the software?

The only thing that makes some sense to do in the hardware is to set the stack limit and have an exception when stack overflow is about to happen.
Alex
 

Offline SiliconWizard

  • Super Contributor
  • ***
  • Posts: 15479
  • Country: fr
Re: Pointer confusion in C -language
« Reply #13 on: June 22, 2021, 05:42:32 pm »
Why would you implement it in the hardware when it can be trivially be implemented in the software?

Maybe because the method you mentioned above:
* Requires a modified startup code. Not necessarily a big problem, but it's not completely "non-invasive".
* Is not fully reliable: although the probability of data written to the stack being equal to your magic values is likely pretty low in general, it's not completely guaranteed. For an approximate indicator, it's probably OK. For something exact, a bit less so.
* Would have too much overhead for tracing stack size during programming execution, something that can be interesting. Your method is OK for manual analysis after program execution, but not really for "real-time" tracing. There could be a lot of interesting uses for this IMO.

The only thing that makes some sense to do in the hardware is to set the stack limit and have an exception when stack overflow is about to happen.

That's something else, and complementary indeed. That's an interesting feature to have, but doesn't give you the same kind of information. Also, on implementations that make use of several stacks,the stack limit has of course to be strictly updated every time the stack is switched. Obvious, but could be a source of additional bugs. Anyway, this feature is a protection feature, whereas the above one is a tracing feature. Pretty different.

 

Offline ataradov

  • Super Contributor
  • ***
  • Posts: 11785
  • Country: us
    • Personal site
Re: Pointer confusion in C -language
« Reply #14 on: June 22, 2021, 07:13:12 pm »
I have never seen a case where I need to trace stack usage to the byte in real time. I don't know of any other implementations that do this, probably because it is not really needed.

You don't need to modify startup code itself, you can fill the top of the stack from the main code. In fact that's what many RTOSes do for the "stack guard" feature. They put a magic word on top of the task stack, and periodically check for that guard value when switching tasks. If the guard value is wrong, then the stack has overflown.

And in this case we are discussing the RTOS use case, so stack pointer would be jumping all the time, so your hardware thing would have to be resettable for each task too.
Alex
 

Offline SiliconWizard

  • Super Contributor
  • ***
  • Posts: 15479
  • Country: fr
Re: Pointer confusion in C -language
« Reply #15 on: June 22, 2021, 09:55:04 pm »
Yep. And yep for having to save and restore this special register upon stack switch as well.

Thing is, you are describing what is being done with existing hardware. But if there was a specific register, as I suggested, to track the stack pointer, it could be used instead, in a much simpler way with less overhead than checking for guard values (and the fact guard values are not 100% foolproof). If it was implemented, why not use it?

It frankly doesn't look like much in terms of hardware. Sure you wouldn't add this on very small cores, but for anything moderately complex, it would be pretty negligible.

The fact this kind of thing hasn't been implemented probably has a number of reasons. One is, as you mentioned, it can more or less be emulated in pure software (but as all software solutions, they are bound to add some overhead AND not be as robust as a pure hardware implementation). Another is, IMHO, that actually very little has been done for stack protection/monitoring if you compare what has been done with all the disastrous stack overflow issues that have plagued software in the last decades. Now on systems that embed a MMU, stack protection can be implemented with the MMU (at least stack overflow/underflow). But there is relatively little that has been done in terms of stack monitoring. I know you mentioned software approaches, but I again think a pure hardware approach would be more robust and have less overhead.

Regarding the usefulness of stack monitoring during program execution, I do think it could give interesting insights. Actually most of the time, we just set up stacks, try to implement reasonable code, and hope for the best. We may implement stack protection when it's available, through periodic checks or exceptions. That would trigger when things have gone bad.

But we often have absolutely no clue how the stack is really being used during program execution. I'm sure it could be interesting, and I bet we would often not expect what we see. I've always found using stacks a bit like shooting in the dark. Just a thought.

 

Offline brucehoult

  • Super Contributor
  • ***
  • Posts: 4566
  • Country: nz
Re: Pointer confusion in C -language
« Reply #16 on: June 23, 2021, 03:08:46 am »
Yep. And yep for having to save and restore this special register upon stack switch as well.

Thing is, you are describing what is being done with existing hardware. But if there was a specific register, as I suggested, to track the stack pointer, it could be used instead, in a much simpler way with less overhead than checking for guard values (and the fact guard values are not 100% foolproof). If it was implemented, why not use it?

It frankly doesn't look like much in terms of hardware. Sure you wouldn't add this on very small cores, but for anything moderately complex, it would be pretty negligible.

The fact this kind of thing hasn't been implemented probably has a number of reasons. One is, as you mentioned, it can more or less be emulated in pure software (but as all software solutions, they are bound to add some overhead AND not be as robust as a pure hardware implementation). Another is, IMHO, that actually very little has been done for stack protection/monitoring if you compare what has been done with all the disastrous stack overflow issues that have plagued software in the last decades. Now on systems that embed a MMU, stack protection can be implemented with the MMU (at least stack overflow/underflow). But there is relatively little that has been done in terms of stack monitoring. I know you mentioned software approaches, but I again think a pure hardware approach would be more robust and have less overhead.

Even the smallest commercial RISC-V CPU cores usually implement PMP (Physical Memory Protection) with typically 8 or 16 memory regions which can each be assigned Read/Write/eXecute protections for each of Machine/Supervisor (if it exists)/User modes. This is separate to and much lighter weight than MMU.

On big machines MMU is typically managed by Supervisor-mode software while PMP is used by Machine mode/Hypervisor to stop Supervisor mode OS software from poking around in hardware or other OS areas.

On small machines with an RTOS PMP can be used to protect threads from each other. Of course it needs to be swapped out on task switch, but it's less state than the integer registers (let alone FP) so no big deal, especially if only two or three regions are changed. If every thread uses the same PMP configuration register for its stack region (which of course the RTOS probably would do) and stacks are constrained to be a naturally power of 2 in size (different size and address for each thread) then you need to update a single 32 bit CSR to swap between stack regions.

FE310-G002 and later have PMP (and User mode). So do GD32V and K210.
 

Offline ataradov

  • Super Contributor
  • ***
  • Posts: 11785
  • Country: us
    • Personal site
Re: Pointer confusion in C -language
« Reply #17 on: June 23, 2021, 03:38:42 am »
You can do the same with MPU on ARM. Swapping all that state sucks. And realistically not worth it, especially given that stack overflow results in unrecoverable error anyway. Simple software check is sufficient in most cases.
Alex
 

Offline brucehoult

  • Super Contributor
  • ***
  • Posts: 4566
  • Country: nz
Re: Pointer confusion in C -language
« Reply #18 on: June 23, 2021, 03:55:07 am »
You can do the same with MPU on ARM. Swapping all that state sucks. And realistically not worth it, especially given that stack overflow results in unrecoverable error anyway. Simple software check is sufficient in most cases.

Only if the typical thread runs only a couple of function calls between task switches! In which case you're being killed by register save/restore anyway and would be better off using continuations not threads.

If you dedicate a register to stack limit (which is not really affordable on 32 bit ARM) then you need two fast instructions in every function to check it. If the stack limit has to be loaded from memory then that's at least a slow load instruction as well. It's significant code bloat as well.
 

Offline brucehoult

  • Super Contributor
  • ***
  • Posts: 4566
  • Country: nz
Re: Pointer confusion in C -language
« Reply #19 on: June 23, 2021, 03:57:39 am »
First of all variable "res" is not with * inside the ftoa -function.
Because:
Code: [Select]
a[b]  == *(a + b)Quite literally, including both + and the square brackets operators being commutative.

Even more than that (which maybe you know as you mentioned both being commutative, but the OP won't):

Code: [Select]
a[b]  == *(a + b) == *(b + a) == b[a]
 

Offline Nusa

  • Super Contributor
  • ***
  • Posts: 2418
  • Country: us
Re: Pointer confusion in C -language
« Reply #20 on: June 23, 2021, 08:07:47 am »
As for declarations, these are all equivalent:
char *res;
char * res;
char* res;
char*res;

You'll see the first three if you see enough code, the last one not so much. The majority of spaces in C are there for human readability and style purposes, not because they're required.
 

Offline golden_labels

  • Super Contributor
  • ***
  • Posts: 1378
  • Country: pl
Re: Pointer confusion in C -language
« Reply #21 on: June 23, 2021, 09:40:32 am »
Even more than that (which maybe you know as you mentioned both being commutative, but the OP won't):
Indeed I do and initially I entered that code, but later removed it to not confuse OP. :)
People imagine AI as T1000. What we got so far is glorified T9.
 

Online Nominal Animal

  • Super Contributor
  • ***
  • Posts: 6998
  • Country: fi
    • My home page and email address
Re: Pointer confusion in C -language
« Reply #22 on: June 23, 2021, 01:51:36 pm »
Which is giving me an idea that I can try on my RISC-V core. In a typical architecture in which the stack pointer is growing downwards, we could just keep track of the *minimum* value of the stack pointer every time it changes, and store that in a dedicated register (like in a CSR.)
If you expand that idea, so that every access to memory is verified in hardware to be within an allowed range or an interrupt is triggered, you'll end up with a segmented memory model with linear address space.

Due to experimenting with named address spaces in C and C++ (no, they are definitely not standardized; just an extension implemented by GCC for C and clang for C and C++), I've been surprised to find how extremely useful these are when they do have hardware support.  (Even on x86-64, there are essentially four independent address spaces: the "default" one, the stack (accessed via the SS segment), and FS and GS segments.  On many AVRs, being based on Harvard architecture, there are two: code ("progmem"), and data.)

Named address space support as implemented by gcc and clang use the type of a pointer variable to indicate its address space; and this can be overridden/cast via explicit expressions.  The named address space in the type specification does affect overloading and templates (C++) and type generics (C _Generic), so with a workable software engineering mindset, these Just Work.

It seems to me OP has the misguided understanding that types and type specifications are just specifications for how the underlying hardware memory access is done; and that the confusion stems from this fundamental misconception.  (You could, and I'm sure someone will, claim that "You're wrong! That's exactly what types are!", based on the fact that compilers and interpreters use the type to perform the correct memory access; to that, I retort that just because a human can be eaten, does not make humans "food".)

In a much more fundamental sense, type specifies all the known information about the variable or memory access expression at that point, that the value of the variable or expression does not.

(Not realizing this, and being fixated on their own definitions, is why the GCC C++ front-end developers and C++ standard committee claims that named address space support is impossible/"too hard" to implement for C++; and inversely, having realized the above, the Clang/LLVM C++ developers just went and implemented it because it was needed for OpenCL anyway.  I'm very tempted to draw a parallel to an universal list-based structural markup format we could replace XML with, and both significantly increase the parsing speed and minimizing RAM usage (especially important for IoT) while allowing basically a complexity explosion in the options for specifying structured metadata associated with each node; and how getting it accepted or standardized without first implementing actual real-world examples handily beating their current XML/XML-derivative "competitors" –– and even afterwards! –– is basically impossible because most human minds are set in their ways.  Smart people can be scarily stupid, you see.)

In C, the exact meaning of the asterisk (*) is heavily dependent on the context.  It can be the binary multiplication operator, the unary dereferencing operator, or a pointer type specifier.  Similarly, the ampersand (&) can be the binary 'and' operator, or the unary address-of operator.  So, one of the first skills one needs to cultivate, is the skill of recognizing what context to apply to any statement, lexical sequence, or expression.

For type specifications – for example, when specifying what kind of parameters a function takes –, the asterisk-as-pointer-type-specifier is particularly simple: it reads as "is a pointer to".  Type specifications themselves are split at asterisks into type specifier sets.  The order within a type specifier set is irrelevant: const volatile int, volatile const int, int volatile const, volatile int const, and so on, are all equal; although many humans prefer a specific order to keep things familiar.  The order of the sets, however, is important: they are read from right to left.  A final wrinkle is that if the type specification names a variable, any const or volatile preceding the variable name without an asterisk in between, means those are associated with the variable and not the type.

For example, const volatile int *const x;says, quite literally, "x is a const pointer to a const volatile int".  The first const in the English statement, corresponding to the rightmost const in the C expression, is a promise to the compiler that the code does not try to change the value of variable x.  The other const, the leftmost const in the C expression, is a promise to the compiler that the code does not try to change the value of whatever this pointer refers to.  The volatile tells the compiler that although this code won't try to change that value, other code may, and therefore the compiler should not try and cache the value.  Aside from the asterisk * that tells us the declared variable x is a pointer, the only thing left is the int: it specifies the type of the object that variable x points to, is an int.

The same simple logic applies to all type specifications.  For function pointers, the fact that the pointer variable name is in the middle with its parameter specification in parentheses to the right does make things harder to read, but the rules stay the same.

Type specifications can also appear in cast expressions.  In C, a cast is an operation that affects the type of a variable or an expression.  These are common in "accessor" functions: small functions whose purpose is to make code more maintainable and easier to read.  For example, if you have a binary communications protocol, you might wish to have an accessor function that can convert four consecutive bytes in a specific byte order ("endianness") to a 32-bit signed integer:
Code: [Select]
static inline int32_t  get_s32_le(const void *ref)
{
    const unsigned char *const buf = ref;  /* = (const unsigned char *const)ref */
    return (int32_t)(  ((uint32_t)buf[0])
                    | (((uint32_t)buf[1]) << 8)
                    | (((uint32_t)buf[2]) << 16)
                    | (((uint32_t)buf[3]) << 24));
}
However, some "programmers" think they need to write clever code minimizing the length to show how "good" they are, so they may instead write the above as a macro
Code: [Select]
#define  S32_LE(ptr)  ((int32_t)( *((const unsigned char *)(ptr)+0) \
                                | ((uint32_t)(*((const unsigned char *)(ptr)+1)) << 8) \
                                | ((uint32_t)(*((const unsigned char *)(ptr)+2)) << 16) \
                                | ((uint32_t)(*((const unsigned char *)(ptr)+3)) << 16) ))
or even as
Code: [Select]
#define  S32_LE(ptr)  ({ const unsigned char *_p = (ptr); (int32_t)( p[0] | (((uint32_t)p[1]) << 8) | (((uint32_t)p[2]) << 16) | (((uint32_t)p[3]) << 24) );})and congratulate themselves for having "optimized" the heck out of this operation, without realizing that using either GCC or Clang with typical/recommended optimizations enabled (-O2), in the final binaries, all three end up generating the same machine code.  Yet, two of the three are quite unreadable (thus likely sources of bugs; can you find the one that I might have inserted there on purpose?).

True, the static inline function does have unnecessary code – it's verbose like me; but not for verbositys sake, only to try and convey the underlying concepts and ideas in as useful form as possible – like the cast of the first byte to 32-bit unsigned integer.  However, since they generate no extra code, whether one should keep or drop them, depends on which form one believes is the most efficiently maintainable one: which form is easiest to maintain in the long term, keeping the probability of a bug (being accidentally introduced and/or left unnoticed in this code) as low as possible.

If the couple of us still left reading this novel of a post circle back to the original question at hand, using what we learned just above (with the named address spaces being just a spice on top to remind us how oddly useful and strange types can be), we'll find we have the following:
  • We have functions void ftoa(float v, char *p, int n); and int intToStr(int v, char *p, int n);
  • We have char res[20]; and float n;
  • We call ftoa(n, res, 4);
  • Within function ftoa(), we have int i = intToStr( (int)v, p, 0);.
A detail in C not yet discussed is that the name of an array variable "decays" to a pointer to its first element.
That is, in a very real sense, res is an array of 20 chars, but when we use res in an expression (that does not just specify a type, so excluding for example expressions like sizeof res, which evaluates to 20 and not to sizeof (char *)), it behaves like it was declared as char *res;.
Indeed, (res + 1) == &(res[1]) is true.

This means that when main() calls ftoa(n, res, 4), res decays to a pointer to the first element in an array of 20 chars.  The definition of the function ftoa() says the second parameter is a pointer to char, so this is absolutely fine.

To convert the integer part of nto i chars, ftoa() calls the equivalent of int i = intToStr( (int)v, p, 0); .  Why is the second parameter just p and not &p?  Because p is a pointer to char, that's why.  &p would pass a parameter of type char **: a pointer to a pointer to a char.

Because of pointer arithmetic and array variables decaying to pointers to their first element in C, there is no distinction in C between pointers pointing to a single element, and pointers pointing to several consecutive elements.  Put simply, there is no reason to expect p above to point to a single character.  If there is no separate parameter specifying how many there are, we just do not know.  If the code overflows the buffer, then we can just say "ouch, that wasn't what I intended", and then fix it.

It would be much better to use something like the following function signature here:
    char *float2str(char *buffer, size_t buflen, float value, int decimals);
The first parameter is a pointer to the array of characters used to store the resulting string.  (A string in C is just an array of chars with a terminating nul byte, '\0', at end.)  The second parameter is the number of chars in that array.  Because we have the wonderful sizeof operator (and in C, sizeof (char) == 1 always), this includes the string-terminating nul byte at the end. The third parameter is the value to be converted, and the fourth is the number of decimal digits desired.  The function returns a pointer to the first character of the string describing the value as a decimal number, stored somewhere within the specified buffer, but not necessarily at the beginning of the buffer.
If there is a problem, for example the buffer is too small for the number of decimals desired, the function can return a NULL pointer indicating the conversion was impossible.  (For embedded code, I would use a dedicated "error" pointer, one that always points to a nul byte, however.  That way checking for errors would be optional, but barring implementation bugs, it would Always Just Work.)

The trick to implementing such a function efficiently, is to start at the decimal point.  The integral and fractional parts are constructed separately; it does not really matter which one first.  Operate on magnitudes only (absolute values); and if the original value was negative, prepend a '-' just before the most significant decimal digit.  The integral part advances left via repeated division (of the integral part only) by ten, and the fractional part right via repeated multiplication (of the fractional part only) by ten.  The same approach works fine for all fixed-point formats also, and you only need integer division-by-ten-with-remainder (the remainder corresponding to the digit at that position), and fractional multiplication by ten (followed by extracting the integral part of the result as the digit, with rounding ever applied to the last presented digit.

If anyone is interested, I can post an example implementation; however, I'd prefer if OP tried their hand at it first.  Not only is it interesting – implemented this way the function is much faster than e.g. snprintf() in hosted C environments, while still yielding the exact same string for all finite floats/fixed-point numbers – so there is real motivation to implement and test one of their own, but everything I blabbed about above about learning the proper context, is easier to learn in practice.  You'll find that whenever you read or write code that specifies a type, you automatically think in terms of "type specification context and syntax".
The English equivalent is that lead and lead do not rhyme.
« Last Edit: June 23, 2021, 01:58:00 pm by Nominal Animal »
 

Offline SiliconWizard

  • Super Contributor
  • ***
  • Posts: 15479
  • Country: fr
Re: Pointer confusion in C -language
« Reply #23 on: June 23, 2021, 04:24:25 pm »
Which is giving me an idea that I can try on my RISC-V core. In a typical architecture in which the stack pointer is growing downwards, we could just keep track of the *minimum* value of the stack pointer every time it changes, and store that in a dedicated register (like in a CSR.)
If you expand that idea, so that every access to memory is verified in hardware to be within an allowed range or an interrupt is triggered, you'll end up with a segmented memory model with linear address space.

Yes, but to you and those who mentioned something similar: keep in mind what I said above: what you describe is memory protection. What I suggested was for code instrumentation (with stacks in mind here, but I do have a knack for code instrumentation anyway.) Those are two different things.

(Just a thought about memory protection for stacks - but we had a long discussion about stacks a while ago, so it's probably already all there: you can define a memory area for a stack and protect it, but there's little way you can prevent some program to write outside of the stack area, when it means to write inside of it, if this 'outside' location is itself another memory area that is allowed to be written to. Sure you can leave some space between the stack and any other memory area to mitigate this, but this is assuming any access to the stack would be strictly monotonous, meaning if the program ever had a stack overflow, it would necessarily overflow *right* after the allowed stack area. This certainly can't catch *all* faulty accesses to the stack, but again, let's refer to the other thread, because I think we discussed this, and I'm already sorry to have gotten a bit off-topic here.)

There's a number of stack analysis tools on the market. Some are purely static analysis tools (and a few commercial ones are pretty expensive). Some are dynamic tools. The dynamic approaches usually consists in adding instrumentation code. Filling stacks with magic values to estimate how much stack was used, as someone mentioned above, is a classic. I'm just suggesting something implemented in hardware. Now of course, something like this would work only for languages sticking to the standard ABI.

A word on static analysis tools: GCC can give you the stack usage of each defined function with the '-fstack-usage'. Although it can be interesting, it obviously doesn't tell you much about total stack usage as it doesn't perform call tree analysis combined with this, so you don't have an estimated total stack usage. I haven't found any open-source tools that can do this, but if anyone knows of one, please share.
« Last Edit: June 23, 2021, 04:33:05 pm by SiliconWizard »
 

Offline DiTBho

  • Super Contributor
  • ***
  • Posts: 4247
  • Country: gb
Re: Pointer confusion in C -language
« Reply #24 on: June 23, 2021, 04:27:23 pm »
Even the smallest commercial RISC-V CPU cores usually implement PMP (Physical Memory Protection) with typically 8 or 16 memory regions which can each be assigned Read/Write/eXecute protections for each of Machine/Supervisor (if it exists)/User modes. This is separate to and much lighter weight than MMU.

Hey? That's awesome! I had to pay extra money to buy MIPS32 commercial cores with PMP implemented. I really like to hear it's a default feature with RISC-V commercial cores :D
The opposite of courage is not cowardice, it is conformity. Even a dead fish can go with the flow
 


Share me

Digg  Facebook  SlashDot  Delicious  Technorati  Twitter  Google  Yahoo
Smf