C standards and committees (was Re: strangest systems I've sent email from)

Sean Conner spc at conman.org
Fri May 20 20:19:32 CDT 2016


It was thus said that the Great Swift Griggs once stated:
> On Fri, 20 May 2016, Sean Conner wrote:
> > By the late 80s, C was available on many different systems and was not 
> > yet standardized.
> 
> There were lots of standards, but folks typically gravitated toward K&R or 
> ANSI at the time. Though I was a pre-teen, I was a coder at the time.  
> Those are pretty raw and primitive compared to C99 or C11, but still quite 
> helpful, for me at least. Most of the other "standards" were pretty much a 
> velvet glove around vendor-based "standards", IMHO.

  In 1988, C had yet to be standardized.

  In 1989, ANSI released the first C standard, commonly called ANSI C or
C89.  

  I stared C programming in 1990, so I started out with ANSI C pretty much
from the start.  I found I prefer ANSI-C over K&R (pre-ANSI C), because the
compiler can catch more errors.

> > The standards committee was convened in an attempt to make sense of all 
> > the various C implementations and bring some form of sanity to the 
> > market.
> 
> I'm pretty negative on committees, in general. However, ISO and ANSI 
> standards have worked pretty well, so I suppose they aren't totally 
> useless _all_ the time.
> 
> Remember OSI networking protocols? They had a big nasty committee for all 
> their efforts, and we can see how that worked out. We got the "OSI model" 
> (which basically just apes other models already well established at the 
> time). That's about it (oh sure, a few other things like X.500 inspired 
> protocols but I think X.500 is garbage *shrug* YMMV). Things like TPx 
> protocols never caught on. Some would say it was because the world is so 
> unenlightened it couldn't recognize the genius of the commisar^H^H^H 
> committee's collective creations. I have a somewhat different viewpoint.

  The difference between the two?  ANSI codified existing examples where as
ISO created a standard in a vacuum and expected people to write
implementaitons to the standard.

> > All those "undefined" and "implementation" bits of C?  Yeah, competing 
> > implementations.
> 
> Hehe, what is a long long? Yes, you are totally right. Still, I assert 
> that C is still the defacto most portable language on Earth. What other 
> language runs on as many OS's and CPUs ? None that I can think of.

  A long long is at least 64-bits long.

  And Lua can run on as many OSs and CPUs as C.  

> > And because of the bizarre systems C can potentially run on, pointer 
> > arithmetic is ... odd as well [4].
> 
> Yeah, it's kind of an extension of the same issue, too many undefined grey 
> areas. In practice, I don't run into these types of issues much. However, 
> to be fair, I typically code on only about 3 different platforms, and they 
> are pretty similar and "modern" (BSD, Linux, IRIX).

  Just be thankful you never had to program C in the 80s and early 90s:

	http://www.digitalmars.com/ctg/ctgMemoryModel.html

  Oh, wait a second ...

	http://eli.thegreenplace.net/2012/01/03/understanding-the-x64-code-models

> > It also doesn't help that bounds checking arrays is a manual process, 
> > but then again, it would be a manual process on most CPUs [5] anyway ...
> 
> I'm in the "please don't do squat for me that I don't ask for" camp. 

  What's wrong with the following code?

	p = malloc(sizeof(somestruct) * count_of_items);

  Spot the bug yet?  

  Here's the answer:  it can overflow.  But that's okay, because sizeof()
