apple 為什麼要改 objc_msgSend 的類型申明

来源:https://www.cnblogs.com/pencilCool/archive/2022/06/25/16410845.html
-Advertisement-
Play Games

Android multiple back stacks導航 談談android中多棧導航的幾種實現. 什麼是multiple stacks 當用戶在app里切換頁面時, 會需要向後回退到上一個頁面, 頁面歷史被保存在一個棧里. 在Android里我們經常說"back stack". 有時候在app ...


原文: https://mikeash.com/pyblog/objc_msgsends-new-prototype.html

總結 :

objc_msgSend 類型申明改變的原因: 讓錯誤在編譯的時候發生,而不是等到運行時。
為什麼有 運行時錯誤 : ABI 的錯配,調用方的ABI (對參數的傳遞) 和 接受方ABI( 對參加的處理)錯配了
為什麼 錯配: 我傳的是float ,你把我當double 了,浮點 轉成 雙精度浮點,這可不是和short 轉 int 單單高位補幾個0就可以了
為什麼float 專 double 了: C 語言 經常搞這種騷操作,畢竟處理數據的時候,用更高的精度有好處。
怎麼阻止 float 轉 double : 可以把函數中的參數類型申明成float ,而不是模棱兩可
等等函數參數 怎麼會模棱兩可: C 語言 變參函數瞭解一下 ,也就是void functionA(int a,int b, ...) 這種點點點函數,不幸原來的objc_msgSend 就是長這個樣子的

翻譯筆記:

objc_msgSend 變了

objc_msgSend 和 objc_msgSendSuper 的類型申明改了,那他們實際上接受什麼參數,以及它實際上返回什麼?

objc_msgSend 不得不用彙編

objc_msgSend 是用彙編實現的,不只是為了快,只用 C 是實現不了objc_msgSend的功能是的(pencilCool: 因為不能去擾亂傳參的寄存器呀 )

objc_msgSend 做的基礎工作

objc_msgSend的fast path 做了幾件關鍵的事情:

  • 載入對象的類
  • 在該類的方法緩存中查找selector 。
  • 跳轉到在緩存中找到的方法實現。

objc_msgSend 不幹擾函數傳參的寄存器

因為objc_msgSend直接跳到了方法實現,而沒有進行函數調用,所以一旦它的工作完成,它就有效地消失了。
這個實現很謹慎,沒有干擾任何可以用來向函數傳遞參數的寄存器。
調用者在調用objc_msgSend時,就像直接調用方法實現一樣,以同樣的方式傳遞所有的參數。
一旦objc_msgSend查找到實現並跳轉到它,這些參數仍然是實現所期望的位置。當實現返回時,它直接返回給調用者,而返回值是由標準機制提供的。

這就回答了上面的問題:objc_msgSend的原型是它最終調用的方法實現的原型。

舊的原型在某些情況下可以工作(當ABI匹配時),而在其他情況下卻奇怪地失敗(當ABI不匹配時)。

新的原型永遠不會工作,除非你先把它投到適當的類型。只要你把它投到正確的類型上,它就總是能工作。因此,新的做事方式鼓勵正確做事,並使做錯事變得更難。

最小的原型

儘管objc_msgSend的原型取決於將被調用的方法實現,但有兩件事在所有的方法實現中是共同的:第一個參數總是id self,第二個參數總是SEL _cmd。objc_msgSend需要這兩個信息來執行它的方法調度工作,所以它們必須總是在同一個地方,以便它能夠找到它們。

我們可以為objc_msgSend寫一個近似的泛化原型來表示這一點。

objc_msgSend(id self, SEL _cmd, ???)

其中,??的意思是我們不知道,它取決於將被調用的特定方法實現。當然,C語言沒有辦法表示這樣的通配符。

對於返回值,我們可以嘗試挑選一些常見的東西。因為Objective-C是關於對象的,所以假設返回值是id是有意義的。

id objc_msgSend(id self, SEL _cmd, ???)

這不僅涵蓋了返回值是一個對象的情況,也涵蓋了返回值是無效的情況,以及其他一些返回值是不同類型但沒有被使用的情況。

