📜 ⬆️ ⬇️

Flytouch 2 / Superpad III and an attempt to save bytes in the Linux kernel

I had planned to write this article a long time ago, but in the last months I could not find enough time. While I was pondering over the article, making examples and checking my guesses, constancy was already discussed on Habré - [one] [2] .

For fun, let's try to make such savings not with a spherical project in a vacuum, but with the most living and ambitious project - with the Linux kernel!

The article has the following structure:
  1. Prehistory
  2. Flytouch 2 / Superpad III Tablet
  3. Constancy
  4. Measurements
  5. Conclusion
  6. List of sources

In theory, the article could be divided into two parts: the technical details about the tablet and the use of extern const in the Linux kernel. It seems to me that then full-fledged publications would not work, so I combined all the material into one article.

Background ^


Back in 2011, I was reading a lot of reviews about Chinese countries - [3] [four] [5] . Naturally, I wanted such a miracle and I ordered it.
')
I played a little, looked at the cinema, thought about mahjong and ... And soon my opinion became alternative to the previous one - [6] .

After that, it was decided to study the tablet from the programmer's point of view, namely, to install the usual Linux there, to compile something.

Flytouch 2 / Superpad III ^


Everything described above is valid for a china card with such identifying signs:
to show
DF-MID10-IX210-V1.1
2010-01-20

XW11070501B512M03101

Here [8] Geek porn available. In the pictures you can clearly see the findings of JTAG & USART. Also note the labeling.

Firmware ^

To flash the device, you need to upload files with the names firmware2, firmware-discovery, bootloader-discovery onto the SD card with the FAT file system; Insert it into the slot and turn on the tablet. (It worked for me with a 256MB flash drive and a W95 FAT32 file system - identifier 0xB in fdisk).

You will need a working firmware. Although the subject of the article has long been outdated, you can still find a working version here - [12] . I took the version of Axlien.

Files Purpose:
  1. * -discovery files are just zip archives with the necessary data for updating. firmware-discovery contains everything related to Android. I can’t say anything about the bootloader - it didn’t come across to me, but one can guess from the name that this is an update of the bootloader - it’s not clear which one;
  2. firmware2 file is of the greatest interest. At that time, I did not find any references to the format of this file anywhere on the network. I thought, well, the Chinese cannot, and even for such cheap junk, create a powerful system based on cryptography ... and it turned out to be right! The first 192 bytes are just garbage. You can take firmware2 from the working firmware and wipe the specified header with zeros - the firmware update process will still start [10] . Next comes the usual U-Boot image, the parameters of which can be obtained using mkimage.

Thus, if you build your own working core (zImage) for a tablet, create a U-Boot image with the same parameters and add a 192-byte header, you will be able to load your version of Linux.

It is also possible to play with the "official" firmware2. There are tools for unpacking and back-packing the zImage file [9] . You can unpack and examine the device update scripts, in particular, to understand what is updating the bootloader-discovery and how. On the other hand, it is possible to disable the mentioned scripts and be in the busybox shell.

Kernel build ^

In search of the kernel sources for this processor, I came across a discussion [13] , where someone atp_uestc reported the discovery of the kernel sources for the ZT-180 tablet.

Now it is difficult for me to remember the chronology of events and what core I tried to load first: either “vanilla” from here - [14] , whether modified from here - [15] . Only the result was remembered - it did not work, the boot process froze on the “Decompressing kernel ...”

After some time, yuray appeared in the discussion, adding support for this processor to the third version of the Linux kernel. It was this version of the kernel that I was able to launch in Chinese.

Download source 3.4.x kernel sources from kernel.org . At the time of this writing, version 3.4.108 and below was successfully compiled. Download and apply kernel patches from yuray from here rtck.org/zt180/patches/zt180_b0_3.4.patch.xz :
$ cd /path/to/linux-3.4.x $ patch -p1 -i /path/to/zt180_b0_3.4.patch 

Configure the config. I haven’t met the correct config for this Chinese, so I suggest taking the dot-config from here [19] :
 $ make V=1 ARCH=arm CROSS_COMPILE=/path/to/toolchain/arm-infotm-linux-gnueabi- oldconfig 

