Abstract

Apple has a strict limit on the size of iOS apps. Exceeding the limit will prevent users from downloading apps over cellular networks, directly affecting the conversion of new users. Exceeding the executable file limit will lead to the rejection of App approval, directly affecting the launch. Toutiao explores the implementation of the __TEXT segment migration technology, successfully reducing the download size by 32% and solving the problem of limited executable file size.

I. Background knowledge

1. Download size limit

App size has the concept of download size and install size.

Download size refers to the space occupied by the compressed App package (i.e..ipa file). When users download the App, they download the compressed package, which can save traffic. When the compressed package is downloaded, it is automatically decompressed. The decompression process is commonly referred to as the installation process. The installation size refers to the space occupied by the compressed package after decompression.

The install size is visible on the App Store and generally affects the user’s willingness to download:

The download size can only be seen by developers in the background of App Store Connect, but not by users. It affects the traffic consumption and duration of the download:

If the download size exceeds the limit, you will not be able to use the cellular network to download the App (before iOS 13), and will receive a message indicating that the file capacity is too large. You need to download the App over the Wi-Fi network. Here’s how Apple’s App download size limit has changed over the years:

  • In July 2008, the iPhone 3G with the App Store was released with a download limit of 10 MB
  • In February 2010, Apple raised the download limit for the iPhone 3G from 10 MB to 20 MB
  • In March 2012, with the release of iOS 5.1, the download limit was increased from 20 MB to 50 MB
  • In September 2013, the download limit of iOS 7 was increased from 50 MB to 100 MB
  • In September 2017, the download limit was increased from 100 MB to 150 MB with the release of iOS 11
  • In May 2019, the download limit was increased from 150 MB to 200 MB
  • When iOS 13 becomes available in September 2019, users will be able to choose whether to download it over a cellular network if the download size exceeds 200 MB

Nowadays, when the App download size exceeds 200 MB, two things happen:

  • Users with iOS 13 or lower cannot download apps using cellular data

  • Users with iOS 13 or higher will need to manually set up to download apps over cellular networks

2. Limit the size of executable files

According to the description of maximum build file size [1], Apple also has a clear limit on executable file size, exceeding which will result in App approval rejection:

ERROR: ERROR ITMS-90122: "Invalid ExecutaBe Size. The size of your app's executaBe file 'News.app/News' is 68534272 bytes for architecture 'arm64', which exceeds the maximum allowed size of 60 MB."
Copy the code

Specific restrictions are as follows:

  • Before iOS 7, all binaries__TEXTThe sum of segments must not exceed 80 MB
  • IOS 7.x to iOS 8.x, binaries, in each particular architecture__TEXTSegments must not exceed 60 MB
  • After iOS 9.0, all binaries__TEXTThe sum of segments must not exceed 500 MB

Two, facing problems

As the Internet has spread and data costs have fallen, Apple has relaxed the restrictions. However, if the download size exceeds 200 MB, you can be sure that there will still be some impact on new additions. This is a huge loss for the App with hundreds of millions of users. In line with the concept of pursuing perfection and getting the top spot among competitors, we consider the download size of 200 MB as a red line for package size.

The download size of Toutiao App is close to 180 MB, but after years of extreme optimization (including but not limited to code/image/other resource optimization, compilation/link parameter optimization, promotion of useless business offline, access port, etc.), it is difficult to reduce it significantly. The platform and the parties invested a lot of effort, even at the expense of business iteration space, to optimize/suppress download size.

In the second half of 2020, we explored and implemented a new method of __TEXT segment migration: we moved some sections of the __TEXT segment of the executable file to other segments, which improved the compression efficiency of the executable file and reduced the download size of the App by 60 MB.

This solution completely solves the problem of download size limits, as well as the problem of executable file size limits for apps that still support iOS 8.x.

Three, technical principle

1. Introduction to The Mach-O file format

The iOS executable file is in Mach-O format and consists of Header, Load Commands, and Data.

It can be simply considered as:

  • HeaderProvides general information about a file.
  • Load CommandsBy the multipleLoad CommandComposition, they describeDataLayout information in binary files and virtual memory, with this layout information can be knownDataHow it’s laid out in binary files and virtual memory, it’s kind of like a blueprint for fixing a house.
  • DataStores the actual content, mainly the instructions and data of the program, which are arranged exactly according toLoad CommandsIn the description.

The Data part of mach-O file is mainly organized by the way of Segment and Section. Just like there are grades and classes in schools and departments and groups in companies, the content with common characteristics can be organized together to facilitate management and improve efficiency.

$xcrun size-lm

to view the size of the Data part of the Mach-o file and the size of each Segment/Section From the template project construction). This command is handy when more detailed information is not needed.

The figure above shows the basic information about content layout in Data.

According to the figure, in this document:

  • There are 5 segments in the Data section.

    • __PAGEZERO
    • __TEXT
    • __DATA_CONST
    • __DATA
    • __LINKEDIT
  • In addition to__PAGEZERO__LINKEDITThere are more than one in each segmentSection.

