[HN Gopher] Weekend projects: getting silly with C
___________________________________________________________________
Weekend projects: getting silly with C
Author : nothacking_
Score : 211 points
Date : 2024-06-30 05:07 UTC (17 hours ago)
(HTM) web link (lcamtuf.substack.com)
(TXT) w3m dump (lcamtuf.substack.com)
| mgaunard wrote:
| aren't the switch shenanigans important to the duff's device?
| tialaramex wrote:
| Duff is relying on the fact you're allowed to intermingle the
| switch block and the loop in K&R C's syntax, the (common at the
| time but now generally frowned on or even prohibited in new
| languages) choice to drop-through cases if you don't explicitly
| break, and the related fact that C lets your loop jump back
| inside the switch.
|
| Duff is trying to optimise MMIO, you wouldn't do anything close
| to this today even in C, not least because your MMIO is no
| longer similarly fast to your CPU instruction pace and for non-
| trivial amounts of data you have DMA (which Duff's hardware did
| not). In a modern language you also wouldn't treat "MMIO" as
| just pointer indirection, to make this stay working in C they
| have kept adding hacks to the type system rather than say OK,
| apparently this is an intrinsic, we should bake it into the
| freestanding mode of the stdlib.
|
| Edited to add:
|
| For my money the successor to Tom Duff's "Device" is WUFFS'
| "iterate loops" mechanism where you may specify how to
| partially unroll N steps of the loop, promising that this has
| equivalent results to running the main loop body N times but
| potentially faster. This makes it _really easy_ for
| vectorisation to see what you 're trying to do, while still
| handling those annoying corner cases where M % N != 0 correctly
| because that's the job of the tool, not the human.
| uecker wrote:
| Not sure what you mean by "hacks to the type system". All
| modern computing essentially converged to unified memory,
| which is exactly C's model.
| tialaramex wrote:
| While it's convenient _technically_ to have unified memory
| and so it makes a lot of sense for your machine code, in
| fact the MMIO isn 't just memory, and so to make this work
| anyway in the C abstract machine they invented the
| "volatile" qualifier. (I assume you weren't involved back
| then?)
|
| This should be a suite of intrinsics. It's the same mistake
| as "register" storage, a layer violation, the actual
| mechanics bleeding through into the abstract machine and
| making an unholy mess.
|
| If you had intrinsics it's obvious where the platform
| specific behaviour lives. Can we "just" do unaligned 32-bit
| stores to MMIO? Can we "just" write one bit of a hardware
| register? It depends on your platform and so as an
| intrinsic it's obvious how to reflect this, whereas for a
| type qualifier we have no idea what the compiler did and
| the ISO document of course has to be vague to be inclusive
| of everybody.
| uecker wrote:
| I wasn't involved back then, but I know the history. I
| thought you were talking about something more recent.
|
| But this is all opinions and terms such as "unholy mess"
| etc do not impress me. In my opinion "volatile" is just
| fine as is "register. Neither are layer violations nor a
| type system problem. That the exact semantics of a
| volatile access are implementation defined seem natural.
| How is this better with an intrinsic? What I would call a
| mess are the atomics intrinsics, which - despite being
| intrinsics - are entirely unsafe and dangerous and indeed
| mess (just saw a couple of new bugs in our bug tracker).
| tialaramex wrote:
| Sure, it's just an opinion. I think the consequences
| speak very well for themselves.
| uecker wrote:
| What consequences?
| masklinn wrote:
| > Duff is relying on the fact you're allowed to intermingle
| the switch block and the loop
|
| That's just a special case of being able to intermingle
| switch with arbitrary syntax, which is what TFA does, before
| it jumps to computed gotos.
| doe_eyes wrote:
| The overarching point appears to be getting rid of angle
| brackets, which is not something that Duff is doing.
| Further, Duff's device keeps case labels on the left of its
| control structure; moving ifs to the left is the other
| "innovation" here.
|
| I think you really have to squint your eyes to see the
| similarities, beyond the general theme of exploiting the
| counterintuitive properties of switch statements.
| smusamashah wrote:
| Found these silly tricks by the author of this blog on twitter
| first. Switch statement can do loops too
| https://twitter.com/lcamtuf/status/1807129116980007037
| viraptor wrote:
| Also on the actually social network
| https://infosec.exchange/@lcamtuf/112701486085621844
| teo_zero wrote:
| Another source of surprise: 4[arr] // same as
| arr[4]
| stefanos82 wrote:
| Thanks to array decay to pointer, we basically have
| `*(array_label+offset)` which in this case of yours we have
| `*(offset+array_label)`; in other words, `*(arr+4)` is the same
| as `*(4+arr)`...that's it, really!
| trealira wrote:
| By the same principle, these are exactly the same:
| arr[i][j] j[i[arr]]
|
| These are the simplifications you'd do. You only need to know
| that a[x][y] is equivalent to (a[x])[y], and that a[x] is the
| same as x[a]. arr[i][j] (arr[i])[j]
| (i[arr])[j] j[i[arr]]
| geon wrote:
| This can be used to implement coroutines in C.
| https://stackoverflow.com/questions/24202890/switch-based-co...
| emmericp wrote:
| uIP (TCP/IP stack for tiny microcontrollers) is a another fun
| real-world example for these types of coroutines:
| https://github.com/adamdunkels/uip/blob/master/uip/lc-switch...
| nxobject wrote:
| If only there was a way of using setjmp/longjmp-style contexts
| instead of goto, un/winding the stack as required. So we could
| travel around in time... unfortunately you can't work with a
| setjmp buffer before it's actually created, unlike gotos.
| gpderetta wrote:
| sigaltstack tricks to the rescue! (Although POSIX only, not ISO
| C)
| JohnMakin wrote:
| My undergrad was entirely in the C language and I'm very glad for
| it. Sometimes more modern languages can throw me for a loop, no
| pun intended, but the beauty (and horror) of C is that you are
| pretty close to the metal, it's not very abstracted at all, and
| it allows you a lot of freedom (which is why it's so foot gunny).
|
| I will never love anything as much as I love C, but C development
| jobs lie in really weird fields I'm not interested in, and I'm
| fairly certain I am not talented enough. I have seen C wizardry
| up close that I know I simply cannot do. However, one of the more
| useful exercises I ever did was implement basic things like a
| file system, command line utilities like ls/mkdir etc. Sometimes
| they are surprisingly complex, sometimes no.
|
| After you program in C for a while certain conventions meant to
| be _extra_ careful kind of bubble up in languages in a way that
| seems weird to other people. for example I knew a guy that'd auto
| reject C PR's if they didn't use the syntax if (1==x) rather than
| if (x==1). The former will not compile if you accidentally use
| variable assignment instead of equality operator (which everyone
| has done at some point).
|
| This tendency bites me a lot in some programming cultures, people
| (ime) tend to find this style of programming as overly defensive.
| smackeyacky wrote:
| In an embedded environment, overly defensive is an asset
| JohnMakin wrote:
| That's precisely where my little professional C experience
| was. I then switched to a python shop and was initially
| horrified at some conventions, took some getting used to.
| uecker wrote:
| I force my students to do C development. And it turns out that
| it is not that hard if you approach it with modern tools which
| catch a lot of problems. The lack of abstraction is fixed with
| good libraries.
|
| C evolved a lot and many foot guns are not a problem anymore.
| For example for
|
| if (x = 1)
|
| you nowaday get a warning. https://godbolt.org/z/79acPPro6
|
| Implicit int, calling functions without prototypes, etc. are
| hard errors. And so on.
| tialaramex wrote:
| The warning says to add parentheses, which sure enough
| silences the warning, your foot, however, still has a bullet
| hole in it.
| uecker wrote:
| The warning is very clear. If you did intend to use the
| result of an assignment as truth value, you would notice.
| In any case, did not have a single problem with this type
| of error in the last decades, working with programmers of
| various skill levels including beginners.
| lelanthran wrote:
| > The warning says to add parentheses, which sure enough
| silences the warning, your foot, however, still has a
| bullet hole in it.
|
| The warning also says that it's an assignment. It's a
| pretty clear warning meant to force the programmer to do
| extra work to get the error.
| Joel_Mckay wrote:
| The libglib-dev with gcc is very handy for toy projects, but
| only _after_ students try to write their own versions:
|
| https://docs.gtk.org/glib/data-structures.html
|
| It could be fun to do a lab summary after the lists and
| hashes introduction.
|
| Have a wonderful day, =)
| uecker wrote:
| I absolutely I agree that learning to create you own
| abstractions is an incredible useful skill. It depends
| though. For a programming course this makes absolutely
| sense. But for applied problems in, say, biomedical
| engineering, this does not work. Many students know only a
| bit of Python, and then it is too much and "too
| inconvenient" to start from scratch in C. With Python they
| have a lot of things more easily available, so they make
| quick progress. This does not lead to good results though!
| For most of the Python projects, we end of throwing away
| the code later. Another problem is that students often do
| not know what they are doing, e.g. the use some statistical
| package or visualization package and get nicely looking
| results, but they do not know what it means and often it is
| entirely wrong. For machine learning projects it is even
| worse. So much nonsense and errors from copying other
| people Python code....
| Joel_Mckay wrote:
| Python like Basic abstracted far to many details away
| from students, and trying to convince people they need to
| know how a CPU works later is nearly impossible.
|
| In general, digging deep enough down a stack, and it
| drops back into the gsl:
|
| https://www.gnu.org/software/gsl/
|
| Indeed, first month attrition rates for interns at some
| companies is over 46%. =3
| 8372049 wrote:
| > if they didn't use the syntax if (1==x) rather than if
| (x==1). The former will not compile if you accidentally use
| variable assignment instead of equality operator
|
| No need for Yoda notation. clang will warn of this by default
| and gcc will do so if you compile with -Wall, which should also
| be your default.
| frou_dh wrote:
| > for example I knew a guy that'd auto reject C PR's if they
| didn't use the syntax if (1==x) rather than if (x==1). The
| former will not compile if you accidentally use variable
| assignment instead of equality operator
|
| I've seen that one and personally dislike that mindset: Making
| the code less readable to compensate for a disinterest in using
| actual static analysis tooling.
| tuveson wrote:
| These days GCC and Clang will both give you warnings for this
| if you have -Wall, which everyone should.
| mgerdts wrote:
| > I have seen C wizardry up close that I know I simply cannot
| do.
|
| I have written C at least a few times per year for over 30
| years. About ten years of that was OS development on Solaris
| and its derivatives.
|
| Articles like this show crazy things you can do in C. I've
| never found the need to do things like this and have never seen
| them in the wild.
|
| The places that wizardry is required are places like integer
| and buffer overflow, locking, overall structure of large
| codebases, build infrastructure, algorithms, etc. Many of these
| are concerns in most languages.
|
| > auto reject C PR's if they didn't use the syntax if (1==x)
| rather than if (x==1)
|
| When I was a student in the 90s advice like this would have
| been helpful. Compiler warnings and static analyzers are so
| much better now that tricks like this are not needed.
| lelanthran wrote:
| > I knew a guy that'd auto reject C PR's if they didn't use the
| syntax if (1==x) rather than if (x==1). The former will not
| compile if you accidentally use variable assignment instead of
| equality operator (which everyone has done at some point).
|
| That's not so much of a footgun anymore - the common C
| compilers will warn you about that so there's not much point in
| defending against it.
|
| Same with literal format string parameters to printf functions:
| the compiler is very good at warning about mismatched types.
| JonChesterfield wrote:
| This features the construct switch(k) {
| if (0) case 0: x = 1; if (0) case 1: x = 2; if
| (0) default: x = 3; }
|
| which is a switch where you don't have to write break at the end
| of every clause. #define brkcase if (0) case
|
| That might be worth using. Compilers won't love the control flow
| but they'll probably delete it effectively.
| leni536 wrote:
| Surely the following would work just as well?
| #define brkcase break;case
|
| kinda defeats the purpose of the macro even.
| MaxBarraclough wrote:
| That strikes me as better. The original macro presumably
| misbehaves if there's more than one statement in a sequence,
| as the _if_ will only affect the first statement.
| wrsh07 wrote:
| I think the behavior is slightly different since this one
| breaks the above case, and the other one only omits its case
| from fallthrough
|
| Incidentally, what happens if you use your brkcase as the
| first case?
|
| I don't find either particularly exciting - a macro that
| would append break to the current case feels better
| leni536 wrote:
| Both version of the macro makes this fall through from 0:
| switch (a) { brkcase 0: foo(); case 1:
| bar(); }
|
| so in a sense the `if (0) case` trick also affects the
| previous case, not the current one. But that one also falls
| apart when there are multiple statements under the brkcase.
| jppittma wrote:
| I think it is super unclear how this works, and I would prefer
| the same control flow using goto, rather than the duffs device
| style switch abuses.
| asveikau wrote:
| It only works if the case label body is a single line or is
| enclosed in brackets.
|
| I'll confess, I've used this construct to mean "omit the first
| line of the next case label but otherwise fall through".
|
| If you think of the case label as merely a label and not a
| delimiter between statements all of this makes sense.
| fanf2 wrote:
| see also https://www.chiark.greenend.org.uk/~sgtatham/mp/
|
| Metaprogramming custom control structures in C by Simon Tatham
| metadat wrote:
| Discussed in July 2021 (43 comments):
|
| https://news.ycombinator.com/item?id=27781784
| quietbritishjim wrote:
| > The above example will print the value of a, but it won't be
| initialized to 123!
|
| It certainly could do though. In C, using an uninitialised
| variable does _not_ mean "whatever that memory happened to have
| in it before" (although that is a potential result). Instead,
| it's undefined behaviour, so the compiler can do what it likes.
|
| For example, it could well unconditionally initialise that memory
| to 123. Alternatively, it could notice that the whole snippet has
| undefined behaviour so simply replace it with no instructions, so
| it doesn't print anything at all. It could even optimise away the
| return that presumably follows that code in a function, so it
| ends up crashing or doing something random. It could even
| optimise away the instructions _before_ that snippet, if it can
| prove that they would only be executed if followed by undefined
| behaviour - essentially the undefined behaviour can travel back
| in time!
| uecker wrote:
| UB can not travel back in time in C. Although it is true that
| it can affect previous instructions, but that code is reordered
| or transformed in complicated ways is true even without UB.
| emmericp wrote:
| The time-travelling UB interpretation was popularized by this
| blog post about 10 years ago [1].
|
| I'm not enough of a specification lawyer to say that this is
| definitely true, but the reasoning and example given there
| seems sound to me.
|
| [1] https://devblogs.microsoft.com/oldnewthing/20140627-00/?p
| =63...
| uecker wrote:
| Yes, random blog posts did a lot of damage here. Also
| broken compilers [1]. Note that blog post is correct about
| C++ but incorrectly assumes this is true for C as well.
|
| [1]. https://developercommunity.visualstudio.com/t/Invalid-
| optimi...
| kibwen wrote:
| I'm inclined to trust Raymond Chen and John Regehr on
| these matters, so if you assert that they're incorrect
| here then a source to back up your assertion would help
| your argument.
| uecker wrote:
| I am a member of WG14. You should check the C standard. I
| do not see how "time-travel" is a possible reading of the
| definition of UB in C. We added another footnote to C23
| to counter this idea:
|
| https://www.open-
| std.org/jtc1/sc22/wg14/www/docs/n3220.pdf "Any other
| behavior during execution of a program is only affected
| as a direct consequence of the concrete behavior that
| occurs when encountering the erroneous or non portable
| program construct or data. In particular, all observable
| behavior (5.1.2.4) appears as specified in this document
| when it happens before an operation with undefined
| behavior in the execution of the program."
|
| I should point out that compilers also generally do not
| do true time-travel: Consider this example:
| https://godbolt.org/z/rPG14rrbj
| grumbelbart wrote:
| So maybe we have different definitions of "time travel".
| But I recall that
|
| - if a compiler finds that condition A would lead to UB,
| it can assume that A is never true - that fact can
| "backpropagate" to, for example, eliminate comparisons
| long before the UB.
|
| Here is an older discussion:
| https://softwareengineering.stackexchange.com/q/291548
|
| Is that / will that no longer be true for C23? Or does
| "time-travel" mean something else in this context?
| singron wrote:
| E.g. this godbolt: https://godbolt.org/z/eMYWzv8P8
|
| There is unconditional use of a pointer b, which is UB if
| b is null. However, there is an earlier branch that
| checks if b is null. If we expected the UB to
| "backpropagate", the compiler would eliminate that
| branch, but both gcc and clang at O3 keep the branch.
|
| However, both gcc and clang have rearranged the side
| effects of that branch to become visible at the end of
| the function. I.e. if b is null, it's as if that initial
| branch never ran. You could observe the difference if you
| trapped SIGSEGV. So even though the compiler didn't
| attempt to "time-travel" the UB, in combination with
| other allowed optimizations (reordering memory accesses),
| it ended up with the same effect.
| uecker wrote:
| There may be different definitions, but also a lot of
| incorrect information. Nothing changes with C23 except
| that we added a note that clarifies that UB can not time-
| travel. The semantic model in C only requires that
| observable effects are preserved. Everything else can be
| changed by the optimizer as long as it does not change
| those observable effects (known as the "as if"
| principle). This is generally the basis of most
| optimizations. Thus, I call time-travel only when it
| would affect previous _observable_ effects, and this what
| is allowed for UB in C++ but not in C. Earlier non-
| observable effects can be changed in any case and is
| nothing speicifc to UB. So if you call time-travel also
| certain optimization that do not affect earlier
| observable behavior, then this was and is still allowed.
| But the often repeated statement that a compiler can
| assume that "A is never true" does not follow (or only
| in very limited sense) from the definition of UB in ISO C
| (and never did), so one has to be more careful here. In
| particular it is not possible to remove I/O before UB.
| The following code has to print 0 when called with zero
| and a compiler which would remove the I/O would not be
| conforming.
|
| int foo(int x)
|
| { printf("%d\n", x);
| fflush(stdout); return 1 / x;
|
| }
|
| In the following example
|
| int foo(int x)
|
| { if (x) bar(x); return 1 / x;
|
| }
|
| the compiler could indeed remove the "if" but not because
| it were allowed to assume that x can never be zero, but
| because 1 / 0 can have arbitrary behavior, so could also
| call "bar()" and then it is called for zero and non-zero
| x and the if condition could be removed (not that
| compilers would do this)
| lmm wrote:
| While interactions with _volatile_ and interactive
| streams cannot time-travel, anything else is free to -
| the standard only imposes requirements on a conforming
| implementation in terms of the contents of files at
| program termination, and programs with undefined
| behaviour are not required to terminate, so there are
| approximately no requirements on a program that invokes
| undefined behaviour.
| quietbritishjim wrote:
| > Also broken compilers [1].
|
| The issue you linked to is not a counter example because,
| as the poster said, g may terminate the program in which
| case that snippet does not have undefined behaviour even
| if b is zero. The fact that they bothered to mention that
| g may terminate the program seems like an acknowledgement
| that it would be valid to do that time travelling if it
| didn't.
|
| > Note that blog post is correct about C++ but
| incorrectly assumes this is true for C as well.
|
| Presumably you're referring to this line of the C++
| standard, which does not appear in the C standard:
|
| > However, if any such execution contains an undefined
| operation, this International Standard places no
| requirement on the implementation executing that program
| with that input (not even with regard to operations
| preceding the first undefined operation).
|
| I looked at every instance of the word "undefined" in the
| C standard and, granted, it definitely didn't have
| anything quite so clear about time travel as that. But it
| also didn't make any counter claims that operations
| before are valid. It pretty much just said that undefined
| behaviour causes behaviour that is undefined! So, without
| strong evidence, it seem presumptuous to assume that
| operations provably before undefined behaviour are well
| defined.
| uecker wrote:
| The poster is me. You are right that this is not an
| example for time-travel. There aren't really good
| examples for true time travel because compilers generally
| do not do this. But my point is that with compilers
| behaving like this, people might confuse this for time-
| traveling UB. I have certainly met some who did and the
| blog posts seems to have similar examples (but I haven't
| looked closely now).
|
| Note that I am a member of WG14. We added more
| clarification to C23 to make clear that this is not a
| valid interpretation of UB, see here: https://www.open-
| std.org/jtc1/sc22/wg14/www/docs/n3220.pdf
| quietbritishjim wrote:
| Ok, fair enough. I must admit I was looking at C99 as I
| thought that was most generally relevant, I don't follow
| recent C standards (as much as I do those for C++) and
| C23 hasn't been ratified yet. I've found your new
| snippet:
|
| > In particular, all observable behavior (5.1.2.4)
| appears as specified in this document when it happens
| before an operation with undefined behavior in the
| execution of the program.
|
| I consider that a change in the standard but, of course,
| that's allowed, especially as it's backwards compatible
| for well defined programs.
|
| The wording is a little odd: it makes it sound a like you
| need some undefined behaviour in order to make the
| operations beforehand work, and, taken very literally,
| that operations between two undefined behaviours will
| work (because they're still "before an operation with
| undefined behavior"). But I suppose the intention is
| clear.
| uecker wrote:
| The definition of UB (which hasn't changed) is:
| "behavior, upon use of a nonportable or erroneous program
| construct or of erroneous data, for which this document
| imposes no requirement."
|
| Note that the "for which" IMHO already makes this clear
| that this can not travel in time. When everything could
| be affected these words ("for which") would be
| meaningless.
| AlotOfReading wrote:
| I didn't notice that section when I last read through
| C23, but I'm very glad to see it. Reining in UB is one of
| the hardest problems I've had to deal with, and being
| able to say operations are defined up to the point of UB
| makes my job so much easier.
|
| The lack of clarity in earlier standards made it
| impossible to deal with code incrementally, since all the
| unknown execution paths could potentially breach back in
| time and smash your semantics.
| uecker wrote:
| Thank you. This was my motivation. It is only a small
| step... much more work to do.
| ant6n wrote:
| > but that code is reordered or transformed in complicated
| ways is true even without UB.
|
| Without undefined behavior, the compiler emits code that has
| the behavior defined by the code --- the ordering may be
| altered, but not the behavior.
| uecker wrote:
| Yes, and with undefined behavior, the compiler has to emit
| code that has the behavior defined by the code up to the
| operation that has undefined behavior.
| jftuga wrote:
| This reminds me of some silly C code I once wrote for fun, which
| counts down from 10 to 1: #include <stdio.h> //
| compile & run: gcc -Wall countdown.c -o countdown && ./countdown
| int n = 10; int main(int argc, char *argv[]) { printf("%d\n", n)
| && --n && main(n, NULL); }
|
| Python version: import sys # run: python3
| countdown.py 10 def main(n:int):
| sys.stdout.write(f"{n}\n") and n-1 and main(n-1)
| main(int(sys.argv[1]))
|
| Shell version: # run ./countdown.sh 10
| echo $1 && (($1-1)) && $0 $(($1-1))
| cbrpnk wrote:
| I don't think I've ever thought of explicitly calling main().
| Made me chuckle.
| akdev1l wrote:
| I think it is UB
|
| Edit: actually looks like it is UB in C++ but not C
| colejohnson66 wrote:
| Why would calling main be UB!? How is crt0 supposed to
| work?
| quietbritishjim wrote:
| Nitpick: you could replace sys.stdout.write(f"{n}\n") with
| print(n). The current code looks very much like it was written
| for Python 2 (apart from the f string!), where print was a
| statement. As of Python 3, print is just a regular function. It
| returns None, which is falsey, so you'd also need to change
| your first "and" to an "or".
| pdimitar wrote:
| _Fun at parties alert:_
|
| Let's stop getting silly with C, too many CVEs!
|
| ---
|
| _Serious comment:_
|
| It's a rather cool article actually. Not something I'd do daily
| but it's kind of sort of useful to know these techniques.
| nj5rq wrote:
| Why did I not know that this: case 1 ... 10:
|
| Is valid C? I have been programming in C for years, what standard
| is this from?
| G4E wrote:
| Unless it has been recently standardized it's not valid C, it's
| a GNU extension.
| dekhn wrote:
| It appears to be a GNU C extension:
| https://gcc.gnu.org/onlinedocs/gcc/Case-Ranges.html but I
| couldn't find the history of the extension. I believe it is not
| in standard C (not sure about clang).
| nj5rq wrote:
| I just tried it, and it works with clang version 17.0.6.
| o11c wrote:
| Due to the way lifetimes work in C (they begin with the block,
| not the declaration), the following is legal:
| #include <stdio.h> #include <stddef.h> int
| main() { { int *p = NULL;
| if (p) { what:
| printf("a = %d\n", *p); return 0;
| } int a = 123; p = &a;
| goto what; } }
| junon wrote:
| > switch (i) case 1: puts("i = 1");
|
| I've seen this in the wild, particularly with macros.
| #define assert(c) if (!c) ... if (foo)
| assert(...); else bar(); // oops!
| codext wrote:
| The final obfuscated code snippet in the article brought to light
| another GCC extension:
|
| https://stackoverflow.com/questions/34559705/ternary-conditi...
___________________________________________________________________
(page generated 2024-06-30 23:01 UTC)