returns an unsiged quantity, and count_of_items *should* be an unsigned
quantity (both size_t) and overflow on unsigned quantities *is* defined to
wrap (it's signed quantities that are undefined).  But that's *still* a
problem because if "sizeof(somestruvt) * count_of_items" exceeds the size of
a size_t, then the result is *smaller* than expected and you get a valid
pointer back, but to smaller pool of memory that expected.

  This may not be an issue on 64-bit systems (yet), but it can be on a
32-bit system.  Correct system code (in C99) would be:

	if (count_of_items > (SIZE_MAX / sizeof(somestruct)))
	  error();
	p = malloc(sizeof(somestruct) * count_of_items);

  Oooh ... that reminds me ... I have some code to check ... 

> I know that horrifies and disgusts some folks who want GC and auto-bounds
> checking everywhere they can cram it in. Would SSA form aid with all kinds
> of fancy compiler optimizations, including some magic bounds checking? 
> Sure. However, perhaps because I'm typical of an ignorant C coder, I would
> expect the cost of any such feature would be unacceptable to some. 

  Don't discount GC though---it simplifies a lot of code


> Also,
> there are plenty of C variants or special compilers that can do such
> things. Also, there are a few things that can be run with LD_PRELOAD which
> can help to find issues where someone has forgot to do proper bounds
> checking. 

  I'm generally not a fan of shared libraries as:

	1) Unless you are linking against a library like libc or libc++, a
	lot of memory will be wasted because the *entire* library is loaded
	up, unlike linking to a static library where only those functions
	actually used are linked into the final executable

	2) because of 1, you have a huge surface area exposed that can be
	exploited.  If function foo() is buggy but your program doesn't call
	foo(), in a static compile, foo() is not even in memory; with a
	dynamically loaded library, foo() is in memory, waiting to be called
	[1].

	3) It's slower.  Two reasons for this:

		3a) It's linking at runtime instead of compile time.  Yes,
		there are mechanisms to mitigate this, like lazy runtime
		linking (where a routine isn't resolved until it's called
		for the first time) but that only helps over the long
		term---it *still* has to be resolved at some point.

		3b) Not all CPUs have PC (program counter) relative modes
		(like the relatively obscure and little used x86---ha ha)
		and because of this, extra codes needs to be included to do
		the indirection.  So, your call to printf() is not:

					call	printf

		but more like:

					call	printf at plt

			printf at plt:	jmp	shared_lib_printf

		where printf at plt is constructed at runtime, *for every call
		in a shared library*.  This indirection adds up.  Worse,
		global data in a shared library becomes a
		multi-pointer-dereference mess.

	To see how silly this can be on a modern Linux system, run

		% ldd pick-your-executable

	for each process running and see just how many of those "shared"
	libraries are actually shared (in addition to the potential attack
	surface).

> > Because "x+1" can *never* be less than "x" (signed overflow?  What's 
> > that?)
> 
> Hmm, well, people (me included in days gone by) tend to abuse signed 
> scalars to simply get bigger integers. I really wish folks would embrace 
> uintXX_t style ints ... problem solved, IMHO. It's right there for them in 
> C99 to use.

  Um, see the above malloc() example---it's not fixed there.

  I use the uintXX_t types for interoperability---known file formats and
network protocols, and the plain (or known types like size_t) otherwise.

> > Except for, say, the Intel 432.  Automatic bounds checking on that one.
> 
> You can't always rely on the hardware, but perhaps that's your point.

  It was a joke.  Have you actually looked into the Intel 432?  Granted,
there's not much about it on the Internet, but it was slow.  And it was slow
even if you programmed in assembly!

> > It's not to say they can't test for it, but that's the problem---they 
> > have to test after each possible operation.
> 
> That's almost always the case when folks want rubber bumpers on C. That's 
> really emblematic of my issues with that seemly instinctual reaction some 
> folks have to C.

  You miss the point.  I mean, an expression like:

	y = 17 * x + 5

can't be optimized:

		mov	edx,eax
		shl	eax,4
		lea	ecx,[eax+edx+5]

but has to be:

		imul	eax,17
		jo	error	; [2]
		add	eax,5
		jo	error

For some perspective on this, there are two blogs I would recommend.  The
first is "programming in the twenty-first century":

	http://prog21.dadgum.com/

and the second on compiling and optimizing C code:

	http://blog.regehr.org/

  -spc (Sigh---called into work to debug a production problem with C and
	Lua)

[1]	How can a program that doesn't call foo() be enticed with calling
	foo()?  Return-oriented programming.

	https://en.wikipedia.org/wiki/Return-oriented_programming

[2]	Using INTO is slow:

		http://boston.conman.org/2015/09/05.2

	while JO isn't:

		http://boston.conman.org/2015/09/07.1

	mainly because JO can be branch-predicted and so the overhead of the
	actual JO instruction is practically zero.  On the other hand, the
	fact that you have to use JO means more code, which could increase
	presure on the I-cache, *and* you have to use instructions which set
	the O flag (LEA does not).  It's this (having to use instructions to
	set the O flag) that cause perhaps as much as a 5% penalty (worse
	case), depending upon the code.


More information about the cctalk mailing list