Note: Data and __DATA are two different concepts. Data is part of the Mach-O file and contains multiple segments. __DATA is just a segment of Data.

__PAGEZERO is 4 GB in size, but not its true size in mach-O files. This is the size of __PAGEZERO after Mach-o has loaded it into memory. It is not readable or writable, and is mainly used to catch references to NULL Pointers. An EXC_BAD_ACCESS error is raised if the __PAGEZERO segment is accessed. __PAGEZERO doesn’t actually take up space in the Data section in Mach-o.

__TEXT, __DATA_CONST, and __DATA are used to hold program code instructions and data.

__LINKEDIT contains the information needed to start the App, such as the address of bind & Rebase, code signature, symbol table, etc.

2. The principle of __TEXT segment migration

The program construction process consists of four main stages: pre-processing -> compilation -> assembly -> linking. After completion, the mach-O executable file is produced.

$man ld -rename_section orgSegment orgSection newSegment newSection Use this parameter to change the name of orgSegment/orgSection to newSegment/newSection.

You can pass this parameter in Other Linker Flags. Such as:

-Wl,-rename_section,__TEXT,__text,__BD_TEXT,__text
-Wl,-segprot,__BD_TEXT,rx,rx
Copy the code

Where -wl tells Xcode that the parameters following it are added to the Ld linker and will take effect during the link phase.

The first line of arguments creates a new __BD_TEXT section and moves __TEXT, __TEXT, to __BD_TEXT, __TEXT.

The second line of arguments grants readable and executable permissions to __BD_TEXT.

After the build is complete, let’s look at the mach-o file after moving __TEXT, __TEXT:

As you can see, __TEXT has been moved to __BD_TEXT and its address has changed from 0x100005e5c to 0x100010000. This is because the operating system only cares about the read/write/execute permissions of the segment, not the name of the segment or section. Even if -rename_section is used to move the Segment/Section, the address of each symbol is corrected by the linker, so the program can run properly after the Segment is moved.

Back in the days of minimum support for iOS 8, many large apps experienced problems with the __TEXT segment in the executable exceeding 60 MB. Facebook [2] used the -rename_section technique to avoid this problem. The link parameters they use are:

-Wl,-rename_section,__TEXT,__cstring,__RODATA,__cstring
-Wl,-rename_section,__TEXT,__gcc_except_tab,__RODATA,__gcc_except_tab
-Wl,-rename_section,__TEXT,__const,__RODATA,__const
-Wl,-rename_section,__TEXT,__objc_methname,__RODATA,__objc_methname
-Wl,-rename_section,__TEXT,__objc_classname,__RODATA,__objc_classname
-Wl,-rename_section,__TEXT,__objc_methtype,__RODATA,__objc_methtype
Copy the code

The __cstring, __gCC_EXCEPt_tab, __const, __objc_methName, __objc_classname, and __objc_methType parameters in __TEXT are moved to __RODATA. Since the six sections are read-only, they named the new segment __RODATA, which means read-only segment. After doing this, the size of __TEXT is reduced, and Apple only scans __TEXT segment, so when __TEXT segment is reduced below 60 MB, the problem of __TEXT segment exceeding 60 MB is avoided. This scheme is also common in large domestic apps at that time.

Toutiao also adopted this solution after encountering this problem in May 2018, in order to avoid the problem of the __TEXT segment exceeding 60 MB. Now the test shows that the above parameters are also optimized for the download size of 12 MB.

Why does moving the __TEXT section reduce the download size? This is explained in detail in the next section.

Note that using -rename_section requires turning off Bitcode.

3. Principle of download size reduction

From apple official documentation [3] :

When your app is approved for the App Store, it is encrypted with DRM and recompressed. The added encryption and DRM affects the ability to compress your binary, and as a result you may see a larger App Store file size for your binary than the binary you uploaded on App Store Connect. The exact final size for your app cannot be determined in advance to the accuracy of a single byte.

After archiving the project, an.xcarchive file is generated, which contains App, dsYMS, and other information. Here is what is contained in the.xcarchive file:

After uploading the.xcarchive file to App Store Connect, Apple will DRM encrypt the executable files in the App and compress the App into an IPA file before publishing it to the App Store. Encryption has little effect on the size of the executable itself (2 MB for Toutiao App), but it can seriously affect the compression efficiency of the executable, resulting in an increase in the compressed IPA size, i.e. the download size.

In practice, the encryption is almost useless and can be easily decrypted using commercially available unmasking tools on any jailbroken phone.

Decryption of the Mach-O file code occurs when the Mach-O file is loaded by the Mach Loader. The Mach Loader reads the LC_ENCRYPTION_INFO Load Command in Mach-O to determine whether the executable is encrypted.

You can also run the otool -l

command to check whether Mach -o was encrypted.

