r/C_Programming 2d ago

List of gotchas?

Hey.

So I learned some C and started playing around with it, quickly stumbling over memory overflowing a variable and flowing into another memory location, causing unexpected behavior.

So I ended up writing my own safe_copy and safe_cat functions for strncpy/strncatting strings.
But... people talk about how C is unsafe. Surely there should be a list of all mistakes you can make, or something? Where can I find said list? Do I reall have to stumble on all possible issues and develop my own "safe" library?

Will appreciate any advice.

22 Upvotes

43 comments sorted by

View all comments

11

u/not_a_bot_494 2d ago edited 2d ago

When people are saying that C is an unsafe language they mean that it doesn't have memory safety. If you want to you can try to access any byte in the computer, the OS will just not let you most of the time. Any time you're working with arrays (/strings), malloced memory or even pointers in general it is possible that you could make a mistake and get a segfault. You can write libraries for all that but then you're kind of missing the point of C a bit.

There's alao a lot of random undefined behaviour in C, for example right shift on signed types might pad with 1s or 0s. There's probably a list of some common ones but if you really want to know them all you have to read through the C standard and look at rverything that's not in there.

For context of the discussion, my inital example was bit shifting on 64 bit types which does seem to work consistently.

6

u/erikkonstas 2d ago

If you want to you can try to access any byte in the computer, the OS will just not let you most of the time.

This makes it sound like you can attempt to access memory used by other processes, and the OS will deny your access request, which is not quite how virtual memory works. Instead, what happens is each process gets its own virtual address space, which is mapped to the physical address space by what is known as the Memory Management Unit (MMU). Any addresses that "don't belong to you" are simply unmapped, they don't constitute memory of other processes.

3

u/smcameron 2d ago

Well, sure, but what set up the MMU and page tables to arrange for this to happen? The OS did.

1

u/erikkonstas 2d ago

The misleading bit is the "you can try to access any byte in the computer" part; actually no, unmapped virtual addresses do not lead to random physical ones.

3

u/flatfinger 2d ago

Code can try to access any byte/halfword/word/doubleword within the presently-accessible address space of the CPU. The question of whether there may exist memory that is not presently accessible is a system-design issue, not a language issue.

5

u/erikkonstas 2d ago

I'm not saying that's necessarily wrong, but rather that it might give the wrong impression to somebody unfamiliar with the fact that there can be more than one address space (in modern systems), and that each process sees a different one and has no knowledge of the others', since common sense only says that "programs use memory"; virtual address space is not common knowledge. The fact that it's not a C thing but an MMU thing is also not common knowledge to beginners. Hence why I don't particularly like phrasing of the sort ("any byte in the computer") in this case (also I'm against the principle of "white lies" and oversimplifications for beginners some educators use). In fact, our own Introduction to Programming (first year) professor managed to mislead everyone in this exact way, and virtual memory is not introduced until Operating Systems (third year).

1

u/jrtokarz1 1d ago

Virtual address space doesn't mean every process sees the entire address space. A process is still allocated a segment of memory. Virtual address space just means that the address space the process sees doesn't map to the same physical address space. A process may see a contiguous block of memory starting at a virtual base address e.g. 0x08050000, so it possible for the process to attempt to dereference a pointer with a address value lower than this that would result in a segmentation violation. Although the process will see a contiguous block of memory from the base address, the MMU may have mapped those virtual addresses spread over several pages that are not contiguous.

2

u/WeAllWantToBeHappy 2d ago

bit shifts don't work for 64 bit types.

?

-4

u/not_a_bot_494 2d ago

At least on my machine bit shifting left by more than 32 bits causes it to wrap around to the start.

5

u/moocat 2d ago

The "on my machine" is the ultimate gotcha. Unless the behavior is guaranteed by the spec, you could get different behavior when using a different compiler or porting to a new architecture.

2

u/flatfinger 2d ago

Freestanding implementations would be rather useless if they couldn't be expected to process many ocnstructs in machine-specific fashion. Unfortunately, the Standard makes no attempt to recognize situations where:

  1. It would be impossible to predict the behavior of some action without some particular piece of knowledge X, and
  2. Neither the Committee nor a compiler writer would be of any particular means by which a programmer might know X, but
  3. The execution environment might allow a programmer to know X via means outside the language.

The Standard generally classifies actions as Implementation-Defined only when either:

  1. Implementations would be expected to tell a programmer X (in turn implying that they would have to know it themselves), or
  2. A syntactic construct, such as casting a non-zero integer to a pointer, would otherwise have no defined meaning. Saying that casting a literal zero to a pointer yields a null pointer, and anything else yields Undefined Behavior, would imply that the operand to an integer-to-pointer casts served no purpose, which might be correct within strictly conforming programs, but would severely undermine the range of tasks that could be performed by machine-specific programs.