Kernel will require initramfs. I am a user of the Slackware GNU / Linux OS, so it was decided to take the installer's initrd out of it in order to later install the slak on the tablet. Load uinitrd-kirkwood.img [16] , there are also commands for unpacking the image in the kernels / README.txt file. From the original, the image is weak; contains binaries for x86 / amd64, but we need them for ARM.

Create a folder, unpack the image into it:
 $ mkdir -p /path/to/uinitrd.extracted $ pushd !$ $ dd if=/path/to/uinitrd-kirkwood.img bs=64 skip=1 | gzip -dc | sudo cpio -div $ popd 

Specify the path to the folder with the image in the config:
# ...
CONFIG_BLK_DEV_INITRD = y
CONFIG_INITRAMFS_SOURCE = "/ path / to / uinitrd.extracted"
CONFIG_INITRAMFS_ROOT_UID = 0
CONFIG_INITRAMFS_ROOT_GID = 0
# ...

Compile the kernel in two passes:
 # clean initrd directory $ sudo rm -rf /path/to/uinitrd.extracted/lib/{modules, firmware} # build kernel for the first time $ make KALLSYMS_EXTRA_PASS=1 ARCH=arm CROSS_COMPILE=/path/to/toolchain/arm-infotm-linux-gnueabi- -j2 zImage # build modules against the kernel $ make KALLSYMS_EXTRA_PASS=1 ARCH=arm CROSS_COMPILE=/path/to/toolchain/arm-infotm-linux-gnueabi- -j2 modules # install modules into initramfs dir $ sudo make ARCH=arm CROSS_COMPILE=/path/to/toolchain/arm-infotm-linux-gnueabi- -j2 modules_install INSTALL_MOD_PATH=/path/to/uinitrd.extracted/ # install firmware to initramfs folder $ sudo make ARCH=arm CROSS_COMPILE=/path/to/toolchain/arm-infotm-linux-gnueabi- -j2 firmware_install INSTALL_MOD_PATH=/path/to/uinitrd.extracted/ # remove unnecessary files $ sudo rm -rf /path/to/uinitrd.extracted/lib/modules/3.4.*/{build,source} # build the kernel again, with initramfs dir contains modules $ make KALLSYMS_EXTRA_PASS=1 ARCH=arm CROSS_COMPILE=/path/to/toolchain/arm-infotm-linux-gnueabi- -j2 zImage 

Can get out the error [17] [18] . I was helped by the solution that is proposed in the text of the error: the parameter KALLSYMS_EXTRA_PASS = 1.

Then we create firmware2:
 # make u-boot image $ /path/to/mkimage -A arm -C none -O linux -T kernel -a 0x40008000 -e 0x40008000 -n linux-3.4 -d arch/arm/boot/zImage arch/arm/boot/uImage # make firmware2 $ cat /path/to/firmware2.header arch/arm/boot/uImage > firmware2 
and load the tablet from it.

A script was written to automate the build. [19] . The following command was used:
 $ ( time ./my_build.sh ) |& tee `date +%M%H_%F`-build-log.txt 

Loaded - YUSB does not work, more precisely, there was no power at the ports. I asked for help from yuray, to which he replied that most likely the layout of my china map has a different layout, different from the ZT-180.

Then I found this great article. [11] how to disassemble parts of the working core. I decided to restore the code of functions of the “original” firmware related to USB and compare them with the versions in the source code. To my disappointment, the functions matched up to digraphs. However, the working core had functions that were not in the source code. Apparently there lies some magic that inspires life in USB ports, but I left it under the veil of secrecy ...