Load  command 13
          cmd LC_ENCRYPTION_INFO_64
      cmdsize 24
     cryptoff 16384
    cryptsize 101695488
      cryptid 1
          pad 0
Copy the code

Cryptoff indicates that the encryption field is located in the file with an offset of 16384 bytes. Cryptsize indicates that the length of encrypted content is 101695488 bytes; Cryptid indicates that the encryption method is 1. If the value is 0, no encryption is required.

Check the range of the __TEXT segment in LC_SEGMENT_64

Load command 1
      cmd LC_SEGMENT_64
  cmdsize 1432
  segname __TEXT
   vmaddr 0x0000000100000000  4294967296
   vmsize 0x0000000006100000  101711872
  fileoff 0
 filesize 101711872
Copy the code

From the above results we can calculate that the encrypted content is actually in __TEXT.

You can assume that Apple encrypts only the __TEXT section of the Mach-O file and not the other sections. As long as the __TEXT section can be moved to another section, it can reduce apple’s encryption range, thus improving compression efficiency and reducing the download size. This also answers the question posed in the previous section.

Generally speaking, the executable file accounts for 80% of the size of the App, while the encrypted part accounts for 70% of the executable file. Encryption will affect 60% of the compression rate, so removing the encrypted part will increase the download size by 34%. Based on our experience with multiple apps, this solution can reduce download sizes by 32~34%.

Note that:

Apple has optimized the download size for iOS 13, so this solution cannot further optimize the download size for iOS 13 devices.

That is, if the user’s device is < iOS 13, this scheme can reduce the download size of App on the device by 32~34%;

If the user’s device >= iOS 13, this scheme will not further optimize the download size of the App on the device, nor will it have a negative impact.

So, don’t be surprised if you see the download size of the App Store Connect background display drastically decrease from the iPhone 11, which started with iOS 13+ by default.

It is speculated that Apple is also optimizing for compression in iOS 13, either by removing encryption or by compression before encryption.

Apple described in the iOS 13 update log [4] that they had optimized the package size, as shown below:

Four, the practice

By removing all sections from the __TEXT section, you can minimize the download size.

Is it that simple? Not really. On small apps, there’s no problem doing this. But on larger apps, this is not an easy task.

In practice, the Toutiao App solved the problem of Crash and an extremely difficult link failure.

1. Crash

The cause of Crash is that the specified section could not be found while executing the code.

The operating system only cares about the read/write/execute permissions of segments, not the names of segments or sections. Even if -rename_section is used to move the Segment/Section, the address of each symbol is corrected by the linker, so the program can run properly after the Segment is moved.

But if the code specifies a Section in __TEXT to be read, the Section cannot be moved, otherwise the code cannot read it, and an error occurs.

First, dyld[5] checks the __unwind_info and __eh_frame sections during startup. If you move these two sections, the program will Crash after startup.

Second, Swift related sections cannot be moved, otherwise a Crash will occur.

After using Swift, there are some Swift related sections in the binary:

None of them can be moved. There are the following sections:

__TEXT,__swift5_typeref
__TEXT,__swift5_reflstr
__TEXT,__swift5_fieldmd
__TEXT,__swift5_types
__TEXT,__swift5_capture
__TEXT,__swift5_assocty
__TEXT,__swift5_proto
__TEXT,__swift5_protos
__TEXT,__swift5_builtin
Copy the code

Third, you specify the Section to read in the code. Currently, there is no such Crash situation in our code, but some of our scripts have codes that detect __TEXT and __TEXT. After the __TEXT segment migration, the scripts are affected, so we need to re-adapt such scripts.

2. The link fails

The most difficult problem to solve with __TEXT segment migration is the link failure problem, caused by CPU limits on addressing range and defects in the LD64 linker.

2.1 Overview of symptoms and causes

If the Mach-O file is large enough, moving segments/sections precipitately can easily raise LD64 linker exceptions.

To make a CPU work, it must be supplied with instructions and data, which are stored in memory while the program is running. The CPU uses the address bus to specify the address of a memory unit. The width of the address bus determines the ADDRESSING capability of the CPU, so the CPU has certain limits on the addressing range. Different cpus have different address bus widths and instruction modes [6], so different cpus have different addressing ranges.

B, BL instruction is ARM processor jump instruction, can let the processor jump to the specified target address, from there to continue execution. Since the addressing range is limited, the jump distance cannot exceed this limit. Ld64 linker will check all jump instructions when the final Output (write executable file), and if the jump distance exceeds the limit, it will immediately throw ld: B (L) ARM64 branch out of range exception, so that the link fails, as shown in the figure.

As summarized in apple’s open source LD64-530 outputfile.cpp file [7], the specific addressing range of common CPUS is as follows:

2.2 What the LD64 linker does

According to the above description, with the expansion of the business, the expansion of the code, the Mach-O file will be bigger and bigger, so the application cannot link successfully if the mach-O file is too large.