那麼參數呢?C語言實際上有一種方法來表示任意類型的任意數量的參數,其形式是可變參數函數原型。在參數列表的末尾有一個省略號,意味著後面有一個任意類型的可變數量的值。

id objc_msgSend(id self, SEL _cmd, ...)

這正是最近改變之前的原型。

ABI錯配

運行時的相關問題是調用地點的ABI是否與方法實現的ABI匹配。也就是說,接收者是否會從相同的位置,以調用者傳遞的相同格式檢索參數?如果調用者將一個參數放入 $rdx,那麼實現者就需要從 $rdx中獲取該參數,否則就會出現混亂。

最小的原型可能能夠表達傳遞任意數量的任意類型的概念,但是為了讓它在運行時真正發揮作用,它需要使用與方法實現相同的ABI。該實現幾乎肯定使用了不同的原型,並且通常有固定數量的參數。

不能保證一個變數函數的ABI與一個有固定參數數的函數的ABI相匹配。在一些平臺上,它們幾乎完全匹配。在其他平臺上,它們完全不匹配。

英特爾ABI

讓我們看一個具體的例子。macOS使用x86-64的標準System V ABI。ABI中有大量的細節,但我們將專註於基礎知識。

參數是在寄存器中傳遞的。整數參數在寄存器rdi、rsi、rdx、rcx、r8和r9中傳遞,按順序排列。浮點參數在SSE寄存器xmm0到xmm7中傳遞。當調用一個變數函數時,寄存器 al 被設置為用於傳遞參數的SSE寄存器的數量。整數返回值被放在rax和rdx中,浮點返回值被放在xmm0和xmm1中。

變參函數的ABI幾乎與普通函數的ABI相同。唯一的例外是傳遞al中使用的SSE寄存器的數量。然而,當使用變參函數 ABI調用普通函數時,這一點是無害的,因為普通函數會忽略al的內容。

C語言把事情搞得有點亂。C語言規定,當作為變數參數傳遞時,某些類型會被提升為更廣泛的類型。小於int的整數(如char和short)被提升為int,而float被提升為double。如果你的方法簽名中包括這些類型之一,那麼如果調用者使用變數原型,就不可能將參數傳遞為該確切的類型。

對於整數來說,這實際上並不重要。整數被存儲在相應的寄存器的底層位,無論哪種方式,其位數最終都在同一個地方。然而,這對浮點數來說是災難性的。將一個較小的整數轉換為int只需要用額外的位來填充它。將float轉換為double涉及到將數值完全轉換為一個不同的結構。float中的位與double中的相應位並不一致。如果你試圖使用一個變數原型來調用一個接受浮動參數的非變數函數,該函數將收到垃圾。

To illustrate this problem, here's a quick example:

    // Use the old variadic prototype for objc_msgSend.
    #define OBJC_OLD_DISPATCH_PROTOTYPES 1

    #import <Foundation/Foundation.h>
    #import <objc/message.h>

    @interface Foo : NSObject @end
    @implementation Foo
    - (void)log: (float)x {
        printf("%f\n", x);
    }
    @end

    int main(int argc, char **argv) {
        id obj = [Foo new];
        [obj log: (float)M_PI];
        objc_msgSend(obj, @selector(log:), (float)M_PI);
    }

It produces this output:

    3.141593
    3370280550400.000000

正如你所看到的,當寫成消息發送時,這個值是正確的,但是當通過顯式調用objc_msgSend時,這個值被完全扭曲了。

這個問題可以通過轉換objc_msgSend的簽名來解決。回顧一下,objc_msgSend的實際原型是最終將被調用的方法的原型,所以使用它的正確方法是將其轉換為相應的函數指針類型。這個調用可以正常工作。

((void (*)(id, SEL, float))objc_msgSend)(obj, @selector(log:), M_PI);

ARM64 ABI

讓我們看看另一個相關的例子。iOS使用了ARM64的標準ABI的一個變體。

整數參數在寄存器x0到x7中傳遞。浮點參數在v0到v7中傳遞。 其他參數在堆棧中傳遞。返回值被放置在作為參數傳遞的同一個或多個寄存器中。