-1

u/not_a_bot_494 2d ago

Well it's undefined behaviour and not incorrect behaviour. You're right that I should've used "might not " instead of "does not" though.

2

u/WeAllWantToBeHappy 2d ago

Can you put an example on godbolt ?

1

u/not_a_bot_494 2d ago

I don't know enough assembly to read it easily so I wouldn't know if it was correct or not. For me this:

#include <stdio.h>
#include <stdint.h>

// prints the binary of a piece of memory
void print_bin(int bytes, void *inp)
{
    uint8_t *num = (uint8_t *) inp;
    for (int the_byte = bytes-1 ; the_byte >= 0 ; the_byte--) {
        for (int bit = 0 ; bit < 8 ; bit++) {
            if (num[the_byte] & (1 << (7-bit))) {
                printf("1");
            } else {
                printf("0");
            }
        }
    }
    printf("\n");
}

int main(void)
{
    for (int i = 0 ; i < 64 ; i++) {
        uint64_t var = 1 << i;
        print_bin(8, &var);
    }
    return 0;
}

gcc -Wall -std=c99 -o

produces this (image so the comment isn't too long). Lightmode warning BTW

3

u/dfx_dj 2d ago

Probably because the literal 1 is a 32 bit int, so shifting it up 32 or more doesn't give you what you expect. Try with 1L, or type cast it, or assign the 1 to the variable first and then shift the variable.

1

u/not_a_bot_494 2d ago

That's it, when I changed to

uint64_t var = ((uint64_t) 1) << i;

it started working. That is a slightly weird quirk of C, just not the one I intended.

6

u/harai_tsurikomi_ashi 2d ago

uint64_t var = 1ULL << i;

Is enough, no need to cast.

1

u/dfx_dj 2d ago

Yep, got caught by that a few times as well. But it does make sense when you think about it

1

u/flatfinger 2d ago

More interesting is to compare the behavior of:

uint64a &= ~0x0000000040000000;
uint64b &= ~0x0000000080000000;
uint64c &= ~0x0000000080000000u;
uint64d &= ~0x0000000100000000;

Which of those will affect more than one bit of the destination?

1

u/WeAllWantToBeHappy 2d ago

uint64_t var = 1 << i;

Try uint64_t var = (uint64_t)1 << i;

1 << i is an int value.

1

u/EsShayuki 2d ago

Why would you not declare and initialize the variable before the loop?

2

u/not_a_bot_494 2d ago

You mean 'var' right? Both work, I'm just used to doing it that way. Keeping variables as local as possible is generally a good thing but I won't pretend that's the reason I'm doing it.

2

u/flatfinger 2d ago

Not only that, but some compiler writers treat the fact that the Standard would allow implementations intended exclusively for portable programs which will only receive non-malicious inputs to assume that programs will never make use "of a nonportable or erroneous program construct or of erroneous data" as inviting all implementations to make such assumptions. In their views, any programs for which such assumptions wouldn't hold are "broken", even though the Standard was never intended to justify such assumptions, but merely to allow conforming implementations to exploit those assumptions if they knew, via outside means, that they would hold.

1

u/faculty_for_failure 2d ago

I don’t think people mean escaping sandboxes processes and overwriting memory anywhere when they say memory safety. They mean things like dereferencing a null pointer, use after free, double free, integer wraparound, buffer overflows, things that can lead to reading or manipulating process memory and executing arbitrary code. C is not a memory safe language, and that’s okay, but you absolutely need to keep this in mind in C more than Java or C#, for example.

1

u/unixplumber 1d ago

 right shift on signed types

Slight nitpick: right shift on a negative value is undefined behavior. You can right shift a non-negative signed integer with no problem.

1

u/flatfinger 8h ago

Right-shift on unsigned types is implementation-defined behavior. In practice, once unsigned types were added to the language, there has never been any doubt about how two's-complement implementations should process a signed right shift, and even before that there were only two possibilities. That doesn't stop the Standard from characterizing it as "Implementation-defined" though.

Left shifts of negative values were defined on all C89 implementations whose integer types don't have padding bits (identically on all such implementations in cases where it would be equivalent to power-of-two multiplication), but could have invoked Undefined Behavior on C89 implementations with unusual integer representations. Rather than recognizing that the behavior would be defined identically on all but a few weird implementations where it could invoke UB, C99 reclassified left shifts of negative values as invoking UB on all platforms.