Of course not! In fact, THE LD64 linker knows that the jump distance will exceed the limit, so it performs the Branch Island[8] algorithm during the link process to protect the jump instructions that exceed the limit.

PowerPC can do PC relative branches as far as +/-16MB insert one or more // “branch islands” between the branch and its target that // allows island hopping to the target. // Branch Island Algorithm // // If the __TEXT Segment < 16MB, then no branch islands needed // Otherwise, every 14MB into the __TEXT Segment a region is // added which can contain branch islands. Every out-of-range // B instruction is checked. If it crosses a region, an island // is added to that region with the same target and the B is // adjusted to target the island instead. // // In theory, if too many islands are added to one region, it // could grow the __TEXT enough that other previously in-range // B branches could be pushed out of range. We reduce the // probability this could happen by placing the ranges every // 14MB which means the region would have to be 2MB (512,000 islands) // Before any branches could be pushed out of range.

From the principle part we know that the Data part of Mach-O has many segments/sections. The ld64 linker actually classes each Section, which can be found in line 547 of the Ld. HPP file in Apple’s ld64-530:

Each Section belongs to one of these types. The Branch Island algorithm checks for jumps in sections whose type is typeCode. If the jump is too far, it inserts “Branch islands” between them, and jumps to a Branch Island first. Then jump from the branch island to the target address to ensure that the jump distance does not exceed the limit. The code for this section can be found in the branch_island.cpp file.

The type of __TEXT is typeCode. Therefore, out-of-range jump instructions in __TEXT and __TEXT are protected and branch out of range exceptions will not occur in the last Output check. As a result, properly built apps, even large ones, don’t fail to connect, thanks to the Branch Island algorithm.

In mach-o files, there is only __TEXT, and __TEXT is typeCode (the Section type does not change after moving a Segment/Section using -rename_section). There are only two ways to jump a source address in a __text instruction: __text -> __text and __text -> __stubs.

The Section protected by Branch Island and the Section where the target address resides are only two cases:

But in fact when Output, ld64 linker will check all jump instructions in the file, not only limited to the source address in __text jump instructions. This means checking for multiple conditions:

Summary: The Branch Island algorithm only protects jump instructions in __text that are out of bounds. When Output, the LD64 linker checks whether all jump instructions in the file exceed the limit.

2.3 Defects of the Branch Island algorithm

Since the Branch Island algorithm protects sections whose types are typeCode from out-of-limit jump instructions, -rename_section does not change the Section’s type. -rename_section causes branch out of range exception.

There are two main reasons:

1. The checking logic of the Branch Island algorithm does not adapt to the case where the Section is moved.

In analyzing the Mach-O file, only segments/sections were introduced, and the actual linker thought there was atom in sections (the basic unit of links) and fixup in Atom (which describes the reference relationships between different Atoms).

Part of the Branch Island algorithm in the BRANch_island. CPP file for LD64-530 is shown in figure. This section is to judge whether the jump instruction jump distance exceeds the limit. If the limit is exceeded, the jump instruction will be protected; otherwise, it will not be protected.

SrcAddr is the source address of the jump instruction, dstAddr is the target address, displacement is the distance between the target address and the source address.

This code calculates srcAddr and dstAddr with offset, which is relative distance:

  • atom->sectionOffset()target->sectionOffset()Are allatomThe distance relative to the starting address of each Section.
  • fit->offsetInAtomaddendAre allfixupRelative to each otheratomIn the distance.

Therefore, the calculated srcAddr and dstAddr are the distances of the fixUp relative to the start address of their respective sections. Displacement is calculated by subtracting the dstAddr and srcAddr to calculate the distance between the dstAddr and srcAddr. In the absence of -rename_section, this calculation is fine; Using -rename_section results in an inaccurate calculation of distance displacement, causing scenes that are expected to be protected by jump instructions to not be protected.

2. The Branch Island algorithm does not protect custom sections.

The Branch Island algorithm only protects typeCode sections. A custom Section is classified as typeUnclassified. If the code in a custom Section uses a jump instruction, If the jump instruction is out of range, the link will fail whether -rename_section or not.

The following three scenarios are combined to analyze the defects of the Branch Island algorithm in detail.

2.3.1 scene a

__TEXT, the __TEXT shift is not clean causing the link to fail.

The __text section is such a large proportion of the __text section that it must be removed to achieve optimization, otherwise there is little optimization at all. To start with, use -wl,-rename_section,__TEXT, __TEXT, __BD_TEXT, __TEXT to try to migrate __TEXT, __TEXT, but there is always a small part left in __TEXT, __TEXT.

The resulting problem is that the jump distance in __TEXT and __TEXT at the top and __BD_TEXT at the bottom of __TEXT exceeds the limit. Ld64 linker found this error when Output, threw an exception, and the link failed.

We already know that the Branch Island algorithm protects the jump instruction in __text by inserting Branch Island when the jump distance exceeds the limit. So why did it happen?