這隻適用於普通參數。變數參數從不在寄存器中傳遞。它們總是在堆棧中傳遞,即使是在有參數寄存器的情況下。

沒有必要仔細分析這在實踐中是如何實現的。ABI是完全不匹配的,一個方法在被調用的時候,如果沒有cast objc_msgSend,那麼它的參數中就會收到垃圾。

新的原型

新的原型簡短而貼心。

void objc_msgSend(void)。

這一點都不正確。然而,舊的原型也不正確。這個原型的錯誤更明顯,這是件好事。舊的原型使你可以很容易地使用它而不去鑄造它,並且經常工作,以至於你很容易就會認為一切都很好。當你遇到有問題的情況時,這些錯誤就非常不清楚了。

這個原型甚至不允許你傳遞self和_cmd這兩個必要參數。你可以在完全沒有參數的情況下調用它,但它會立即崩潰,這應該是很明顯的出錯原因。如果你試圖使用它而不進行轉換,編譯器會抱怨,這比奇怪的破參數值要好得多。

因為它仍然有一個函數類型,你仍然可以把它投到一個適當類型的函數指針上,然後用這種方式調用它。只要你把類型弄對了,這就能正確工作。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 用例演示 - 創建實體 本節將演示一些示例用例並討論可選場景。 創建實體 從實體/聚合根類創建對象是實體生命周期的第一步。聚合/聚合根規則和最佳實踐部分 建議為Entity類創建一個主構造函數,以保證創建一個有效的實體。因此,無論何時我們需要創建實體的實例,我們都應該使用那個構造函數 參見下麵的問題 ...
  • 痞子衡嵌入式半月刊: 第 57 期 這裡分享嵌入式領域有用有趣的項目/工具以及一些熱點新聞,農曆年分二十四節氣,希望在每個交節之日準時發佈一期。 本期刊是開源項目(GitHub: JayHeng/pzh-mcu-bi-weekly),歡迎提交 issue,投稿或推薦你知道的嵌入式那些事兒。 上期回顧 ...
  • 2022年6月初合宙新上市了 Air32F103 系列 MCU, 分 Air32F103CBT6 和 Air32F103CCT6 兩個型號, 分別是 32K RAM + 128K FLASH 和 32K RAM + 256K FLASH, 支持的最高主頻216MHz, 可以Pin2Pin替換STM3... ...
  • ## 電腦性能設置 優化設置一:開啟卓越性能 其實,win10系統中有一個卓越性能的隱藏設置,它可以讓我們的電腦,在現有配置的情況下,發揮出最優良的性能。 1、 同時按住“win+R”打開運行視窗,輸入“powershell”並點擊“確定” 2、 打開命令提示符頁面後,輸入並執行以下字元,就會出現 ...
  • 寫在前面: 這幾天留校,在做一個電機驅動的項目,使用的是合肥傑發的平臺,車規級晶元AC7801/11系列晶元。 但在進行模擬和程式燒錄的時候遇到了各種問題,藉助這個機會,私下裡總結了常見的模擬與燒錄程式常見的幾種方式,以及相關的操作,希望對大家有幫助。 單片機ISP、IAP和ICP幾種燒錄方式的區別 ...
  • 對於 MySQL 資料庫作為各個業務系統的存儲介質,在系統中承擔著非常重要的職責,如果資料庫崩了,那麼對於讀和寫資料庫的操作都會受到影響。如果不能迅速恢復,對業務的影響是非常大的。本文我將分享MySQL 雙主 + Keepalived 的高可用落地和踩坑之路。 ...
  • 1、簡述 binlog 二進位日誌文件,這個文件記錄了MySQL所有的DML操作。通過binlog日誌我們可以做數據恢復,增量備份,主主複製和主從複製等等。 2、Docker中無法使用vim問題解決 https://blog.csdn.net/Tomwildboar/article/details/ ...
  • 當用戶有跨語種交流或音頻內容翻譯的需求時,應用需要能自動檢測語音內容再輸出為用戶需要的語言文字。 HMS Core機器學習服務提供同聲傳譯能力,同聲傳譯實現將實時輸入的長語音實時翻譯為不同語種的文本以及語音,並實時輸出原語音文本、翻譯後的文本以及翻譯文本的語音播報。 在直播類,會議類的應用中,同聲傳 ...
