I find these "hey there is this language called C and you can do some really weird stuff in it!" articles humorous because from the beginning of time there have been people who are fascinated by this aspect of the C language and gave rise to the whole obfuscated C contest thing.
The idea that a symbol references an address and its type is only a convenience in the source code always messes people up. When I've taught C to people there is always that one person who says "What if you cast a string pointer to a function and called it! Huh?" and I explain that is perfectly legal C and has been exploited for years and you can practically see their brain change conceptual planes in mid-air :-)
I like to start with the assembler, then just tell people that high level languages are fancy assemblers. Some of them restrict you to using the designer's favourite macros, some of them allow you to build on highly general tools at approximately the lowest level, but ultimately all computer programming languages are fancy macro assembler syntaxes.
This is an OK mental model until a wholly compliant C or C++ compiler blows it all to smithereens with an optimization exploiting UB.
You gotta write C or C++ for the abstract machine defined by the standards. Thinking about C in terms of some concrete assembly language or architecture can be problematic.
That's a good point, but the long running (although shrinking) gap between what the standard recommends, vs. what you can do with real compilers is one of the reasons that coding in C can be so error-prone.
The thing I'm wondering about with this is why ld by default creates only two LOAD segments, one r-x and one rw-. .text and .rodata are put into the former while .data is put into the latter. This is the only reason this works. So my question is why there isn't a third LOAD segment with access r-- for the read only data? If anything it would reduce the surface of where to find ROP gadgets slightly so couldn't hurt, no?
Now, you'd expect all to be read-only, but with Position Independent Code (PIC) and Position Independent Executables (PIE), which are prerequisite for Address Space Layout Randomization (ASLR), it's not, because that errors array contains pointers to those literal strings, which means those pointers need to be "relocated": the executable or library can't contain the actual pointer to those, since their addess may vary between executions. So the executable/library only contains offsets, and the dynamic linker adjusts those offsets to make them real pointers. Thus that errors array is actually not read-only.
There is an additional ELF segment for those read-only-but-really-write-because-relocated data, GNU_RELRO, which tells the dynamic linker to remove the write bit on those parts of the read-write section where relocations happened.
LLD (the new linker from the LLVM project) actually does exactly this; by default, it creates a separate read-only segment, and you have to pass --no-rosegment explicitly if you don't want this.
Amusingly enough, I ran into some issues because of this; e.g. valgrind's symbolication was failing on LLD-linked binaries unless I used --no-rosegment. I didn't dig into it too much, but it's probably making some bad assumptions about the text section's load address. (LLD places the read-executable segment after the read-only segment, and I think valgrind was assuming that the text section would be part of the first segment.)
That's architecture-specific. On some machines there is indeed a separate "execute-only" segment in the ELF file. But on many architectures (x86) there has historically been no hardware support for that, and for compatibility reasons the standard linker output still maintains the same scheme.
It's not a lack of execute-only segment but of a read-only, no-execute segment I'm talking about. x86 has had support for that for exactly as long as it has supported no-execute, which the second LOAD segment is marked with (i.e. the one that .data goes in).
Platform support is irrelevant, systems not supporting the access flags simply gives you more access. E.g. running a modern Linux on pre-Athlon 64 CPUs will just cause all readable pages to be executable as well.
The compatibility requirement is the result of the historic lack of x86 platform support, though: x86 used not to support r-- permissions, so it put rodata in an r-x segment, so there are likely programs in the wild which accidentally rely on that, so the linker can't now tighten the rodata permissions without breaking some existing set of programs of unknown size. Whether you think that's a good tradeoff depends on your opinion on the size of that set of programs and how heavily you weight 'avoid breaking code that used to work' against 'tighten permissions for security reasons'.
It would be interesting to know if you can ask the linux linker to put rodata in its own r-- segment -- I scanned the docs but didn't see an option for it.
Depends on what you mean, the original page table entries have a R (Read/Write) flag, if not set the page is read-only. What you couldn't do was mark a page non-executable. But nevertheless the second LOAD segment is marked rw- and not rwx, so it would seem that it wasn't deemed a problem in the past having segments with unsupported permissions.
At the time when we got the NX bit it did happen that some programs broke because they expected executable data, but the security benefits were more important.
> It would be interesting to know if you can ask the linux linker to put rodata in its own r-- segment -- I scanned the docs but didn't see an option for it.
You have to write your own linker script, see e.g. [0].
By the r-- syntax I meant specifically 'readable, not writable, not executable' as distinct from rw- 'readable, writable, not executable' or r-x 'readable, not writable, executable'.
On i386 the descriptor cannot be both writable and executable at same time. But in order to support sane semantics for C, typical Unix OS (which for purposes of this discussion includes 32bit Windows) loads CS, DS and SS with different descriptor selectors that nevertheless alias to same range of linear addresses and thus essentially disable most of the MMU's protection logic and rely only on paging. And traditional 32bit i386 page table entries only have two flags: accessible at all (called "present") and writable.
The old a.out format used by BSD had a third, virtual section for uninitialized data. It was allocated at runtime but of course took no space in the object file.
It is different only in the sense that in ELF it is real segment (that even has sane ELF segment header when loaded), but in MZ, most flavours of a.out and some flavours of COFF it is just a single word quantity somewhere in the image header.
It is not really there in the ELF. It exists as a section. But sections are not used at runtime, they only exists for tools like debuggers etc. You can strip them from the binary with e.g. the sstrip[0] tool and it still works fine.
What is used at runtime is the program headers named LOAD, which specifies the segments. Here is an example:
Idx Name Size VMA LMA File off Algn
25 .bss 00000420 0000000000601040 0000000000601040 00001030 2**5
ALLOC
So note that .bss is placed at the end of the second segment (.bss at 0x601040 + 0x420 = 0x601460,
and the second segments ends in memory at 0x600e10 + 0x650 which is also 0x601460).
But note also that the file size is only 0x220, which places the end of the data mapped from the
file at 0x600e10 + 0x220 = 0x601030 which is slightly before .bss starts. So what happens is
that the information about .bss is actually described by setting the memory size of the LOAD
segment bigger than the file size, the dynamic linker will then fill the rest with zeroes.
I stand corrected about real linker behavior :) In fact merging all RW sections into one big segment with .bss at the end makes perfect sense.
My point was essentially that for ELF this is not some kind of kludgeish special case for .bdd, but general feature that any segment can be zero extended to arbitrary size larger than what is contained in the executable image (although on sane platforms it is not useful for anything but .bss)
On the subject of machine code as data, there was an interesting text file released at this year's SIGBOVIK: https://www.cs.cmu.edu/~tom7/abc/paper.txt (you'll need to resize your browser window to read it properly -- for an easier reading experience you can also check out the PDF and video at https://www.cs.cmu.edu/~tom7/abc/).
The program entry point is actually _start (which does some setup and later calls main()) so for even more extreme TA befuddlement, write a program that doesn't even call main()!
This actually would not link, because _start() or something it calls into (depending on implementation of CRT on given platform) would contain unresolved reference to main. (and goven the fact that all this CRT startup code is usually one .o, you cannot just patch out the part that calls main(), you have to replace it completely)
We joked with him about how he needs to make a program that works, but the grading TAs wouldn’t be able to figure out how it works.
Unless you come across a TA like me (I've been one before), who will comment on the fact that using xor+inc or push+pop would be shorter ways to set a register to a small immediate. ;-)
> Since I knew the target system is going to be 64bit Linux
Knowing the target system makes a lot of these things quite a bit easier. I had really good luck in college knowing our graphics teacher was using a 286.
As a TA you pretty much can be a hardass or save time for the rest of the semester and just mark 100.
Another early scenario was declaring main() as void in some embedded systems. I guess there was nothing to return to but it was still odd.
N1256 5.1.2.2.3 p1: If the return type of the main function is a type compatible with int, a return from the initial call to the main function is equivalent to calling the exit function with the value returned by the main function as its argument; reaching the } that terminates the main function returns a value of 0. If the return type is not compatible with int, the termination status returned to the host environment is unspecified.
main() is __cdecl__ or __stdcall__ on the major platforms.
That calling convention on x86 specifies that the return value is red from the EAX register. So, when your main() function exits, the return value is red from EAX, it's that simple.
The compilers may add boilerplate code around the main, in fact main() is rarely the real main() function, but that doesn't change the spec.
The two comments are not incompatible, they are just very different worldviews. One tells you what the standard says (that the termination status is unspecified if main does not return an int), the other tells you what usually happens (you get what happened to be in AX).
And as I've just tested, gcc doesn't return zero termination status if you reach the } at the end of main.
Calling conventions are as much a standard as the C spec.
The main() is called like a regular function, by the system thing that executes programs.
Depending on the compiler and the flags, the main() is not the real entry point of the program. It can add another entry point to do some magic, like setting the return code.
Absolutely not. You can look at a crashing programs and see the return code is a random value. You can also make a program that doesn't return anything and see that it's also a random value, though some compilers on some platforms might initialize that to zero.
MSVC doesn't claim to implement C99, where this rule was added, so it kind of makes sense that it doesn't happen. (generally, if they added C99-stuff then only where it was required for C++)
I love C but this whole thread highlights a lot of the criticism of it. Everyone's right based on some standard or switch, and everyone's wrong for the same reason.
This is not specific to C, but common to all evolving standards. If you want portable code just drive on the middle of the road.
Often this is as easy as coding to an older standard, like C89 with a few selected features from later standards, and adding some compatibility features / problem detection in the build system.
And don't rely on features that are difficult to explain and/or add only questionable value. Just return 0 from main, it's the obvious simple thing to do.
Unless the assignment in question specifies architecture, the TA could (should? I know I would) run it on a 32 bit Windows environment and mark it down for not working.
The general idea is to find a set of bytes which can be interpreted in different (and valid) ways by all the architectures you want to support, and using those differences, jump to architecture-specific code.
Yes, in hosted environment it is UB, unless the implementation specifies otherwise. Relevant quotes from N1256:
1. In this International Standard, "shall" is to be interpreted as a requirement on an implementation or on a program;
2. If a "shall" or "shall not" requirement that appears outside of a constraint is violated, the behavior is undefined.
3. (5.1.2.2) A hosted environment need not be provided, but shall conform to the following specifications if present.
4. (5.1.2.2.1) The function called at program startup is named main. The implementation declares no prototype for this function. It shall be defined with a return type of int and with no parameters:
int main(void) { /* ... */ }
or with two parameters (referred to here as argc and argv, though any names may be used, as they are local to the function in which they are declared):
int main(int argc, char *argv[]) { /* ... */ }
or equivalent; or in some other implementation-defined manner.
5. (J.2 Undefined behavior) - A program in a hosted environment does not define a function named main using one of the specified forms (5.1.2.2.1).
UB comes with high-level languages. "main" is not specified in the high-level language, but by the OS. You write some machine code, give it the name "main" and the OS jumps to that location and starts executing. One should adhere to the "C calling convention" (managing the stack correctly) if that code is expected to behave within the system.
But "main is not a function" does not produce undefined behavior.
> "main" is not specified in the high-level language, but by the OS. You write some machine code, give it the name "main" and the OS jumps to that location
main() is specified by C. The OS as such doesn't know or care about main. Headers in the binary executable specify where the OS should begin execution, and this is rarely in main.
ah, this reminded me of some embeded oses where driver came as precompiled arrayblobs, that are included with ifdef guards set in some central config tool
I find these "hey there is this language called C and you can do some really weird stuff in it!" articles humorous because from the beginning of time there have been people who are fascinated by this aspect of the C language and gave rise to the whole obfuscated C contest thing.
The idea that a symbol references an address and its type is only a convenience in the source code always messes people up. When I've taught C to people there is always that one person who says "What if you cast a string pointer to a function and called it! Huh?" and I explain that is perfectly legal C and has been exploited for years and you can practically see their brain change conceptual planes in mid-air :-)