In the mach-o file, the total size of __TEXT and __TEXT is 110, and there are two symbols, A and B, and the jump instruction will jump from A to B. Their offsets from Section __TEXT and __TEXT are 30 and 90, respectively, and their actual distances are 60. The Branch Island algorithm will protect the jump instruction and calculate the distance between A and B to be 60, so the Branch Island will not be inserted. At Output, the LD64 linker checks that the distance between A and B is 60, less than 128. No exception thrown, link successful.

After removing the 90 size of __TEXT, __TEXT, __TEXT becomes 20, B is moved to __BD_TEXT, __TEXT, The offsets of A and B with respect to their sections will probably change (this is not important), assuming they become 5 and 80, respectively.

At the moment, the actual distance between A and B is 80 + 40 + 15 = 135. However, when protecting Branch Island instructions, the Branch Island algorithm still calculates the distance between them and their sections, and calculates that the distance between them is 80-5 = 75, without inserting Branch Island. The actual size of 135 will be wrong for arm64 and ARMV7.

At the last Output, LD64 calculated the distance according to the absolute address of A and B in the file, and the distance was 135, which exceeded 128. The error caused by moving the Section and the defect of Branch Island algorithm was checked out. Branch out of range exception thrown, connection failed.

Therefore, if you want to move __TEXT, __TEXT, you must ensure that __TEXT, __TEXT all removed, otherwise there may be link failure problems. (This is not a problem if your App executable is small and the jump distance never exceeds 128M.)

__TEXT,_text, __TEXT, __textCOAL_nt, __TEXT,__StaticInit There is the following snippet in the source code:

This code will rename __TEXT, __textcoal_nt, and __TEXT,__StaticInit (merge) to __TEXT, __TEXT, and leave the parts in __TEXT.

__textcoal_nt is a Section generated by GCC. It has been deprecated since at least 16 years ago, but there are still many libraries carrying this Section. __StaticInit does not find much more information.

I found traces of these two sections in Apple’s LD update log [9]. Apple has merged these two sections into __text since 2007 and 2008.

2008-07-15 Nick Kledzik [email protected]

rdar://proBem/6061904 automatically order initializers to start of __TEXT

* src/MachOReaderRelocataBe.hpp: merge __StaticInit into __text



2007-04-30 Nick Kledzik <[email protected]>

<rdar://proBem/5065659> unaBe to link VTK because __textcoal_nt too large

* src/MachOReaderRelocataBe.hpp: when doing a final link map __textcoal_nt to __text

But Apple’s merge happens after our -rename_section, so we use -rename_section,__TEXT, __TEXT, __BD_TEXT, __TEXT without removing both of them.

To make __TEXT and _text clean, just add -rename_section to both. Use the following configuration:

-Wl,-rename_section,__TEXT,__text,__BD_TEXT,__text,
-Wl,-rename_section,__TEXT,__textcoal_nt,__BD_TEXT,__text,
-Wl,-rename_section,__TEXT,__StaticInit,__BD_TEXT,__text
Copy the code

Note: BD in the string __BD_TEXT is an abbreviation for ByteDance, and __BD_TEXT is just a name that can be changed at will. If your App uses -rename_section,__TEXT, __TEXT, __BD_TEXT, __TEXT itself, then it does not contain __TEXT, __textCOAL_nt, and __TEXT,__StaticInit. You do not need to add this configuration.

2.3.2 scenario 2

Not moving __stubs causes the link to fail.

The core of __TEXT segment migration to reduce package size is to remove __TEXT, __TEXT. __TEXT, __TEXT -> __TEXT,__stubs, so if you only move __TEXT, __TEXT without moving __TEXT,__stubs, it will be similar to the situation described in question 1: The Branch Island algorithm checks the distance between symbols in __text and __BD_TEXT, and __stubs is the distance between symbols in __text. This calculation of displacement does not match their actual distance. There was no insert at the place where the branch island was inserted, an error was detected on Output, and an exception was thrown.

But __TEXT,__stubs is a little different:

TotalTextSize is the total size of __TEXT, __TEXT and __TEXT,__stubs.

The code logic describes: If the Section type is typeStub (__stubs in arm64, __picSymbolStub4 in armV7), the Branch Island algorithm makes the jump instruction’s destination address dstAddr totalTextSize, Then we calculate the spacing displacement.

This needs to be graphically analyzed:

As shown, in a normal Mach-o file, __TEXT, __TEXT has a size of 110 and __TEXT,__stubs has a size of 20. The A symbol exists in __TEXT, __TEXT, and the B symbol exists in __TEXT,__stubs. A is 90 from __TEXT, and B is 10 from __TEXT, and the actual distance between A and B is 30.

DstAddr = 130 (110 + 20 = 130); dstAddr = 130-90 = 40; Do not insert branch Island. When the final Output is checked, their actual distance is 30, less than 128, no exception will be thrown, and the link is successful.