一周排行
    -Advertisement-
    Play Games
  • 概述:在C#中,++i和i++都是自增運算符,其中++i先增加值再返回,而i++先返回值再增加。應用場景根據需求選擇,首碼適合先增後用,尾碼適合先用後增。詳細示例提供清晰的代碼演示這兩者的操作時機和實際應用。 在C#中,++i 和 i++ 都是自增運算符,但它們在操作上有細微的差異,主要體現在操作的 ...
  • 上次發佈了:Taurus.MVC 性能壓力測試(ap 壓測 和 linux 下wrk 壓測):.NET Core 版本,今天計劃準備壓測一下 .NET 版本,來測試並記錄一下 Taurus.MVC 框架在 .NET 版本的性能,以便後續持續優化改進。 為了方便對比,本文章的電腦環境和測試思路,儘量和... ...
  • .NET WebAPI作為一種構建RESTful服務的強大工具,為開發者提供了便捷的方式來定義、處理HTTP請求並返迴響應。在設計API介面時,正確地接收和解析客戶端發送的數據至關重要。.NET WebAPI提供了一系列特性,如[FromRoute]、[FromQuery]和[FromBody],用 ...
  • 原因:我之所以想做這個項目,是因為在之前查找關於C#/WPF相關資料時,我發現講解圖像濾鏡的資源非常稀缺。此外,我註意到許多現有的開源庫主要基於CPU進行圖像渲染。這種方式在處理大量圖像時,會導致CPU的渲染負擔過重。因此,我將在下文中介紹如何通過GPU渲染來有效實現圖像的各種濾鏡效果。 生成的效果 ...
  • 引言 上一章我們介紹了在xUnit單元測試中用xUnit.DependencyInject來使用依賴註入,上一章我們的Sample.Repository倉儲層有一個批量註入的介面沒有做單元測試,今天用這個示例來演示一下如何用Bogus創建模擬數據 ,和 EFCore 的種子數據生成 Bogus 的優 ...
  • 一、前言 在自己的項目中,涉及到實時心率曲線的繪製,項目上的曲線繪製,一般很難找到能直接用的第三方庫,而且有些還是定製化的功能,所以還是自己繪製比較方便。很多人一聽到自己畫就害怕,感覺很難,今天就分享一個完整的實時心率數據繪製心率曲線圖的例子;之前的博客也分享給DrawingVisual繪製曲線的方 ...
  • 如果你在自定義的 Main 方法中直接使用 App 類並啟動應用程式,但發現 App.xaml 中定義的資源沒有被正確載入,那麼問題可能在於如何正確配置 App.xaml 與你的 App 類的交互。 確保 App.xaml 文件中的 x:Class 屬性正確指向你的 App 類。這樣,當你創建 Ap ...
  • 一:背景 1. 講故事 上個月有個朋友在微信上找到我,說他們的軟體在客戶那邊隔幾天就要崩潰一次,一直都沒有找到原因,讓我幫忙看下怎麼回事,確實工控類的軟體環境複雜難搞,朋友手上有一個崩潰的dump,剛好丟給我來分析一下。 二:WinDbg分析 1. 程式為什麼會崩潰 windbg 有一個厲害之處在於 ...
  • 前言 .NET生態中有許多依賴註入容器。在大多數情況下,微軟提供的內置容器在易用性和性能方面都非常優秀。外加ASP.NET Core預設使用內置容器,使用很方便。 但是筆者在使用中一直有一個頭疼的問題:服務工廠無法提供請求的服務類型相關的信息。這在一般情況下並沒有影響,但是內置容器支持註冊開放泛型服 ...
  • 一、前言 在項目開發過程中,DataGrid是經常使用到的一個數據展示控制項,而通常表格的最後一列是作為操作列存在,比如會有編輯、刪除等功能按鈕。但WPF的原始DataGrid中,預設只支持固定左側列,這跟大家習慣性操作列放最後不符,今天就來介紹一種簡單的方式實現固定右側列。(這裡的實現方式參考的大佬 ...