In the process of studying the restored assembler code, I noticed one interesting point: in many functions, strange instructions followed their epilogue:
 /* ... */ c039e32c: e59f0030 ldr r0, [pc, #48] ; c039e364 <_binary_0xc039e2e8_imapx200_decode_suspend_start+0x7c> /* ... */ c039e360: e89da800 ldm sp, {fp, sp, pc} c039e364: f0200000 undefined instruction 0xf0200000 c039e368: c24b1c9c subgt r1, fp, #39936 ; 0x9c00 

See the 0xF0200000? Strange vague instructions ... and even found in several places. This is a kind of base address, I thought, which means that theoretically it is possible to save 4 (four!) Bytes with each such function: place this value in one place, and all functions will load it at one address - get a constant!

Constancy ^


By meyers [20] everything is very well written. I will give only some explanations.

Compilers of C and C ++ languages ​​are one-pass, and therefore cannot independently optimize constants. At a minimum, help from the linker is required, for example, the / Gw parameter [1] .

The same one-pass weakness can play into the hands: use extern const [1] . The idea is to describe the constant in the header file as follows:
 #ifndef MY_CONST_H #define MY_CONST_H extern const int my_constant; #endif /* MY_CONST_H */ 

Then write a line in some file:
 const int my_constant = 42; 
This can be placed in any translation unit. Now the compiler will not be able to sew the value of the constant into the code, but will have to make some reference to it. The linker will receive only one object file, in which there will be a constant value, and then substitute the final addresses into the code. Of course there may be options, but in general, the compiler alone is helpless before such a trick and is forced to do what is described above. However, there is a small nuance [21] .

For the experiment, you will need to make the following edits. [19] :
to show
 diff -ru ./linux-3.4.108/arch/arm/mach-imapx200/Makefile ../linux-3.4.108/arch/arm/mach-imapx200/Makefile --- ./linux-3.4.108/arch/arm/mach-imapx200/Makefile 2015-09-09 12:17:52.483020878 +0300 +++ ../linux-3.4.108/arch/arm/mach-imapx200/Makefile 2015-09-08 17:11:45.775035963 +0300 @@ -6,7 +6,7 @@ #IMAPX200 support files -obj-$(CONFIG_CPU_IMAPX200) += irq.o clock.o time.o devices.o pwm.o +obj-$(CONFIG_CPU_IMAPX200) += irq.o clock.o time.o devices.o pwm.o constants.o diff -ru ./linux-3.4.108/arch/arm/mach-imapx200/include/mach/imapx_base_reg.h ../linux-3.4.108/arch/arm/mach-imapx200/include/mach/imapx_base_reg.h --- ./linux-3.4.108/arch/arm/mach-imapx200/include/mach/imapx_base_reg.h 2015-09-09 12:17:52.498020874 +0300 +++ ../linux-3.4.108/arch/arm/mach-imapx200/include/mach/imapx_base_reg.h 2015-09-08 17:11:45.778035706 +0300 @@ -15,13 +15,23 @@ #define IMAPX200_SDRAM_PA (0x40000000) /************************Virtual address for peripheral*************************/ -#define IMAP_VA_SYSMGR IMAP_ADDR(0x00200000) -#define IMAP_VA_IRQ IMAP_ADDR(0x00000000) -#define IMAP_VA_TIMER IMAP_ADDR(0x00300000) -#define IMAP_VA_WATCHDOG IMAP_ADDR(0x00600000) -#define IMAP_VA_GPIO IMAP_ADDR(0x00400000) -#define IMAP_VA_NAND IMAP_ADDR(0x00500000) -#define IMAP_VA_FB IMAP_ADDR(0x00700000) +#if defined(IMAP_USE_MACRO_CONSTANTS) || defined(__ASSEMBLY__) +# define IMAP_VA_SYSMGR IMAP_ADDR(0x00200000) +# define IMAP_VA_IRQ IMAP_ADDR(0x00000000) +# define IMAP_VA_TIMER IMAP_ADDR(0x00300000) +# define IMAP_VA_WATCHDOG IMAP_ADDR(0x00600000) +# define IMAP_VA_GPIO IMAP_ADDR(0x00400000) +# define IMAP_VA_NAND IMAP_ADDR(0x00500000) +# define IMAP_VA_FB IMAP_ADDR(0x00700000) +#else +extern const void __iomem __force * const IMAP_VA_SYSMGR; +extern const void __iomem __force * const IMAP_VA_IRQ; +extern const void __iomem __force * const IMAP_VA_TIMER; +extern const void __iomem __force * const IMAP_VA_WATCHDOG; +extern const void __iomem __force * const IMAP_VA_GPIO; +extern const void __iomem __force * const IMAP_VA_NAND; +extern const void __iomem __force * const IMAP_VA_FB; +#endif /* defined(IMAP_USE_MACRO_CONSTANTS) || defined(__ASSEMBLY__) */ #define PERIPHERAL_BASE_ADDR_PA (0x20C00000) diff -ru ./linux-3.4.108/arch/arm/plat-imap/cpu.c ../linux-3.4.108/arch/arm/plat-imap/cpu.c --- ./linux-3.4.108/arch/arm/plat-imap/cpu.c 2015-09-09 12:17:52.607021038 +0300 +++ ../linux-3.4.108/arch/arm/plat-imap/cpu.c 2015-09-08 17:18:30.646035384 +0300 @@ -1,3 +1,5 @@ +#define IMAP_USE_MACRO_CONSTANTS + /******************************************************************************** ** linux-2.6.28.5/arch/arm/plat-imap/cpu.c ** diff -ru ./linux-3.4.108/arch/arm/plat-imap/gpio.c ../linux-3.4.108/arch/arm/plat-imap/gpio.c --- ./linux-3.4.108/arch/arm/plat-imap/gpio.c 2015-09-09 12:17:52.614020935 +0300 +++ ../linux-3.4.108/arch/arm/plat-imap/gpio.c 2015-09-08 17:18:38.566035206 +0300 @@ -1,3 +1,5 @@ +#define IMAP_USE_MACRO_CONSTANTS + /* arch/arm/plat-imapx200/gpiolib.c * * Copyright 2008 Openmoko, Inc. diff -ru ./linux-3.4.108/arch/arm/plat-imap/pm_imapx200.c ../linux-3.4.108/arch/arm/plat-imap/pm_imapx200.c --- ./linux-3.4.108/arch/arm/plat-imap/pm_imapx200.c 2015-09-09 12:17:52.631020886 +0300 +++ ../linux-3.4.108/arch/arm/plat-imap/pm_imapx200.c 2015-09-08 17:18:52.102036874 +0300 @@ -1,3 +1,5 @@ +#define IMAP_USE_MACRO_CONSTANTS + #include <linux/init.h> #include <linux/suspend.h> #include <linux/serial_core.h> diff -ru ./linux-3.4.108/drivers/video/infotm/imapfb.c ../linux-3.4.108/drivers/video/infotm/imapfb.c --- ./linux-3.4.108/drivers/video/infotm/imapfb.c 2015-09-09 12:17:53.350020920 +0300 +++ ../linux-3.4.108/drivers/video/infotm/imapfb.c 2015-09-08 17:34:34.814042727 +0300 @@ -1,3 +1,5 @@ +#define IMAP_USE_MACRO_CONSTANTS + /***************************************************************************** ** drivers/video/infotm/imapfb.c ** diff -ru --new-file ./linux-3.4.108/arch/arm/mach-imapx200/constants.c ../linux-3.4.108/arch/arm/mach-imapx200/constants.c --- ./linux-3.4.108/arch/arm/mach-imapx200/constants.c 1970-01-01 03:00:00.000000000 +0300 +++ ../linux-3.4.108/arch/arm/mach-imapx200/constants.c 2015-09-09 14:56:53.513487879 +0300 @@ -0,0 +1,11 @@ +#include <linux/compiler.h> + +#include <mach/imapx_base_reg.h> + +const void __iomem __force * const IMAP_VA_SYSMGR = IMAP_ADDR(0x00200000); +const void __iomem __force * const IMAP_VA_IRQ = IMAP_ADDR(0x00000000); +const void __iomem __force * const IMAP_VA_TIMER = IMAP_ADDR(0x00300000); +const void __iomem __force * const IMAP_VA_WATCHDOG = IMAP_ADDR(0x00600000); +const void __iomem __force * const IMAP_VA_GPIO = IMAP_ADDR(0x00400000); +const void __iomem __force * const IMAP_VA_NAND = IMAP_ADDR(0x00500000); +const void __iomem __force * const IMAP_VA_FB = IMAP_ADDR(0x00700000); 

Measurements ^


It was decided to measure two indicators:
A script that counts the size of all functions containing an imap subword is here. [19] .

The results are as follows:
to show
 # diff -ru 1.log 2.log 
 --- 1.log 2015-09-11 16:57:28.430158628 +0300 +++ 2.log 2015-09-11 16:57:20.627161803 +0300 @@ -1,4 +1,4 @@ -Data Size: 17844272 Bytes = 17426.05 kB = 17.02 MB +Data Size: 17843856 Bytes = 17425.64 kB = 17.02 MB Load Address: 0x40008000 Entry Point: 0x40008000 --- 22012015-rom/sizes.txt 2015-01-28 15:25:51.107945315 +0300 +++ 28012015-rom/sizes.txt 2015-01-28 15:15:48.052949785 +0300 @@ -1,25 +1,25 @@ -imapx200_timer_mask - 44 -imapx200_timer_unmask - 52 -imapx200_timer_ack - 44 +imapx200_timer_mask - 52 +imapx200_timer_unmask - 60 +imapx200_timer_ack - 52 imapx200_irq_add - 24 imapx200_irq_init - 32 imapx200_irq_wake - 44 -imapx200_irq_unmask - 136 -imapx200_irq_mask - 132 -imapx200_irq_ack - 120 +imapx200_irq_unmask - 172 +imapx200_irq_mask - 160 +imapx200_irq_ack - 152 imap_clk_enable - 60 imap_clkcon_enable - 76 -imapx200_gettimeoffset - 64 -imapx200_timer_setup - 164 -imapx200_timer_interrupt - 64 +imapx200_gettimeoffset - 68 +imapx200_timer_setup - 168 +imapx200_timer_interrupt - 76 imap_pwm_suspend - 188 imap_pwm_resume - 184 imap_pwm_start - 164 imap_timer_setup - 404 imap_default_idle - 20 -imapx_poweroff - 56 -imapx_reset - 64 -imapx200_idle - 52 +imapx_poweroff - 88 +imapx_reset - 68 +imapx200_idle - 48 imap_set_board - 112 imapx200_gpio_setpull_updown - 56 imapx200_gpio_getpull_updown - 36 @@ -48,7 +48,7 @@ imapx200_pm_prepare - 24 imapx200_pm_finish - 20 imapx200_pm_do_save - 88 -imapx200_pm_enter - 436 +imapx200_pm_enter - 500 imapx200_pm_configure_extint - 20 imapx200_pm_prepare - 24 imapx200_pm_init - 80 @@ -86,8 +86,8 @@ imapfb_resume - 196 imapfb_suspend - 204 imapfb_backlight_power_supply - 20 -imapfb_set_clk - 44 -imapfb_set_gpio - 88 +imapfb_set_clk - 52 +imapfb_set_gpio - 96 imapfb_set_brightness - 36 imapfb_lcd_power_supply - 32 con_get_unimap - 360 @@ -153,7 +153,7 @@ imap_nand_irq - 92 ehci_imapx200_drv_remove - 80 ehci_imapx200_init - 1148 -ehci_imapx200_drv_probe - 552 +ehci_imapx200_drv_probe - 556 ohci_hcd_imapx200_drv_remove - 80 ohci_imapx200_start - 100 ohci_hcd_imapx200_drv_probe - 512 @@ -196,22 +196,22 @@ imapx200_i2c_probe - 824 imapx200_i2c_irq - 920 imapx200_decode_poll - 184 -imapx200_decode_suspend - 132 +imapx200_decode_suspend - 44 imapx200_decode_resume - 68 -imapx200_decode_release - 192 +imapx200_decode_release - 104 imapx200_decode_open - 124 imapx200_decode_remove - 208 imapx200_decode_ioctl - 384 -imapx200_decode_probe - 908 +imapx200_decode_probe - 816 imapx200_decode_irq_handle - 264 imapx200_encode_poll - 84 -imapx200_encode_suspend - 116 +imapx200_encode_suspend - 28 imapx200_encode_ioctl - 348 imapx200_encode_resume - 68 -imapx200_encode_release - 188 +imapx200_encode_release - 100 imapx200_encode_open - 120 imapx200_encode_remove - 232 -imapx200_encode_probe - 1080 +imapx200_encode_probe - 988 imapx200_encode_irq_handle - 136 sdhci_imap_set_clk_src - 52 sdhci_imap_resume - 36 @@ -220,7 +220,7 @@ sdhci_imap_get_timeout_clk - 40 imapfb_probe - 2952 imapfb_init - 28 -sdhci_imap_probe - 604 +sdhci_imap_probe - 608 sdhci_imap_remove - 20 name_imapx200 - 12 imapfb_a1rgb232_8 - 48 @@ -342,8 +342,8 @@ __kstrtab_imap_get_reservemem_paddr - 26 __kstrtab_con_copy_unimap - 16 __kstrtab_con_set_default_unimap - 23 -imapx200_init_clocks - 1120 -imapx200_timer_init - 120 +imapx200_init_clocks - 1104 +imapx200_timer_init - 124 imapx200_register_device - 56 imap_init_pwm - 308 imapx200_fixup - 36 @@ -542,7 +542,7 @@ imapx200_i2c_driver - 116 imapx200_i2c_driver - 116 imapx200_decode_driver - 80 -imapx200_decode_fops - 144 +imapx200_decode_fops - 148 imapx200_encode_driver - 80 imapx200_encode_fops - 180 sdhci_imap_driver - 80 

The size of the core as a whole decreased by 416, but the gain was not the way I expected it: some functions gained weight.

Perhaps a more experienced reader knows the reason, but at that time I was not so clear. Consider the source code of the imapx200_timer_ack function assembler, the size of which increased by 8 bytes after modification:
to show
 --- 0xc0019c40-t-imapx200_timer_ack-2.listing 2015-11-18 22:12:24.196113878 +0300 +++ 0xc0019c50-imapx200_timer_ack-2.listing 2015-11-18 22:12:24.297113880 +0300 @@ -9,10 +9,12 @@ XXXXXXXX: e92dd800 push {fp, ip, lr, pc} XXXXXXXX: e24cb004 sub fp, ip, #4 ; 0x4 XXXXXXXX: e1a00000 nop (mov r0,r0) +c0019c60: e59f2018 ldr r2, [pc, #24] ; c0019c80 <_binary_0xc0019c50_imapx200_timer_ack_start+0x30> XXXXXXXX: e590Y000 ldr rY, [r0] XXXXXXXX: e3a0X001 mov rX, #1 ; 0x1 -c0019c58: e3a0120f mov r1, #-268435456 ; 0xf0000000 +c0019c6c: e5921000 ldr r1, [r2] XXXXXXXX: e1a0XX1Y lsl rX, rX, rY XXXXXXXX: e581X010 str rX, [r1, #16] XXXXXXXX: e581X000 str rX, [r1] XXXXXXXX: e89da800 ldm sp, {fp, sp, pc} +c0019c80: c04e858c subgt r8, lr, ip, lsl #11 

The first thing that catches your eye: the base address itself is so good that the entry in the register fits into four bytes of the mov command:
 c0019c58: e3a0120f mov r1, #-268435456 ; 0xf0000000 

Second: after modification, the compiler had to add the address of our new constant after the epilog of the function; its less convenient value is four bytes times.

One last thing: it is impossible to read values ​​directly from memory, therefore, the address of the constant is first written to the register:
 c0019c60: e59f2018 ldr r2, [pc, #24] ; c0019c80 
- four bytes two.

Conclusion ^


I think that the objective conclusion will be what you need to know about the consequences of defining constants both in the form of macros and using const variables.

Personally, I dream that the compiler (albeit in conjunction with the linker) independently made the decision to embed the value of a constant without difficulties with extern const.

Thanks for attention!

List of sources ^


  1. ^ 1 2 3 Calculate the circumference
  2. ^ And once again about unique constants
  3. ^ Purchase history and experience using the Zenithink ZT-180 Tablet PC
  4. ^ Zenithink Zt-180 10 "Tablet Review
  5. ^ page is no longer available, look in various web archives for habrahabr.ru/blogs/iTablet/110714
  6. ^ page is no longer available, look in various web archives for request www.good-review.ru/pandawill/2011/02/21/obzor-kitayskogo-plansheta-zenithink-zt-180-10.html
  7. Relocation of the soul: linux on android tablet
  8. ^ Photos of entrails
  9. ^ Zimage unpack and pack tools
  10. ^ Decompiling 'firmware2' and 'firmware_discovery'
  11. ^ Disassembly: Smashing the Android Kernel for Fun and Overclock
  12. ^ Topic on forum.china-iphone.ru, dedicated to the tablet
  13. ^ Open source project
  14. ^ github.com/atpboy444/ZT-180
  15. ^ github.com/dandel/linux-2.6.32.y
  16. ^ SlackwareARM-14.1
  17. ^ https://github.com/djwillis/meta-raspberrypi/issues/38
  18. ^ https://lkml.org/lkml/2012/7/6/260
  19. ^ 1 2 3 4 github.com/gshep/flytouch2-helper-scripts
  20. ^ Scott Mayers. Effective use of C ++. 55 sure tips to improve the structure and code of your programs
  21. ^ Initialization order of globals

Source: https://habr.com/ru/post/270885/


All Articles