After moving __TEXT, __TEXT, A is moved to __BD_TEXT, the actual distance between A and B becomes 10 + 40 + 90 = 140, However, the Branch Island algorithm calculated displacement = 130-90 = 40 and did not insert the Branch Island, which would cause the actual jump error. The LD64 linker detects this error in the final Output phase, throws an exception, and the link fails.

The LD64 linker knows that the B symbol must be in __stubs, so dstAddr = totalSize for the Branch Island algorithm; Will only make the dstAddr larger than it actually is. This ensures that jump instructions that are out of range in __stubs are inserted into branch island. But because the dstAddr is a little bit larger, it actually protects some jump instructions that don’t actually exceed the limit.

The Branch Island algorithm uses this relative distance calculation method because at this stage it cannot get the absolute address of A and B symbols (the absolute address is determined before Output), so it uses A trick. Calculate the distance between A and B using their offsets with respect to their sections. It defaults to only one Section in the binary file whose type is typeCode, and that Section comes before __TEXT and __stubs. This algorithm works perfectly in normal Mach-O files, but if we move segments/sections, it doesn’t work and causes problems.

So add the following arguments to remove __TEXT and __stubs as well:

-Wl,-rename_section,__TEXT,__stubs,__BD_TEXT,__stubs,
-Wl,-rename_section,__TEXT,__picsymbolstub4,__BD_TEXT,__picsymbolstub4
Copy the code

In ARM64, the Section name is __stubs, and in ARMV7, the Section name is __picsymbolstub4. To accommodate different schemas, we can also -rename this Section. – Rename a Section that does not exist does not have a problem, so this is ok.

This imposes another limitation on sections whose type is typeStub (__stubs in ARM64, __picsymbolstub4 in armV7). There can be no other Section between __text and __stubs or __picSymbolstub4, otherwise an error may occur, as shown in the following figure:

In normal Mach-o files, the offset of the A symbol relative to __text is 0 and the offset of the B symbol relative to __stubs is 17.5. If we move another Section after __text and __stubs, the Section may appear between __BD_TEXT,__text and __BD_TEXT,__stubs. This will result in an error:

The Branch Island algorithm checks that the distance between A and B is (110 + 17.9) -0 = 127.9, less than 128, so the Branch Island is not inserted. But after moving their actual distance is 110 + 0.5 + 17.5 = 128, it will lead to jump error, so the LD64 linker will throw an exception, link failure.

If A’s offset is 0, then its target address must be in the __stubs range [17.5, 17.90]. Otherwise, the link will fail. The actual distance between A and B will not be more than 128. And the offset of A must be within the range of [0, 0.4]. If A is greater than 0.4, then after __text is moved, A will jump to any position of B within 128M.

Based on this, when we move __cstring, __gCC_except_tab, __const, __objc_methname, __objc_classname, __objc_methType read-only sections, They cannot be moved to the __BD_TEXT section, otherwise they will appear between __BD_TEXT,__text and __BD_TEXT,__stubs and cause errors.

The solution is to use the existing link parameters and move them to another Segment :__RODATA to avoid this problem:

-Wl,-rename_section,__TEXT,__cstring,__RODATA,__cstring  -Wl,-rename_section,__TEXT,__gcc_except_tab,__RODATA,__gcc_except_tab  -Wl,-rename_section,__TEXT,__const,__RODATA,__const  -Wl,-rename_section,__TEXT,__objc_methname,__RODATA,__objc_methname  -Wl,-rename_section,__TEXT,__objc_classname,__RODATA,__objc_classname  -Wl,-rename_section,__TEXT,__objc_methtype,__RODATA,__objc_methtype
Copy the code

2.3.3 scenario 3

Problem with custom sections.

In “2.2 What the LD64 linker does”, it is stated that there are four jump cases. RangeCheck checks these four cases; But the Branch Island algorithm only checks for two jump cases, and it only protects jump instructions in __text.

All jump cases of jump instructions:

The fourth case occurs only if there is a custom Section and the custom Section has jump instructions in it.

Branch Island will be protected:

There are two cases where the Branch Island algorithm does not protect:

__TEXT,__stub_helper      ->        __TEXT,__stub_helper
__TEXT,__custom_section   ->        __TEXT,__text
Copy the code

The reason is that Branch Island only checks jump instructions in sections whose type is typeCode, and only __TEXT, whose type is typeCode.

So, these two cases of jump instructions, in the actual jump will be wrong?

  1. __TEXT,__stub_helper -> __TEXT,__stub_helperNo, because__stub_helperIts size is only 28KB (in the headline), which is much smaller than 128MB, so its internal instructions never jump out of bounds.
  1. __TEXT,__custom_section -> __TEXT,__textIt is possible to fail.

We’ve seen two cases of custom sections.

A. There are two sections in an App: __dof_RACSignal and __dof_RACCompou.

