4 ms·
Of course all of this is very much undefined behavior in standard C and C++. Some programmers really need to learn that they program the "abstract machine" when
by leni536 5y ago
Of course all of this is very much undefined behavior in standard C and C++. Some programmers really need to learn that they program the "abstract machine" when they write C or C++.
- plorkyeran 5y agoThat is probably the smallest and least interesting of the problems with this. Any method for injecting code into another process is inherently going to be platform-specific and outside the bounds of the C++ abstract machine.
- woodruffw 5y agoI don't think most of this is actually UB. The cast from a function pointer to `BYTE *` is, but the rest is "sound" (AFAICT) from the C abstract machine's perspective. The reason it fails is basically orthogonal to C.
- Kranar 5y agoThe reason it fails is due to UB. There's not much point in saying that most of it isn't UB since the part that is UB is the part that causes the failure.
- woodruffw 5y agoThe cast isn't the part that causes the failure (C says that function pointers don't have to be safely representable within `void *` or any other fundamental pointer type, but they are on both x86(-64) and Itanium[1]). The failure happens because the programmer assumed that the code is "self contained" and position-independent, both of which are concepts outside of the C abstract machine. [1]: Function pointers on Itanium are actually fat, but IIRC most compilers hide this by making the "function pointer" point to some kind of thunk instead.
- Kranar 5y agoThis is the type of reasoning that leads people to write insecure code, by trying to outguess the compiler with implementation details that are entirely inapplicable. It suggests that if the code was self-contained or position independent or satisfied any property whatsoever, that using memcpy would be a perfectly fine way to copy it, that is simply untrue, it would never be safe to use memcpy on pointers to functions under any circumstance. It's irrelevant whether you are on x86, Itanium, or any other platform, undefined behavior WILL result in incorrect and invalid program semantics and can not be relied upon to produce consistent results even within the same execution of the same program. There's literally decades worth of people trying to use undefined behavior in clever ways and ultimately failing and yet here we are...
- woodruffw 5y agoI think you're reading too far into my reasoning. I'm not a huge fan of UB, and I mostly write Rust these days to get away from cheekiness like this. My only point was that, for better or worse, UB is not the culprit in this code. C could have well-defined abstract semantics for copying functions or aliasing function pointers through datatype pointers, and this code would still be platform dependent and would still break on different hosts.
- Kranar 5y agoThis is also untrue, if the standard did mandate that pointers to functions could participate in a memcpy and the behavior was as if the instructions of that function were treated as an array of chars, then C++ compilers would be forced to accommodate that implementation regardless of what platform it ran on, Itanium, x86, even a PDP-11 it wouldn't matter. For example it might mean that implementations must tag or otherwise store a dictionary to keep track of addresses to functions so as to differentiate pointers to functions from pointers to objects and produce whatever appropriate behavior is needed to copy said instructions. An implementation could keep an intermediate representation of any function that can potentially be memcpy'd and then translate that representation at runtime when it participates in a memcpy. Whatever the case may be, if the standard mandates certain behavior then an implementation is required to respect it. While not entirely the same, C++ does do something along these lines in certain situations involving pointers to virtual member functions involved in a diamond inheritance structure. Saying that C++ (the code in the article is not C and the two languages differ about the treatment of pointers to functions) could have well defined semantics to handle this is entirely moot... it's about as relevant as saying that a C++ program could run on top of the JVM with a garbage collector [1] and hence eliminate all types of memory errors. Even if a C++ program ran on a platform that had guaranteed garbage collection the fact would still remain that C++ as a language does not have well defined semantics for what happens to a dangling reference and as such a C++ compiler is free to exploit that to make very strong assumptions about the runtime behavior of the program for the sake of generating efficient code. The fact that this is undefined behavior gives a compiler the freedom to perform optimizations under the assumption that runtime behavior will never engender said behavior. Raymond makes use of this property when he discusses COMDAT folding which is a common optimization to elide multiple copies of the same function or to even produce multiple versions of a single function optimized for different scenarios. From the article: "Even without Profile-Guided Optimization, compile-time optimization may inline some or all of a function, so a single function might have multiple copies in memory, each of which has been optimized for its specific call site." This property has nothing to do with x86, or Itanium or PDP-11, it's a purely logical optimization permissible only because of the various forms of undefined behavior in C++ with respect to the treatment of function pointers. [1] http://nestedvm.ibex.org/ http://nestedvm.ibex.org/
- leni536 5y agoFrom the C abstract machine's perspective it is undefined: 1. To substract the two function pointers after casting to BYTE*. 2. To read the bytes through those pointers. 3. To cast the copied bytes back to function and invoke it. The article only focuses on 3, and only about how it's undefined because the compiler is not required to generate position independent code. But an optimizing compiler in theory can just optimize the whole function away from looking at the very first undefined line. The message is that yes, you need to step out of the standard to do stuff like this, but you may have to consult a lot more about your implementation defined behavior than you originally signed up for.
- 10000truths 5y agoThe standard is pointless when what you’re trying to implement is non-portable or architecture-specific by nature. At that point, your compiler implementation and target architecture are what matters.
- alisonkisk 5y agocompiler-defined is extremely different from undefined.
- 10000truths 5y agoThat's the thing, though. What is considered undefined by the standard is NOT necessarily undefined by the compiler or target architecture. Type punning via union is UB in the standard, but well defined in `gcc`. Signed overflow is UB in the standard, but well defined in `gcc` with `-fwrapv`. Casting a void pointer to a function pointer is UB in the standard, but well defined for pretty much every non-MCU target arch (dlsym relies on it, after all!). Dereferencing a runtime-known NULL pointer is UB in the standard, but is well defined to trigger a segfault in pretty much every arch with an MMU. Etcetera, etcetera, etcetera.
- atq2119 5y ago> Dereferencing a runtime-known NULL pointer is UB in the standard, but is well defined to trigger a segfault in pretty much every arch with an MMU. This is incorrect for C/C++ though. Modern compilers definitely treat null dereferences as UB, with real consequences (e.g. eliminating redundant null pointer checks). The compiler is part of the architecture.
- pjmlp 5y agoThe fun starts when there is a new compiler version in town and that changes. Even Linus got to enjoy that fun.