These two sections are introduced by RAC, but their Number of Relocations is 0. There is no Relocations instruction involved. They do not need to be processed.

B. There is an __u_selector Section in the headline:

It is imported from a static library, and __u_selector contains a relocation symbol ___Symbol_A, from which the jump instruction jumps to ___Symbol_B in __text.

Debugging found that in normal executable files, the distance between them is about 10M. There will be no link failures.

It can be assumed that ___Symbol_B is actually at the bottom of __text, which is very large. If __text is moved below __u__selector, the distance between the two instructions increases, and links fail beyond 128 MB. As shown in figure:

So after moving __text, the __custom_section (the custom Section with the jump instruction) must also be moved, keeping it below __text in their original relative positions.

According to this analysis, custom sections in __TEXT are not protected by Branch Island. If the binary file is large enough and the Section has jump instructions, the link will fail if the jump distance exceeds 128 MB, regardless of whether __TEXT is moved or not.

To remove the custom Section, add the following configuration:

-Wl,-rename_section,__TEXT,__custom_section,__CUSTOM_TEXT,__custom_section
Copy the code

Instead of putting custom sections in __BD_TEXT, the custom sections would appear between __text and __stubs, causing the problem described in the second half of “Scenario 2”.

3. Set the segment permission

The executable code is moved to the new __BD_TEXT and __CUSTOM_TEXT segments. Add readable and executable permissions to these segments, otherwise the program will not run:

-Wl,-segprot,__CUSTOM_TEXT,rx,rx
-Wl,-segprot,__BD_TEXT,rx,rx
Copy the code

Five, a line of code

In the Toutiao App, xcConfig [10] is used to manage the build parameters. If you use this method, you can complete the configuration with this line of code:

APP_THIN_LINK_FLAGS = -Wl,-rename_section,__TEXT,__cstring,__RODATA,__cstring,-rename_section,__TEXT,__objc_methname,__RODATA,__objc_methname, -rename_section,__TEXT,__objc_classname,__RODATA,__objc_classname,-rename_section,__TEXT,__objc_methtype,__RODATA,__objc _methtype,-rename_section,__TEXT,__gcc_except_tab,__RODATA,__gcc_except_tab,-rename_section,__TEXT,__const,__RODATA,__co nst,-rename_section,__TEXT,__text,__BD_TEXT,__text,-rename_section,__TEXT,__textcoal_nt,__BD_TEXT,__text,-rename_section ,__TEXT,__StaticInit,__BD_TEXT,__text,-rename_section,__TEXT,__stubs,__BD_TEXT,__stubs,-rename_section,__TEXT,__picsymbo lstub4,__BD_TEXT,__picsymbolstub4,-segprot,__BD_TEXT,rx,rxCopy the code

If you don’t use this option, add the following configuration line by line in Other Linker Flags:

-Wl,-rename_section,__TEXT,__cstring,__RODATA,__cstring
-Wl,-rename_section,__TEXT,__objc_methname,__RODATA,__objc_methname
-Wl,-rename_section,__TEXT,__objc_classname,__RODATA,__objc_classname
-Wl,-rename_section,__TEXT,__objc_methtype,__RODATA,__objc_methtype
-Wl,-rename_section,__TEXT,__gcc_except_tab,__RODATA,__gcc_except_tab
-Wl,-rename_section,__TEXT,__const,__RODATA,__const
-Wl,-rename_section,__TEXT,__text,__BD_TEXT,__text
-Wl,-rename_section,__TEXT,__textcoal_nt,__BD_TEXT,__text
-Wl,-rename_section,__TEXT,__StaticInit,__BD_TEXT,__text
-Wl,-rename_section,__TEXT,__stubs,__BD_TEXT,__stubs
-Wl,-rename_section,__TEXT,__picsymbolstub4,__BD_TEXT,__picsymbolstub4,
-Wl,-segprot,__BD_TEXT,rx,rx
Copy the code

If you have custom sections in your binary, for example, creating a custom Section like __attribute__((Section (“__TEXT,__custom_section”))), You may need to perform the following operations to remove the custom Section. For details, see 2.3.3 Scenario 3.

APP_THIN_LINK_FLAGS = -Wl,-rename_section,__TEXT,__cstring,__RODATA,__cstring,-rename_section,__TEXT,__objc_methname,__RODATA,__objc_methname, -rename_section,__TEXT,__objc_classname,__RODATA,__objc_classname,-rename_section,__TEXT,__objc_methtype,__RODATA,__objc _methtype,-rename_section,__TEXT,__gcc_except_tab,__RODATA,__gcc_except_tab,-rename_section,__TEXT,__const,__RODATA,__co nst,-rename_section,__TEXT,__text,__BD_TEXT,__text,-rename_section,__TEXT,__textcoal_nt,__BD_TEXT,__text,-rename_section ,__TEXT,__StaticInit,__BD_TEXT,__text,-rename_section,__TEXT,__stubs,__BD_TEXT,__stubs,-segprot,__BD_TEXT,rx,rx,-rename_ section,__TEXT,__picsymbolstub4,__BD_TEXT,__picsymbolstub4,-rename_section,__TEXT, __custom_section,__CUSTOM_TEXT,__text,-segprot, __CUSTOM_TEXT,rx,rxCopy the code

Six, answering questions

1. Why not__TEXTWouldn’t it be better to remove all sections of the Section?

It’s not that the more segments you remove, the more effective the compression is, but it depends on the size of the removal. For example, there are 15 sections below, but their size adds up to 578 KB. Removing them would have almost no effect on the compressed download size.

Section __stubs: 28488 (addr 0x105f21644 offset 99751492)
Section __stub_helper: 28428 (addr 0x105f2858c offset 99779980)
Section __swift5_typeref: 2216 (addr 0x105f2f498 offset 99808408)
Section __swift5_fieldmd: 1272 (addr 0x105f2fd40 offset 99810624)
Section __swift5_types: 120 (addr 0x105f30238 offset 99811896)
Section __const: 64184 (addr 0x105f302b0 offset 99812016)
Section __ustring: 281012 (addr 0x105f3fd68 offset 99876200)
Section __swift5_reflstr: 796 (addr 0x105f84720 offset 100157216)
Section __swift5_capture: 376 (addr 0x105f84a3c offset 100158012)
Section __swift5_builtin: 120 (addr 0x105f84bb4 offset 100158388)
Section __swift5_assocty: 312 (addr 0x105f84c2c offset 100158508)
Section __swift5_proto: 308 (addr 0x105f84d64 offset 100158820)
Section __swift5_protos: 40 (addr 0x105f84e98 offset 100159128)
Section __u__selector: 36 (addr 0x105f84ec0 offset 100159168)
Section __eh_frame: 184708 (addr 0x1060cf018 offset 101511192)
Copy the code

In addition, some sections cannot be removed, which may cause crash. Interested readers can try by themselves.

2. What if crash. log cannot be resolved?

After going online, we found that some symbols in Crash. Log in Crash Report could not be resolved, as shown in the figure?? .

The reason for this problem was that crash. log had the wrong address range in virtual memory when analyzing the master binary image.

Figure 0x100010000-0x100203FFF has a range of only 2047999 (2.0 MB), which is significantly smaller than the original size of __text in the main binary of 100 MB. This size of 2.0MB roughly matches the size of the __TEXT segment left after the migration, so it is assumed that crash. log took the size of the __TEXT segment during the analysis, and we removed most of the __TEXT segment.

So when a symbol falls within (2.0m, 100M), crash. log does not know which mirror the address belongs to. , cannot parse.

Solution: This crash. log is manually parsed using the ATOS tool [11], passing in the name of the master mirror as a parameter.

Seven,

In this paper, from the background knowledge and the actual problems faced, introduced the principle of __TEXT section migration and reduce the download size, described the problems we encountered in the process of practice, and from the source point of view of the detailed analysis of the root cause of the problem and the solution, answered the relevant questions and online problems encountered.

At present, this scheme has been applied in several large Bytedance apps, all of which have optimized the download size by more than 30% and run stably.

Join us

We are the iOS team of Bytedance General Information Platform – Client Platform. We are deeply engaged in performance optimization, basic components, business architecture, R&D system, security compliance, offline quality infrastructure, and online problem locating and attribution Platform. Responsible for ensuring and improving the product quality and development efficiency of Toutiao, watermelon video and tomato novel, and extending outward while focusing on these.

If you’re passionate about technology, you love to push the envelope, and you want to change the experience of hundreds of millions of people with your own code, join us. We look forward to your growing with us. At present, we have recruitment requirements in Beijing and Shenzhen, resume email: [email protected]; Email subject: Name – Years of service – GIP – Client platform architecture – iOS/Android.

reference

[1] Maximum build size help.apple.com/app-store-c…

[2] Analysis of Facebook App blog.timac.org/2016/1018-a…

[3] App Store Connect Help help.apple.com/app-store-c…

[4] iOS 13 Update Log support.apple.com/en-us/HT210…

[5] dyld check Section source opensource.apple.com/source/dyld…

[6] the ARM architecture en.wikipedia.org/wiki/ARM_ar…

[7] ld64-530 opensource.apple.com/source/ld64 OutputFile source code…

[8] Branch Island algorithm opensource.apple.com/source/ld64…

[9] Ld update log opensource.apple.com/source/ld64…

[10] xcconfig introduction nshipster.com/xcconfig/

[11] atos man page

www.manpagez.com/man/1/atos/

More share

Tiktok Android performance optimization series: Java memory optimization

Watermelon video stability management system construction I: Tailor principle and practice

Practice of order replenishment in tripartite payment system based on finite state machine and message queue

UME – Rich Flutter debugging tools


Welcome to Bytedance Technical Team

Resume mailing address: [email protected]